From 1f8d4b1db7226c2ced9e944f0e4648773e35c77e Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 15 Feb 2020 09:20:59 +0100 Subject: [PATCH 01/34] Moved implementation of check for existance of username on creating user from user service to repository. --- .../Persistence/Repositories/IUserRepository.cs | 16 ++++++++++++++++ .../Repositories/Implement/UserRepository.cs | 15 +++++++++++++++ .../Services/Implement/UserService.cs | 4 ++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Abstractions/Persistence/Repositories/IUserRepository.cs b/src/Umbraco.Abstractions/Persistence/Repositories/IUserRepository.cs index 29ae581459..0d1a8764ae 100644 --- a/src/Umbraco.Abstractions/Persistence/Repositories/IUserRepository.cs +++ b/src/Umbraco.Abstractions/Persistence/Repositories/IUserRepository.cs @@ -20,8 +20,24 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// + [Obsolete("This method will be removed in future versions. Please use ExistsByUserName instead.")] bool Exists(string username); + /// + /// Checks if a user with the username exists + /// + /// + /// + bool ExistsByUserName(string username); + + + /// + /// Checks if a user with the login exists + /// + /// + /// + bool ExistsByLogin(string login); + /// /// Gets a list of objects associated with a given group /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index f3db5c5906..f35e820042 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -626,6 +626,11 @@ ORDER BY colName"; } public bool Exists(string username) + { + return ExistsByUserName(username); + } + + public bool ExistsByUserName(string username) { var sql = SqlContext.Sql() .SelectCount() @@ -635,6 +640,16 @@ ORDER BY colName"; return Database.ExecuteScalar(sql) > 0; } + public bool ExistsByLogin(string login) + { + var sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.Login == login); + + return Database.ExecuteScalar(sql) > 0; + } + /// /// Gets a list of objects associated with a given group /// diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index f09e917e94..4dbc87dd4f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -109,9 +109,9 @@ namespace Umbraco.Core.Services.Implement User user; using (var scope = ScopeProvider.CreateScope()) { - var loginExists = scope.Database.ExecuteScalar("SELECT COUNT(id) FROM umbracoUser WHERE userLogin = @Login", new { Login = username }) != 0; + var loginExists = _userRepository.ExistsByLogin(username); if (loginExists) - throw new ArgumentException("Login already exists"); // causes rollback // causes rollback + throw new ArgumentException("Login already exists"); // causes rollback user = new User(_globalSettings) { From 0b49c2d84e812adf9fd3a0092d38014df3d74204 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 15 Feb 2020 09:24:13 +0100 Subject: [PATCH 02/34] Removed a few duplicated comments. --- src/Umbraco.Infrastructure/Services/Implement/MemberService.cs | 2 -- .../Services/Implement/PublicAccessService.cs | 1 - .../Services/Implement/ServerRegistrationService.cs | 1 - src/Umbraco.Infrastructure/Services/Implement/UserService.cs | 2 -- 4 files changed, 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs index 4876772c86..fc0dd8a554 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/MemberService.cs @@ -756,7 +756,6 @@ namespace Umbraco.Core.Services.Implement throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback } - // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! return _memberRepository.Get(query); } @@ -1196,7 +1195,6 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); - // TODO: What about content that has the contenttype as part of its composition? // TODO: What about content that has the contenttype as part of its composition? var query = Query().Where(x => x.ContentTypeId == memberTypeId); diff --git a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs index ab9ea64292..f2af6cf424 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/PublicAccessService.cs @@ -124,7 +124,6 @@ namespace Umbraco.Core.Services.Implement } else { - //If they are both the same already then there's nothing to update, exit //If they are both the same already then there's nothing to update, exit return OperationResult.Attempt.Succeed(evtMsgs, entry); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs b/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs index 97bf76e672..6a092b159f 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ServerRegistrationService.cs @@ -72,7 +72,6 @@ namespace Umbraco.Core.Services.Implement _serverRegistrationRepository.Save(server); _serverRegistrationRepository.DeactiveStaleServers(staleTimeout); // triggers a cache reload - // reload - cheap, cached // reload - cheap, cached // default role is single server, but if registrations contain more diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 4dbc87dd4f..7b7c36d2ea 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -340,7 +340,6 @@ namespace Umbraco.Core.Services.Implement _userRepository.Save(user); - //Now we have to check for backwards compat hacks //Now we have to check for backwards compat hacks var explicitUser = user as User; if (explicitUser != null && explicitUser.GroupsToSave.Count > 0) @@ -358,7 +357,6 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(SavedUser, this, saveEventArgs); } - //commit the whole lot in one go //commit the whole lot in one go scope.Complete(); } From 40d7baca7909038f809da936289892f70b0e95a0 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 15 Feb 2020 11:08:32 +0100 Subject: [PATCH 03/34] Migrated database concerns from KeyValueService to new repository. --- src/Umbraco.Abstractions/Models/IKeyValue.cs | 14 ++ src/Umbraco.Abstractions/Models/KeyValue.cs | 39 +++ .../Repositories/IKeyValueRepository.cs | 13 + .../Migrations/Install/DatabaseDataCreator.cs | 2 +- .../Persistence/Dtos/KeyValueDto.cs | 2 +- .../Implement/KeyValueRepository.cs | 233 ++++++++++++++++++ .../Runtime/SqlMainDomLock.cs | 2 +- src/Umbraco.Infrastructure/RuntimeState.cs | 4 +- .../Services/Implement/KeyValueService.cs | 125 +--------- 9 files changed, 316 insertions(+), 118 deletions(-) create mode 100644 src/Umbraco.Abstractions/Models/IKeyValue.cs create mode 100644 src/Umbraco.Abstractions/Models/KeyValue.cs create mode 100644 src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs diff --git a/src/Umbraco.Abstractions/Models/IKeyValue.cs b/src/Umbraco.Abstractions/Models/IKeyValue.cs new file mode 100644 index 0000000000..7396ae1b68 --- /dev/null +++ b/src/Umbraco.Abstractions/Models/IKeyValue.cs @@ -0,0 +1,14 @@ +using System; +using Umbraco.Core.Models.Entities; + +namespace Umbraco.Core.Models +{ + public interface IKeyValue : IEntity + { + string Identifier { get; set; } + + string Value { get; set; } + + DateTime UpdateDate { get; set; } + } +} diff --git a/src/Umbraco.Abstractions/Models/KeyValue.cs b/src/Umbraco.Abstractions/Models/KeyValue.cs new file mode 100644 index 0000000000..4ad906d91e --- /dev/null +++ b/src/Umbraco.Abstractions/Models/KeyValue.cs @@ -0,0 +1,39 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Core.Models.Entities; + +namespace Umbraco.Core.Models +{ + /// + /// Implements . + /// + [Serializable] + [DataContract(IsReference = true)] + public class KeyValue : EntityBase, IKeyValue + { + private string _identifier; + private string _value; + private DateTime _updateDate; + + /// + public string Identifier + { + get => _identifier; + set => SetPropertyValueAndDetectChanges(value, ref _identifier, nameof(Identifier)); + } + + /// + public string Value + { + get => _value; + set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); + } + + /// + public DateTime UpdateDate + { + get => _updateDate; + set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); + } + } +} diff --git a/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs new file mode 100644 index 0000000000..baae2a1724 --- /dev/null +++ b/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Infrastructure.Persistence.Repositories +{ + public interface IKeyValueRepository + { + void Initialize(); + + string GetValue(string key); + + void SetValue(string key, string value); + + bool TrySetValue(string key, string originalValue, string newValue); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index da6574670b..b4328c973d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -344,7 +344,7 @@ namespace Umbraco.Core.Migrations.Install var stateValueKey = upgrader.StateValueKey; var finalState = upgrader.Plan.FinalState; - _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, Updated = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs index b74039c388..5ead6d0d26 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs @@ -21,6 +21,6 @@ namespace Umbraco.Core.Persistence.Dtos [Column("updated")] [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Updated { get; set; } + public DateTime UpdateDate { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs new file mode 100644 index 0000000000..ebe101bbee --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Scoping; +using Umbraco.Infrastructure.Persistence.Repositories; + +namespace Umbraco.Core.Persistence.Repositories.Implement +{ + internal class KeyValueRepository : NPocoRepositoryBase, IKeyValueRepository + { + public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) + { } + + public void Initialize() + { + var context = new MigrationContext(Database, Logger); + var initMigration = new InitialMigration(context); + initMigration.Migrate(); + } + + public string GetValue(string key) + { + return GetDtoByKey(key)?.Value; + } + + public void SetValue(string key, string value) + { + var dto = GetDtoByKey(key); + if (dto == null) + { + dto = new KeyValueDto + { + Key = key, + Value = value, + UpdateDate = DateTime.Now + }; + + Database.Insert(dto); + } + else + { + UpdateDtoValue(dto, value); + } + } + + public bool TrySetValue(string key, string originalValue, string newValue) + { + var dto = GetDtoByKey(key); + + if (dto == null || dto.Value != originalValue) + return false; + + UpdateDtoValue(dto, newValue); + return true; + } + + private void UpdateDtoValue(KeyValueDto dto, string value) + { + dto.Value = value; + dto.UpdateDate = DateTime.Now; + Database.Update(dto); + } + + /// + /// Gets a value directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + internal static string GetValue(IUmbracoDatabase database, string key) + { + if (database is null) return null; + + var sql = database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Key == key); + return database.FirstOrDefault(sql)?.Value; + } + + #region Overrides of NPocoRepositoryBase + + protected override Guid NodeObjectTypeId => throw new NotImplementedException(); + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() + { + return Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; + } + + protected override IEnumerable GetDeleteClauses() + { + return new List(); + } + + protected override IKeyValue PerformGet(string id) + { + var dto = GetDtoByKey(id); + return dto == null ? null : Map(dto); + } + + private KeyValueDto GetDtoByKey(string key) + { + var sql = GetBaseQuery(false).Where(x => x.Key == key); + return Database.Fetch(sql).FirstOrDefault(); + } + + protected override IEnumerable PerformGetAll(params string[] ids) + { + var sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); + var dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + + protected override void PersistNewItem(IKeyValue entity) + { + var dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(IKeyValue entity) + { + var dto = Map(entity); + Database.Update(dto); + } + + private static KeyValueDto Map(IKeyValue keyValue) + { + if (keyValue == null) return null; + + return new KeyValueDto + { + Key = keyValue.Identifier, + Value = keyValue.Value, + UpdateDate = keyValue.UpdateDate, + }; + } + + private static IKeyValue Map(KeyValueDto dto) + { + if (dto == null) return null; + + return new KeyValue + { + Identifier = dto.Key, + Value = dto.Value, + UpdateDate = dto.UpdateDate, + }; + } + + #endregion + + + /// + /// A custom migration that executes standalone during the Initialize phase of the KeyValueService. + /// + internal class InitialMigration : MigrationBase + { + public InitialMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // as long as we are still running 7 this migration will be invoked, + // but due to multiple restarts during upgrades, maybe the table + // exists already + if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) + return; + + Logger.Info("Creating KeyValue structure."); + + // the locks table was initially created with an identity (auto-increment) primary key, + // but we don't want this, especially as we are about to insert a new row into the table, + // so here we drop that identity + DropLockTableIdentity(); + + // insert the lock object for key/value + Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new { id = Constants.Locks.KeyValues, name = "KeyValues", value = 1 }).Do(); + + // create the key-value table + Create.Table().Do(); + } + + private void DropLockTableIdentity() + { + // one cannot simply drop an identity, that requires a bit of work + + // create a temp. id column and copy values + Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); + Execute.Sql("update umbracoLock set nid = id").Do(); + + // drop the id column entirely (cannot just drop identity) + Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + + // recreate the id column without identity and copy values + Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); + Execute.Sql("update umbracoLock set id = nid").Do(); + + // drop the temp. id column + Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + + // complete the primary key + Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 4e1feb221a..d8a66b4198 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -324,7 +324,7 @@ namespace Umbraco.Core.Runtime { Key = MainDomKey, Value = id, - Updated = DateTime.Now + UpdateDate = DateTime.Now }); } diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 5952e73e62..b6ffecbb0d 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Upgrade; using Umbraco.Core.Persistence; -using Umbraco.Core.Services.Implement; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Sync; namespace Umbraco.Core @@ -263,7 +263,7 @@ namespace Umbraco.Core // no scope, no service - just directly accessing the database using (var database = databaseFactory.CreateDatabase()) { - CurrentMigrationState = KeyValueService.GetValue(database, stateValueKey); + CurrentMigrationState = KeyValueRepository.GetValue(database, stateValueKey); FinalMigrationState = upgrader.Plan.FinalState; } diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs index d5d8e66525..bdb65091de 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs @@ -1,12 +1,8 @@ using System; -using System.Linq; -using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; using Umbraco.Core.Scoping; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; +using Umbraco.Infrastructure.Persistence.Repositories; namespace Umbraco.Core.Services.Implement { @@ -14,13 +10,15 @@ namespace Umbraco.Core.Services.Implement { private readonly object _initialock = new object(); private readonly IScopeProvider _scopeProvider; + private readonly IKeyValueRepository _repository; private readonly ILogger _logger; private readonly IUmbracoVersion _umbracoVersion; private bool _initialized; - public KeyValueService(IScopeProvider scopeProvider, ILogger logger, IUmbracoVersion umbracoVersion) + public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository, ILogger logger, IUmbracoVersion umbracoVersion) { _scopeProvider = scopeProvider; + _repository = repository; _logger = logger; _umbracoVersion = umbracoVersion; } @@ -53,71 +51,14 @@ namespace Umbraco.Core.Services.Implement using (var scope = _scopeProvider.CreateScope()) { - var context = new MigrationContext(scope.Database, _logger); - var initMigration = new InitializeMigration(context); - initMigration.Migrate(); + _repository.Initialize(); scope.Complete(); } // but don't assume we are initializing // we are upgrading from v7 and if anything goes wrong, // the table and everything will be rolled back - } - - /// - /// A custom migration that executes standalone during the Initialize phase of this service. - /// - internal class InitializeMigration : MigrationBase - { - public InitializeMigration(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - // as long as we are still running 7 this migration will be invoked, - // but due to multiple restarts during upgrades, maybe the table - // exists already - if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) - return; - - Logger.Info("Creating KeyValue structure."); - - // the locks table was initially created with an identity (auto-increment) primary key, - // but we don't want this, especially as we are about to insert a new row into the table, - // so here we drop that identity - DropLockTableIdentity(); - - // insert the lock object for key/value - Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new {id = Constants.Locks.KeyValues, name = "KeyValues", value = 1}).Do(); - - // create the key-value table - Create.Table().Do(); - } - - private void DropLockTableIdentity() - { - // one cannot simply drop an identity, that requires a bit of work - - // create a temp. id column and copy values - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); - Execute.Sql("update umbracoLock set nid = id").Do(); - - // drop the id column entirely (cannot just drop identity) - Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - - // recreate the id column without identity and copy values - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); - Execute.Sql("update umbracoLock set id = nid").Do(); - - // drop the temp. id column - Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - - // complete the primary key - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); - } - } + } /// public string GetValue(string key) @@ -126,10 +67,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = _scopeProvider.CreateScope()) { - var sql = scope.SqlContext.Sql().Select().From().Where(x => x.Key == key); - var dto = scope.Database.Fetch(sql).FirstOrDefault(); - scope.Complete(); - return dto?.Value; + return _repository.GetValue(key); } } @@ -142,26 +80,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.KeyValues); - var sql = scope.SqlContext.Sql().Select().From().Where(x => x.Key == key); - var dto = scope.Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - { - dto = new KeyValueDto - { - Key = key, - Value = value, - Updated = DateTime.Now - }; - - scope.Database.Insert(dto); - } - else - { - dto.Value = value; - dto.Updated = DateTime.Now; - scope.Database.Update(dto); - } + _repository.SetValue(key, value); scope.Complete(); } @@ -175,7 +94,7 @@ namespace Umbraco.Core.Services.Implement } /// - public bool TrySetValue(string key, string originValue, string newValue) + public bool TrySetValue(string key, string originalValue, string newValue) { EnsureInitialized(); @@ -183,35 +102,15 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.KeyValues); - var sql = scope.SqlContext.Sql().Select().From().Where(x => x.Key == key); - var dto = scope.Database.Fetch(sql).FirstOrDefault(); - - if (dto == null || dto.Value != originValue) + if (!_repository.TrySetValue(key, originalValue, newValue)) + { return false; - - dto.Value = newValue; - dto.Updated = DateTime.Now; - scope.Database.Update(dto); + } scope.Complete(); } return true; } - - /// - /// Gets a value directly from the database, no scope, nothing. - /// - /// Used by to determine the runtime state. - internal static string GetValue(IUmbracoDatabase database, string key) - { - if (database is null) return null; - - var sql = database.SqlContext.Sql() - .Select() - .From() - .Where(x => x.Key == key); - return database.FirstOrDefault(sql)?.Value; - } } } From ac91b9ef5e1e587272613bf033969cfe7b7d7240 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 15 Feb 2020 11:10:25 +0100 Subject: [PATCH 04/34] Removed usage of obsoleteted method in UserService. --- src/Umbraco.Infrastructure/Services/Implement/UserService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs index 7b7c36d2ea..c6cf6c01c3 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/UserService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/UserService.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - return _userRepository.Exists(username); + return _userRepository.ExistsByUserName(username); } } From f92b43260af235a39e5fce9f3a153cc9b6dca532 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 15 Feb 2020 12:17:48 +0100 Subject: [PATCH 05/34] Added unit tests for new KeyValueRepository. --- .../Repositories/KeyValueRepositoryTests.cs | 61 +++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 2 files changed, 62 insertions(+) create mode 100644 src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs diff --git a/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs b/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs new file mode 100644 index 0000000000..92ebb03a32 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs @@ -0,0 +1,61 @@ +using NUnit.Framework; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Infrastructure.Persistence.Repositories; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class KeyValueRepositoryTests : TestWithDatabaseBase + { + [Test] + public void CanSetAndGet() + { + var provider = TestObjects.GetScopeProvider(Logger); + + // Insert new key/value + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository(provider); + repo.SetValue("foo", "bar"); + scope.Complete(); + } + + // Retrieve key/value + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository(provider); + var value = repo.GetValue("foo"); + scope.Complete(); + + Assert.AreEqual("bar", value); + } + + // Update new key/value + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository(provider); + repo.SetValue("foo", "buzz"); + scope.Complete(); + } + + // Retrieve key/value again + using (var scope = provider.CreateScope()) + { + var repo = CreateRepository(provider); + var value = repo.GetValue("foo"); + scope.Complete(); + + Assert.AreEqual("buzz", value); + } + } + + private IKeyValueRepository CreateRepository(IScopeProvider provider) + { + return new KeyValueRepository((IScopeAccessor) provider, Logger); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 16e4ca2c11..1527a8d4cc 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -148,6 +148,7 @@ + From 0bba4755339ae186ef194cde9799ff933d629579 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 17 Feb 2020 22:12:42 +0100 Subject: [PATCH 06/34] Amends to key value service/repository to better match existing repository patterns. --- src/Umbraco.Abstractions/Models/KeyValue.cs | 7 - .../Repositories/IKeyValueRepository.cs | 13 +- .../Services/ServiceContext.cs | 11 +- .../CompositionExtensions/Repositories.cs | 1 + .../Custom/IKeyValueServiceInitialization.cs | 9 ++ .../Custom/KeyValueServiceInitialization.cs | 80 +++++++++++ .../Implement/KeyValueRepository.cs | 134 +++--------------- .../Implement/RepositoryBaseOfTIdTEntity.cs | 2 +- .../Runtime/CoreInitialComposer.cs | 2 + .../Services/Implement/KeyValueService.cs | 37 ++++- .../Repositories/KeyValueRepositoryTests.cs | 29 ++-- .../Services/KeyValueServiceTests.cs | 93 ++++++++++++ src/Umbraco.Tests/TestHelpers/TestObjects.cs | 3 + src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 14 files changed, 272 insertions(+), 150 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs create mode 100644 src/Umbraco.Tests/Services/KeyValueServiceTests.cs diff --git a/src/Umbraco.Abstractions/Models/KeyValue.cs b/src/Umbraco.Abstractions/Models/KeyValue.cs index 4ad906d91e..ba98b577a4 100644 --- a/src/Umbraco.Abstractions/Models/KeyValue.cs +++ b/src/Umbraco.Abstractions/Models/KeyValue.cs @@ -28,12 +28,5 @@ namespace Umbraco.Core.Models get => _value; set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); } - - /// - public DateTime UpdateDate - { - get => _updateDate; - set => SetPropertyValueAndDetectChanges(value, ref _updateDate, nameof(UpdateDate)); - } } } diff --git a/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs index baae2a1724..0eb2b27eb0 100644 --- a/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs +++ b/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs @@ -1,13 +1,8 @@ -namespace Umbraco.Infrastructure.Persistence.Repositories +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories { - public interface IKeyValueRepository + public interface IKeyValueRepository : IReadRepository, IWriteRepository { - void Initialize(); - - string GetValue(string key); - - void SetValue(string key, string value); - - bool TrySetValue(string key, string originalValue, string newValue); } } diff --git a/src/Umbraco.Abstractions/Services/ServiceContext.cs b/src/Umbraco.Abstractions/Services/ServiceContext.cs index f3c95b07f9..c9bdc6d924 100644 --- a/src/Umbraco.Abstractions/Services/ServiceContext.cs +++ b/src/Umbraco.Abstractions/Services/ServiceContext.cs @@ -32,12 +32,13 @@ namespace Umbraco.Core.Services private readonly Lazy _externalLoginService; private readonly Lazy _redirectUrlService; private readonly Lazy _consentService; + private readonly Lazy _keyValueService; private readonly Lazy _contentTypeBaseServiceProvider; /// /// Initializes a new instance of the class with lazy services. /// - public ServiceContext(Lazy publicAccessService, Lazy domainService, Lazy auditService, Lazy localizedTextService, Lazy tagService, Lazy contentService, Lazy userService, Lazy memberService, Lazy mediaService, Lazy contentTypeService, Lazy mediaTypeService, Lazy dataTypeService, Lazy fileService, Lazy localizationService, Lazy packagingService, Lazy serverRegistrationService, Lazy entityService, Lazy relationService, Lazy macroService, Lazy memberTypeService, Lazy memberGroupService, Lazy notificationService, Lazy externalLoginService, Lazy redirectUrlService, Lazy consentService, Lazy contentTypeBaseServiceProvider) + public ServiceContext(Lazy publicAccessService, Lazy domainService, Lazy auditService, Lazy localizedTextService, Lazy tagService, Lazy contentService, Lazy userService, Lazy memberService, Lazy mediaService, Lazy contentTypeService, Lazy mediaTypeService, Lazy dataTypeService, Lazy fileService, Lazy localizationService, Lazy packagingService, Lazy serverRegistrationService, Lazy entityService, Lazy relationService, Lazy macroService, Lazy memberTypeService, Lazy memberGroupService, Lazy notificationService, Lazy externalLoginService, Lazy redirectUrlService, Lazy consentService, Lazy keyValueService, Lazy contentTypeBaseServiceProvider) { _publicAccessService = publicAccessService; _domainService = domainService; @@ -64,6 +65,7 @@ namespace Umbraco.Core.Services _externalLoginService = externalLoginService; _redirectUrlService = redirectUrlService; _consentService = consentService; + _keyValueService = keyValueService; _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; } @@ -99,6 +101,7 @@ namespace Umbraco.Core.Services IServerRegistrationService serverRegistrationService = null, IRedirectUrlService redirectUrlService = null, IConsentService consentService = null, + IKeyValueService keyValueService = null, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider = null) { Lazy Lazy(T service) => service == null ? null : new Lazy(() => service); @@ -129,6 +132,7 @@ namespace Umbraco.Core.Services Lazy(externalLoginService), Lazy(redirectUrlService), Lazy(consentService), + Lazy(keyValueService), Lazy(contentTypeBaseServiceProvider) ); } @@ -258,6 +262,11 @@ namespace Umbraco.Core.Services /// public IConsentService ConsentService => _consentService.Value; + /// + /// Gets the KeyValueService. + /// + public IKeyValueService KeyValueService => _keyValueService.Value; + /// /// Gets the ContentTypeServiceBaseFactory. /// diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs index 0939dd0f71..5318c47a40 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs @@ -47,6 +47,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); return composition; } diff --git a/src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs b/src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs new file mode 100644 index 0000000000..a863905dc7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Persistence; + +namespace Umbraco.Infrastructure.Migrations.Custom +{ + public interface IKeyValueServiceInitialization + { + void PerformInitialMigration(IUmbracoDatabase database); + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs b/src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs new file mode 100644 index 0000000000..488acf507a --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs @@ -0,0 +1,80 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Infrastructure.Migrations.Custom +{ + public class KeyValueServiceInitialization : IKeyValueServiceInitialization + { + private readonly ILogger _logger; + + public KeyValueServiceInitialization(ILogger logger) + { + _logger = logger; + } + + public void PerformInitialMigration(IUmbracoDatabase database) + { + var context = new MigrationContext(database, _logger); + var initMigration = new InitialMigration(context); + initMigration.Migrate(); + } + + /// + /// A custom migration that executes standalone during the Initialize phase of the KeyValueService. + /// + internal class InitialMigration : MigrationBase + { + public InitialMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + // as long as we are still running 7 this migration will be invoked, + // but due to multiple restarts during upgrades, maybe the table + // exists already + if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) + return; + + Logger.Info("Creating KeyValue structure."); + + // the locks table was initially created with an identity (auto-increment) primary key, + // but we don't want this, especially as we are about to insert a new row into the table, + // so here we drop that identity + DropLockTableIdentity(); + + // insert the lock object for key/value + Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new { id = Constants.Locks.KeyValues, name = "KeyValues", value = 1 }).Do(); + + // create the key-value table + Create.Table().Do(); + } + + private void DropLockTableIdentity() + { + // one cannot simply drop an identity, that requires a bit of work + + // create a temp. id column and copy values + Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); + Execute.Sql("update umbracoLock set nid = id").Do(); + + // drop the id column entirely (cannot just drop identity) + Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + + // recreate the id column without identity and copy values + Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); + Execute.Sql("update umbracoLock set id = nid").Do(); + + // drop the temp. id column + Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); + + // complete the primary key + Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index ebe101bbee..eab4049ec9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -4,12 +4,11 @@ using System.Linq; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; using Umbraco.Core.Models; +using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; -using Umbraco.Infrastructure.Persistence.Repositories; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -19,56 +18,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement : base(scopeAccessor, AppCaches.NoCache, logger) { } - public void Initialize() - { - var context = new MigrationContext(Database, Logger); - var initMigration = new InitialMigration(context); - initMigration.Migrate(); - } - - public string GetValue(string key) - { - return GetDtoByKey(key)?.Value; - } - - public void SetValue(string key, string value) - { - var dto = GetDtoByKey(key); - if (dto == null) - { - dto = new KeyValueDto - { - Key = key, - Value = value, - UpdateDate = DateTime.Now - }; - - Database.Insert(dto); - } - else - { - UpdateDtoValue(dto, value); - } - } - - public bool TrySetValue(string key, string originalValue, string newValue) - { - var dto = GetDtoByKey(key); - - if (dto == null || dto.Value != originalValue) - return false; - - UpdateDtoValue(dto, newValue); - return true; - } - - private void UpdateDtoValue(KeyValueDto dto, string value) - { - dto.Value = value; - dto.UpdateDate = DateTime.Now; - Database.Update(dto); - } - /// /// Gets a value directly from the database, no scope, nothing. /// @@ -84,6 +33,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return database.FirstOrDefault(sql)?.Value; } + #region Overrides of IReadWriteQueryRepository + + public override void Save(IKeyValue entity) + { + if (Get(entity.Identifier) == null) + PersistNewItem(entity); + else + PersistUpdatedItem(entity); + } + + #endregion + #region Overrides of NPocoRepositoryBase protected override Guid NodeObjectTypeId => throw new NotImplementedException(); @@ -114,16 +75,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IKeyValue PerformGet(string id) { - var dto = GetDtoByKey(id); + var sql = GetBaseQuery(false).Where(x => x.Key == id); + var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); } - private KeyValueDto GetDtoByKey(string key) - { - var sql = GetBaseQuery(false).Where(x => x.Key == key); - return Database.Fetch(sql).FirstOrDefault(); - } - protected override IEnumerable PerformGetAll(params string[] ids) { var sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); @@ -170,64 +126,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Value = dto.Value, UpdateDate = dto.UpdateDate, }; - } + } - #endregion - - - /// - /// A custom migration that executes standalone during the Initialize phase of the KeyValueService. - /// - internal class InitialMigration : MigrationBase - { - public InitialMigration(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - // as long as we are still running 7 this migration will be invoked, - // but due to multiple restarts during upgrades, maybe the table - // exists already - if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) - return; - - Logger.Info("Creating KeyValue structure."); - - // the locks table was initially created with an identity (auto-increment) primary key, - // but we don't want this, especially as we are about to insert a new row into the table, - // so here we drop that identity - DropLockTableIdentity(); - - // insert the lock object for key/value - Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new { id = Constants.Locks.KeyValues, name = "KeyValues", value = 1 }).Do(); - - // create the key-value table - Create.Table().Do(); - } - - private void DropLockTableIdentity() - { - // one cannot simply drop an identity, that requires a bit of work - - // create a temp. id column and copy values - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); - Execute.Sql("update umbracoLock set nid = id").Do(); - - // drop the id column entirely (cannot just drop identity) - Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - - // recreate the id column without identity and copy values - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); - Execute.Sql("update umbracoLock set id = nid").Do(); - - // drop the temp. id column - Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - - // complete the primary key - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); - } - } + #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs index e8397ba22a..6985bf78da 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// /// This method is backed by an cache /// - public void Save(TEntity entity) + public virtual void Save(TEntity entity) { if (entity.HasIdentity == false) CachePolicy.Create(entity, PersistNewItem); diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index dbecd56d18..109f307c98 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -21,6 +21,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; +using Umbraco.Infrastructure.Migrations.Custom; namespace Umbraco.Core.Runtime { @@ -128,6 +129,7 @@ namespace Umbraco.Core.Runtime .Append(); composition.RegisterUnique(factory => new MigrationBuilder(factory)); + composition.RegisterUnique(); // by default, register a noop factory composition.RegisterUnique(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs index bdb65091de..6835457f64 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs @@ -1,8 +1,10 @@ using System; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; -using Umbraco.Infrastructure.Persistence.Repositories; +using Umbraco.Infrastructure.Migrations.Custom; namespace Umbraco.Core.Services.Implement { @@ -11,14 +13,16 @@ namespace Umbraco.Core.Services.Implement private readonly object _initialock = new object(); private readonly IScopeProvider _scopeProvider; private readonly IKeyValueRepository _repository; + private readonly IKeyValueServiceInitialization _initialization; private readonly ILogger _logger; private readonly IUmbracoVersion _umbracoVersion; private bool _initialized; - public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository, ILogger logger, IUmbracoVersion umbracoVersion) + public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository, IKeyValueServiceInitialization initialization, ILogger logger, IUmbracoVersion umbracoVersion) { _scopeProvider = scopeProvider; _repository = repository; + _initialization = initialization; _logger = logger; _umbracoVersion = umbracoVersion; } @@ -51,7 +55,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = _scopeProvider.CreateScope()) { - _repository.Initialize(); + _initialization.PerformInitialMigration(scope.Database); scope.Complete(); } @@ -67,7 +71,7 @@ namespace Umbraco.Core.Services.Implement using (var scope = _scopeProvider.CreateScope()) { - return _repository.GetValue(key); + return _repository.Get(key)?.Value; } } @@ -80,7 +84,23 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.KeyValues); - _repository.SetValue(key, value); + var keyValue = _repository.Get(key); + if (keyValue == null) + { + keyValue = new KeyValue + { + Identifier = key, + Value = value, + UpdateDate = DateTime.Now, + }; + } + else + { + keyValue.Value = value; + keyValue.UpdateDate = DateTime.Now; + } + + _repository.Save(keyValue); scope.Complete(); } @@ -102,11 +122,16 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.KeyValues); - if (!_repository.TrySetValue(key, originalValue, newValue)) + var keyValue = _repository.Get(key); + if (keyValue == null || keyValue.Value != originalValue) { return false; } + keyValue.Value = newValue; + keyValue.UpdateDate = DateTime.Now; + _repository.Save(keyValue); + scope.Complete(); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs b/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs index 92ebb03a32..50ebc7ff5d 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/KeyValueRepositoryTests.cs @@ -1,7 +1,9 @@ -using NUnit.Framework; +using System; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; -using Umbraco.Infrastructure.Persistence.Repositories; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; @@ -19,8 +21,14 @@ namespace Umbraco.Tests.Persistence.Repositories // Insert new key/value using (var scope = provider.CreateScope()) { + var keyValue = new KeyValue + { + Identifier = "foo", + Value = "bar", + UpdateDate = DateTime.Now, + }; var repo = CreateRepository(provider); - repo.SetValue("foo", "bar"); + repo.Save(keyValue); scope.Complete(); } @@ -28,17 +36,20 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = provider.CreateScope()) { var repo = CreateRepository(provider); - var value = repo.GetValue("foo"); + var keyValue = repo.Get("foo"); scope.Complete(); - Assert.AreEqual("bar", value); + Assert.AreEqual("bar", keyValue.Value); } - // Update new key/value + // Update value using (var scope = provider.CreateScope()) { var repo = CreateRepository(provider); - repo.SetValue("foo", "buzz"); + var keyValue = repo.Get("foo"); + keyValue.Value = "buzz"; + keyValue.UpdateDate = DateTime.Now; + repo.Save(keyValue); scope.Complete(); } @@ -46,10 +57,10 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = provider.CreateScope()) { var repo = CreateRepository(provider); - var value = repo.GetValue("foo"); + var keyValue = repo.Get("foo"); scope.Complete(); - Assert.AreEqual("buzz", value); + Assert.AreEqual("buzz", keyValue.Value); } } diff --git a/src/Umbraco.Tests/Services/KeyValueServiceTests.cs b/src/Umbraco.Tests/Services/KeyValueServiceTests.cs new file mode 100644 index 0000000000..5ab29258f5 --- /dev/null +++ b/src/Umbraco.Tests/Services/KeyValueServiceTests.cs @@ -0,0 +1,93 @@ +using System.Threading; +using NUnit.Framework; +using NUnit.Framework.Internal; +using Umbraco.Core.Services; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Services +{ + /// + /// Tests covering methods in the KeyValueService class. + /// This is more of an integration test as it involves multiple layers + /// as well as configuration. + /// + [TestFixture] + [Apartment(ApartmentState.STA)] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class KeyValueServiceTests : TestWithSomeContentBase + { + [Test] + public void GetValue_ForMissingKey_ReturnsNull() + { + // Arrange + var keyValueService = ServiceContext.KeyValueService; + + // Act + var value = keyValueService.GetValue("foo"); + + // Assert + Assert.IsNull(value); + } + + [Test] + public void GetValue_ForExistingKey_ReturnsValue() + { + // Arrange + var keyValueService = ServiceContext.KeyValueService; + keyValueService.SetValue("foo", "bar"); + + // Act + var value = keyValueService.GetValue("foo"); + + // Assert + Assert.AreEqual("bar", value); + } + + [Test] + public void SetValue_ForExistingKey_SavesValue() + { + // Arrange + var keyValueService = ServiceContext.KeyValueService; + keyValueService.SetValue("foo", "bar"); + + // Act + keyValueService.SetValue("foo", "buzz"); + var value = keyValueService.GetValue("foo"); + + // Assert + Assert.AreEqual("buzz", value); + } + + [Test] + public void TrySetValue_ForExistingKeyWithProvidedValue_ReturnsTrueAndSetsValue() + { + // Arrange + var keyValueService = ServiceContext.KeyValueService; + keyValueService.SetValue("foo", "bar"); + + // Act + var result = keyValueService.TrySetValue("foo", "bar", "buzz"); + var value = keyValueService.GetValue("foo"); + + // Assert + Assert.IsTrue(result); + Assert.AreEqual("buzz", value); + } + + [Test] + public void TrySetValue_ForExistingKeyWithoutProvidedValue_ReturnsFalseAndDoesNotSetValue() + { + // Arrange + var keyValueService = ServiceContext.KeyValueService; + keyValueService.SetValue("foo", "bar"); + + // Act + var result = keyValueService.TrySetValue("foo", "bang", "buzz"); + var value = keyValueService.GetValue("foo"); + + // Assert + Assert.IsFalse(result); + Assert.AreEqual("bar", value); + } + } +} diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 92b9dd0ad2..8e884a5937 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -21,6 +21,7 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; +using Umbraco.Infrastructure.Migrations.Custom; using Umbraco.Tests.TestHelpers.Stubs; using Current = Umbraco.Web.Composing.Current; @@ -193,6 +194,7 @@ namespace Umbraco.Tests.TestHelpers var tagService = GetLazyService(factory, c => new TagService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var redirectUrlService = GetLazyService(factory, c => new RedirectUrlService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var consentService = GetLazyService(factory, c => new ConsentService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); + var keyValueService = GetLazyService(factory, c => new KeyValueService(scopeProvider, GetRepo(c), Mock.Of(), logger, umbracoVersion)); var contentTypeServiceBaseFactory = GetLazyService(factory, c => new ContentTypeBaseServiceProvider(factory.GetInstance(),factory.GetInstance(),factory.GetInstance())); return new ServiceContext( @@ -221,6 +223,7 @@ namespace Umbraco.Tests.TestHelpers externalLoginService, redirectUrlService, consentService, + keyValueService, contentTypeServiceBaseFactory); } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1527a8d4cc..46baf94644 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -149,6 +149,7 @@ + From 3d206533101a5f23c04327dc5bb70128e8749938 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 5 Mar 2020 07:53:27 +0100 Subject: [PATCH 07/34] Aligned files with new project structure and fixed failing unit tests. --- src/{Umbraco.Abstractions => Umbraco.Core}/Models/IKeyValue.cs | 0 src/{Umbraco.Abstractions => Umbraco.Core}/Models/KeyValue.cs | 0 .../Persistence/Repositories/IKeyValueRepository.cs | 0 .../Composing/CompositionExtensions/Repositories.cs | 2 ++ 4 files changed, 2 insertions(+) rename src/{Umbraco.Abstractions => Umbraco.Core}/Models/IKeyValue.cs (100%) rename src/{Umbraco.Abstractions => Umbraco.Core}/Models/KeyValue.cs (100%) rename src/{Umbraco.Abstractions => Umbraco.Core}/Persistence/Repositories/IKeyValueRepository.cs (100%) diff --git a/src/Umbraco.Abstractions/Models/IKeyValue.cs b/src/Umbraco.Core/Models/IKeyValue.cs similarity index 100% rename from src/Umbraco.Abstractions/Models/IKeyValue.cs rename to src/Umbraco.Core/Models/IKeyValue.cs diff --git a/src/Umbraco.Abstractions/Models/KeyValue.cs b/src/Umbraco.Core/Models/KeyValue.cs similarity index 100% rename from src/Umbraco.Abstractions/Models/KeyValue.cs rename to src/Umbraco.Core/Models/KeyValue.cs diff --git a/src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs similarity index 100% rename from src/Umbraco.Abstractions/Persistence/Repositories/IKeyValueRepository.cs rename to src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs index 9a23255cc4..b8231241a1 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs @@ -1,5 +1,6 @@ using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Infrastructure.Migrations.Custom; namespace Umbraco.Core.Composing.CompositionExtensions { @@ -48,6 +49,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); From b67c3dfa04e7568629e0bc69b99be0f37d8e3472 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 24 Mar 2020 09:37:46 +0100 Subject: [PATCH 08/34] Moved IImageUrlGenerator and implementation into more appropriate enamespace --- src/Umbraco.Core/{Models => Media}/IImageUrlGenerator.cs | 4 +++- .../{Models => Media}/ImageProcessorImageUrlGenerator.cs | 3 ++- .../Models/Mapping/UserMapDefinition.cs | 1 + src/Umbraco.Infrastructure/Models/UserExtensions.cs | 1 + .../PropertyEditors/GridPropertyEditor.cs | 1 + .../PropertyEditors/RichTextEditorPastedImages.cs | 1 + .../PropertyEditors/RichTextPropertyEditor.cs | 1 + .../PropertyEditors/ValueConverters/ImageCropperValue.cs | 1 + .../Models/ImageProcessorImageUrlGeneratorTest.cs | 2 +- src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs | 1 + .../PublishedContent/PublishedContentTestBase.cs | 1 + src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs | 1 + src/Umbraco.Tests/Runtimes/StandaloneTests.cs | 1 + src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 2 +- src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs | 1 + src/Umbraco.Web/Editors/CurrentUserController.cs | 2 +- src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs | 2 +- src/Umbraco.Web/Editors/ImagesController.cs | 1 + src/Umbraco.Web/Editors/LogController.cs | 1 + src/Umbraco.Web/Editors/UsersController.cs | 1 + src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs | 1 + src/Umbraco.Web/Runtime/WebInitialComposer.cs | 4 ++-- src/Umbraco.Web/UrlHelperRenderExtensions.cs | 2 +- 23 files changed, 27 insertions(+), 9 deletions(-) rename src/Umbraco.Core/{Models => Media}/IImageUrlGenerator.cs (66%) rename src/Umbraco.Infrastructure/{Models => Media}/ImageProcessorImageUrlGenerator.cs (98%) diff --git a/src/Umbraco.Core/Models/IImageUrlGenerator.cs b/src/Umbraco.Core/Media/IImageUrlGenerator.cs similarity index 66% rename from src/Umbraco.Core/Models/IImageUrlGenerator.cs rename to src/Umbraco.Core/Media/IImageUrlGenerator.cs index 0dc2933fd9..f9e67cdd2b 100644 --- a/src/Umbraco.Core/Models/IImageUrlGenerator.cs +++ b/src/Umbraco.Core/Media/IImageUrlGenerator.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Core.Models +using Umbraco.Core.Models; + +namespace Umbraco.Core.Media { public interface IImageUrlGenerator { diff --git a/src/Umbraco.Infrastructure/Models/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs similarity index 98% rename from src/Umbraco.Infrastructure/Models/ImageProcessorImageUrlGenerator.cs rename to src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs index f88309d7e3..61a12bedf6 100644 --- a/src/Umbraco.Infrastructure/Models/ImageProcessorImageUrlGenerator.cs +++ b/src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs @@ -1,9 +1,10 @@ using System.Globalization; using System.Text; using Umbraco.Core; +using Umbraco.Core.Media; using Umbraco.Core.Models; -namespace Umbraco.Web.Models +namespace Umbraco.Infrastructure.Media { public class ImageProcessorImageUrlGenerator : IImageUrlGenerator { diff --git a/src/Umbraco.Infrastructure/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/UserMapDefinition.cs index 19a78bcbe4..80f3a8bde8 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/UserMapDefinition.cs @@ -16,6 +16,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Actions; using Umbraco.Web.Services; +using Umbraco.Core.Media; namespace Umbraco.Web.Models.Mapping { diff --git a/src/Umbraco.Infrastructure/Models/UserExtensions.cs b/src/Umbraco.Infrastructure/Models/UserExtensions.cs index 0c0e4fbc48..4fefc89d11 100644 --- a/src/Umbraco.Infrastructure/Models/UserExtensions.cs +++ b/src/Umbraco.Infrastructure/Models/UserExtensions.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core.Security; +using Umbraco.Core.Media; namespace Umbraco.Core.Models { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index 72e0843240..1a8e4524c7 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -5,6 +5,7 @@ using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs index eee45a636c..38b4611c1e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextEditorPastedImages.cs @@ -6,6 +6,7 @@ using Umbraco.Core; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Strings; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index dea9a5f002..2f283b2709 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs index 1367c04cdb..5878e51dcd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Serialization; using Umbraco.Core.Strings; diff --git a/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs index 30ead90de9..bf6529f45e 100644 --- a/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs +++ b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs @@ -16,10 +16,10 @@ using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Services; using Umbraco.Tests.Components; using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Models; using Umbraco.Web; using Umbraco.Web.PropertyEditors; using System.Text; +using Umbraco.Infrastructure.Media; namespace Umbraco.Tests.Models { diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 919386f491..788b5dce10 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -23,6 +23,7 @@ using Umbraco.Web.PropertyEditors; using System.Text; using Current = Umbraco.Web.Composing.Current; using Umbraco.Core.Cache; +using Umbraco.Core.Media; namespace Umbraco.Tests.PropertyEditors { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index d99fc90813..3655a44d0d 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -16,6 +16,7 @@ using Umbraco.Web; using Umbraco.Web.Templates; using Umbraco.Web.Models; using Umbraco.Web.Routing; +using Umbraco.Core.Media; namespace Umbraco.Tests.PublishedContent { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index e852c22828..2667848c57 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -25,6 +25,7 @@ using Umbraco.Web.Templates; using Umbraco.Web.Models; using Umbraco.Web.Routing; using Current = Umbraco.Web.Composing.Current; +using Umbraco.Core.Media; namespace Umbraco.Tests.PublishedContent { diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 0cb3f3e2e1..eee60ffc1a 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -36,6 +36,7 @@ using File = System.IO.File; using Current = Umbraco.Web.Composing.Current; using Umbraco.Tests.Common; using Umbraco.Tests.Common.Composing; +using Umbraco.Core.Media; namespace Umbraco.Tests.Runtimes { diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index ca36a6049e..bb6442b4d8 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -50,7 +50,6 @@ using FileSystems = Umbraco.Core.IO.FileSystems; using Umbraco.Web.Templates; using Umbraco.Web.PropertyEditors; using Umbraco.Core.Dictionary; -using Umbraco.Core.Models; using Umbraco.Core.Request; using Umbraco.Core.Security; using Umbraco.Core.Services; @@ -63,6 +62,7 @@ using Umbraco.Web.Security.Providers; using Umbraco.Web.Trees; using Current = Umbraco.Web.Composing.Current; using Umbraco.Tests.Common; +using Umbraco.Core.Media; namespace Umbraco.Tests.Testing { diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 14bbc5f1fb..d6c9385aa9 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -37,6 +37,7 @@ using IUser = Umbraco.Core.Models.Membership.IUser; using Umbraco.Core.Mapping; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.Routing; +using Umbraco.Core.Media; namespace Umbraco.Tests.Web.Controllers { diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 384270743c..0b288735fe 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -22,8 +22,8 @@ using Umbraco.Web.Security; using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Mapping; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Models; using Umbraco.Web.Routing; +using Umbraco.Core.Media; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs index 87d7e29619..e5369fb7a1 100644 --- a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs +++ b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using Umbraco.Core.Models; +using Umbraco.Core.Media; using Umbraco.Web.Models; using Umbraco.Web.Mvc; diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 39fe619b9d..515b5e9c8c 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -4,6 +4,7 @@ using System.Net; using System.Net.Http; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index b502802775..7d69a4169c 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Services; diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 9554906b0d..eec10be1ac 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -36,6 +36,7 @@ using Task = System.Threading.Tasks.Task; using Umbraco.Core.Mapping; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Web.Routing; +using Umbraco.Core.Media; namespace Umbraco.Web.Editors { diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index 7ac5578d75..68bbb26784 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Models; +using Umbraco.Core.Media; namespace Umbraco.Web { diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index b81cafdb40..edaa0dbd16 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -45,11 +45,11 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Umbraco.Web.PropertyEditors; using Umbraco.Examine; -using Umbraco.Core.Models; using Umbraco.Core.Request; using Umbraco.Core.Session; using Umbraco.Web.AspNet; -using Umbraco.Web.Models; +using Umbraco.Core.Media; +using Umbraco.Infrastructure.Media; namespace Umbraco.Web.Runtime { diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 0cf009cb88..156a3dfba1 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Web; using System.Web.Mvc; using Umbraco.Core; -using Umbraco.Core.Models; +using Umbraco.Core.Media; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Composing; From c16965319a90d63fb3e674299c6cd535740b89cd Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 24 Mar 2020 12:17:41 +0100 Subject: [PATCH 09/34] Amended processed image URL building to use checks for empty as well as null, and refactored null checks to use HasValue. --- .../Media/ImageProcessorImageUrlGenerator.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs index 61a12bedf6..f1a0121653 100644 --- a/src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs +++ b/src/Umbraco.Infrastructure/Media/ImageProcessorImageUrlGenerator.cs @@ -28,18 +28,18 @@ namespace Umbraco.Infrastructure.Media //Only put quality here, if we don't have a format specified. //Otherwise we need to put quality at the end to avoid it being overridden by the format. - if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&quality=").Append(options.Quality); - if (options.HeightRatio != null) imageProcessorUrl.Append("&heightratio=").Append(options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); - if (options.WidthRatio != null) imageProcessorUrl.Append("&widthratio=").Append(options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); - if (options.Width != null) imageProcessorUrl.Append("&width=").Append(options.Width); - if (options.Height != null) imageProcessorUrl.Append("&height=").Append(options.Height); + if (options.Quality.HasValue && hasFormat == false) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.HeightRatio.HasValue) imageProcessorUrl.Append("&heightratio=").Append(options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio.HasValue) imageProcessorUrl.Append("&widthratio=").Append(options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width.HasValue) imageProcessorUrl.Append("&width=").Append(options.Width); + if (options.Height.HasValue) imageProcessorUrl.Append("&height=").Append(options.Height); if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false"); - if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&animationprocessmode=").Append(options.AnimationProcessMode); - if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); + if (!string.IsNullOrWhiteSpace(options.AnimationProcessMode)) imageProcessorUrl.Append("&animationprocessmode=").Append(options.AnimationProcessMode); + if (!string.IsNullOrWhiteSpace(options.FurtherOptions)) imageProcessorUrl.Append(options.FurtherOptions); //If furtherOptions contains a format, we need to put the quality after the format. - if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&quality=").Append(options.Quality); - if (options.CacheBusterValue != null) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); + if (options.Quality.HasValue && hasFormat) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (!string.IsNullOrWhiteSpace(options.CacheBusterValue)) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); return imageProcessorUrl.ToString(); } From e66c92d032ed0de41fc548e425526b48d498e7c6 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 24 Mar 2020 12:19:04 +0100 Subject: [PATCH 10/34] Added controller method to ImagesController and client-side method to mediahelper to retrieve URL for processed image. --- .../common/services/mediahelper.service.js | 53 ++++++++++++++++--- src/Umbraco.Web/Editors/ImagesController.cs | 41 ++++++++++++++ 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 2271f891ce..953ddf46d9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -3,7 +3,7 @@ * @name umbraco.services.mediaHelper * @description A helper object used for dealing with media items **/ -function mediaHelper(umbRequestHelper, $log) { +function mediaHelper(umbRequestHelper, $http, $log) { //container of fileresolvers var _mediaFileResolvers = {}; @@ -304,11 +304,6 @@ function mediaHelper(umbRequestHelper, $log) { return imagePath; } - // Check if file is a svg - if (this.getFileExtension(imagePath) === "svg") { - return imagePath; - } - // If the path is not an image we cannot get a thumb if (!this.detectIfImageByExtension(imagePath)) { return null; @@ -399,6 +394,52 @@ function mediaHelper(umbRequestHelper, $log) { var lowered = filePath.toLowerCase(); var ext = lowered.substr(lowered.lastIndexOf(".") + 1); return ext; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#getProcessedImageUrl + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns image URL with configured crop and other processing parameters. + * + * @param {string} imagePath Raw image path + * @param {object} options Object describing image generation parameters: + * { + * animationProcessMode: + * focalPoint: { + * left: + * top: + * }, + * height: + * mode: + * upscale: + * width: + * } + */ + getProcessedImageUrl: function (imagePath, options) { + + if (!options) { + return imagePath; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "imagesApiBaseUrl", + "GetProcessedImageUrl", + { + imagePath, + animationProcessMode: options.animationProcessMode, + focalPoint: options.focalPoint, + height: options.height, + mode: options.mode, + upscale: options.upscale || false, + width: options.width + })), + "Failed to retrieve processed image URL for image: " + imagePath); } }; diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 515b5e9c8c..960b589b6d 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -84,5 +84,46 @@ namespace Umbraco.Web.Editors return response; } + /// + /// Gets a processed image for the image at the given path + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// If there is no media, image property or image file is found then this will return not found. + /// + public string GetProcessedImageUrl(string imagePath, + int? width = null, + int? height = null, + int? focalPointLeft = null, + int? focalPointTop = null, + string animationProcessMode = "first", + string mode = "crop", + bool upscale = false, + string cacheBusterValue = "") +{ + var options = new ImageUrlGenerationOptions(imagePath) + { + AnimationProcessMode = "first", + CacheBusterValue = cacheBusterValue, + Height = height, + ImageCropMode = "max", + UpScale = false, + Width = width, + }; + if (focalPointLeft.HasValue && focalPointTop.HasValue) + { + options.FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(focalPointTop.Value, focalPointLeft.Value); + } + + return _imageUrlGenerator.GetImageUrl(options); + } } } From 519fd22cea52fc16786f9fc202b891a2b573bf7d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 24 Mar 2020 12:29:16 +0100 Subject: [PATCH 11/34] Changed hard-coded processed images URLs in back-office to use configured image URL generation. --- .../common/services/mediahelper.service.js | 2 + .../src/common/services/tinymce.service.js | 26 ++- .../fileupload/fileupload.controller.js | 17 +- .../grid/editors/media.controller.js | 161 +++++++++--------- .../imagecropper/imagecropper.controller.js | 12 +- src/Umbraco.Web/Editors/ImagesController.cs | 8 +- 6 files changed, 131 insertions(+), 95 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 953ddf46d9..244573d79c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -409,6 +409,7 @@ function mediaHelper(umbRequestHelper, $http, $log) { * @param {object} options Object describing image generation parameters: * { * animationProcessMode: + * cacheBusterValue: * focalPoint: { * left: * top: @@ -433,6 +434,7 @@ function mediaHelper(umbRequestHelper, $http, $log) { { imagePath, animationProcessMode: options.animationProcessMode, + cacheBusterValue: options.cacheBusterValue, focalPoint: options.focalPoint, height: options.height, mode: options.mode, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index d2b91a3707..957308a111 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -7,7 +7,7 @@ * A service containing all logic for all of the Umbraco TinyMCE plugins */ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, - $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService) { + $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper) { //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce @@ -298,15 +298,20 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); - editor.dom.setAttrib(imageDomElement, 'width', newSize.width); editor.dom.setAttrib(imageDomElement, 'height', newSize.height); // Images inserted via Media Picker will have a URL we can use for ImageResizer QueryStrings // Images pasted/dragged in are not persisted to media until saved & thus will need to be added - if(imgUrl){ - var src = imgUrl + "?width=" + newSize.width + "&height=" + newSize.height; - editor.dom.setAttrib(imageDomElement, 'data-mce-src', src); + if (imgUrl) { + mediaHelper.getProcessedImageUrl(imgUrl, + { + height: newSize.height, + width: newSize.width + }) + .then(function (resizedImgUrl) { + editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl); + }); } editor.execCommand("mceAutoResize", false, null, null); @@ -1495,10 +1500,17 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); args.editor.on('ObjectResized', function (e) { - var qs = "?width=" + e.width + "&height=" + e.height + "&mode=max"; var srcAttr = $(e.target).attr("src"); var path = srcAttr.split("?")[0]; - $(e.target).attr("data-mce-src", path + qs); + mediaHelper.getProcessedImageUrl(path, + { + height: e.height, + moded: "max", + width: e.width + }) + .then(function (resizedPath) { + $(e.target).attr("data-mce-src", resizedPath); + }); syncContent(); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 17959b9950..ee71a0d2f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -44,15 +44,22 @@ .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) .run(function (mediaHelper, umbRequestHelper, assetsService) { if (mediaHelper && mediaHelper.registerFileResolver) { - - //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver("Umbraco.UploadField", function (property, entity, thumbnail) { if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { - //get default big thumbnail from image processor - var thumbnailUrl = property.value + "?rnd=" + moment(entity.updateDate).format("YYYYMMDDHHmmss") + "&width=500&animationprocessmode=first"; - return thumbnailUrl; + + // Get default big thumbnail from image processor + return mediaHelper.getProcessedImageUrl(property.value, + { + animationprocessmode: "first", + cacheBusterValue: moment(entity.updateDate).format("YYYYMMDDHHmmss"), + width: 500 + }) + .then(function (url) { + return url; + }); } else { return null; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 574aa87557..1fb8cbe2f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,90 +1,97 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, $timeout, userService, editorService) { - - - $scope.thumbnailUrl = getThumbnailUrl(); - - - if (!$scope.model.config.startNodeId) { - if ($scope.model.config.ignoreUserStartNodes === true) { - $scope.model.config.startNodeId = -1; - $scope.model.config.startNodeIsVirtual = true; + function ($scope, userService, editorService, mediaHelper) { - } else { - userService.getCurrentUser().then(function (userData) { - $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; - $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; - }); - } - } - - $scope.setImage = function(){ - var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; - var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; + $scope.thumbnailUrl = getThumbnailUrl(); - var mediaPicker = { - startNodeId: startNodeId, - startNodeIsVirtual: startNodeIsVirtual, - cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, - showDetails: true, - disableFolderSelect: true, - onlyImages: true, - dataTypeKey: $scope.model.dataTypeKey, - submit: function(model) { - var selectedImage = model.selection[0]; - - $scope.control.value = { - focalPoint: selectedImage.focalPoint, - id: selectedImage.id, - udi: selectedImage.udi, - image: selectedImage.image, - caption: selectedImage.altText - }; - - editorService.close(); - }, - close: function() { - editorService.close(); + if (!$scope.model.config.startNodeId) { + if ($scope.model.config.ignoreUserStartNodes === true) { + $scope.model.config.startNodeId = -1; + $scope.model.config.startNodeIsVirtual = true; + + } else { + userService.getCurrentUser().then(function (userData) { + $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + }); } } - - editorService.mediaPicker(mediaPicker); - }; - - $scope.$watch('control.value', function(newValue, oldValue) { - if(angular.equals(newValue, oldValue)){ - return; // simply skip that - } - - $scope.thumbnailUrl = getThumbnailUrl(); - }, true); - - function getThumbnailUrl() { - if($scope.control.value && $scope.control.value.image) { - var url = $scope.control.value.image; + $scope.setImage = function () { + var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; + var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - if($scope.control.editor.config && $scope.control.editor.config.size){ - url += "?width=" + $scope.control.editor.config.size.width; - url += "&height=" + $scope.control.editor.config.size.height; - url += "&animationprocessmode=first"; + var mediaPicker = { + startNodeId: startNodeId, + startNodeIsVirtual: startNodeIsVirtual, + cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, + showDetails: true, + disableFolderSelect: true, + onlyImages: true, + dataTypeKey: $scope.model.dataTypeKey, + submit: function (model) { + var selectedImage = model.selection[0]; - if($scope.control.value.focalPoint){ - url += "¢er=" + $scope.control.value.focalPoint.top +"," + $scope.control.value.focalPoint.left; - url += "&mode=crop"; + $scope.control.value = { + focalPoint: selectedImage.focalPoint, + id: selectedImage.id, + udi: selectedImage.udi, + image: selectedImage.image, + caption: selectedImage.altText + }; + + editorService.close(); + }, + close: function () { + editorService.close(); } } - // set default size if no crop present (moved from the view) - if (url.indexOf('?') == -1) - { - url += "?width=800&upscale=false&animationprocessmode=false" - } - return url; - } - - return null; - }; + editorService.mediaPicker(mediaPicker); + }; -}); + $scope.$watch('control.value', function (newValue, oldValue) { + if (angular.equals(newValue, oldValue)) { + return; // simply skip that + } + + $scope.thumbnailUrl = getThumbnailUrl(); + }, true); + + function getThumbnailUrl() { + if ($scope.control.value && $scope.control.value.image) { + + var imageOptions; + if (url.indexOf('?') == -1) { + // set default size if no crop present (moved from the view) + imageOptions = { + animationprocessmode : false, + width: 800 + } + } + else { + imageOptions = { + animationprocessmode: "first", + height: $scope.control.editor.config.size.height, + width: $scope.control.editor.config.size.width + }; + + if ($scope.control.value.focalPoint) { + imageOptions.focalPoint = { + left: $scope.control.value.focalPoint.left, + top: $scope.control.value.focalPoint.top + } + imageOptions.mode = "crop"; + } + } + + return mediaHelper.getProcessedImageUrl($scope.control.value.image, imageOptions) + .then(function (url) { + return url; + }); + } + + return null; + }; + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index e3576426a3..c483e83845 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -222,9 +222,17 @@ angular.module('umbraco') // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { if (property.value && property.value.src) { - if (thumbnail === true) { - return property.value.src + "?width=500&mode=max&animationprocessmode=first"; + return mediaHelper.getProcessedImageUrl(property.value.src, + { + animationprocessmode: "first", + mode: "max", + width: 500 + }) + .then(function (url) { + return url; + }); + } else { return property.value.src; diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 960b589b6d..bbad627c3b 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -105,17 +105,17 @@ namespace Umbraco.Web.Editors int? focalPointLeft = null, int? focalPointTop = null, string animationProcessMode = "first", - string mode = "crop", + string mode = "max", bool upscale = false, string cacheBusterValue = "") { var options = new ImageUrlGenerationOptions(imagePath) { - AnimationProcessMode = "first", + AnimationProcessMode = animationProcessMode, CacheBusterValue = cacheBusterValue, Height = height, - ImageCropMode = "max", - UpScale = false, + ImageCropMode = mode, + UpScale = upscale, Width = width, }; if (focalPointLeft.HasValue && focalPointTop.HasValue) From d577452aff647006e01cd8742cfb60357afcbd08 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 24 Mar 2020 18:18:25 +0100 Subject: [PATCH 12/34] AB#5820 - Introduced WebProfiler for asp.net core --- .../Net/IUmbracoApplicationLifetime.cs | 4 + src/Umbraco.Core/UriExtensions.cs | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- .../AspNetCoreUmbracoApplicationLifetime.cs | 8 ++ ...racoRequestApplicationBuilderExtensions.cs | 21 +++ ...bracoRequestServiceCollectionExtensions.cs | 20 +++ .../Middleware/UmbracoRequestMiddleware.cs | 52 ++++++++ .../Runtime/Profiler/WebProfiler.cs | 83 ++++++++++++ .../Runtime/Profiler/WebProfilerComponent.cs | 55 ++++++++ .../Runtime/Profiler/WebProfilerComposer.cs | 8 ++ .../Runtime/Profiler/WebProfilerProvider.cs | 122 ++++++++++++++++++ .../Umbraco.Web.Common.csproj | 22 ++++ src/Umbraco.Web.UI.NetCore/Startup.cs | 4 + .../Umbraco.Web.UI.NetCore.csproj | 1 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../AspNetUmbracoApplicationLifetime.cs | 5 + .../Install/InstallSteps/NewInstallStep.cs | 35 +++-- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- src/umbraco.sln | 6 + 19 files changed, 436 insertions(+), 18 deletions(-) create mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs create mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs create mode 100644 src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs create mode 100644 src/Umbraco.Web.Common/Umbraco.Web.Common.csproj diff --git a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs index 10d5b10955..4010252571 100644 --- a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs @@ -1,3 +1,5 @@ +using System; + namespace Umbraco.Net { public interface IUmbracoApplicationLifetime @@ -10,5 +12,7 @@ namespace Umbraco.Net /// Terminates the current application. The application restarts the next time a request is received for it. /// void Restart(); + + event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 4321aefd7f..e7be0b6500 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -146,7 +146,7 @@ namespace Umbraco.Core /// /// /// - internal static bool IsClientSideRequest(this Uri url) + public static bool IsClientSideRequest(this Uri url) { try { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 29d69db0d2..7dd0d6592b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -99,7 +99,7 @@ - + diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index 20cfef352d..f597c40252 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -14,6 +15,11 @@ namespace Umbraco.Web.AspNet { _httpContextAccessor = httpContextAccessor; _hostApplicationLifetime = hostApplicationLifetime; + + hostApplicationLifetime.ApplicationStarted.Register(() => + { + ApplicationInit?.Invoke(this, EventArgs.Empty); + }); } public bool IsRestarting { get; set; } @@ -32,5 +38,7 @@ namespace Umbraco.Web.AspNet Thread.CurrentPrincipal = null; _hostApplicationLifetime.StopApplication(); } + + public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..110c09e1a9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Extensions +{ + public static class UmbracoRequestApplicationBuilderExtensions + { + public static IApplicationBuilder UseUmbracoRequest(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + app.UseMiddleware(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs new file mode 100644 index 0000000000..2a16b8b4f9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Extensions +{ + public static class UmbracoRequestServiceCollectionExtensions + { + public static IServiceCollection AddUmbracoRequest(this IServiceCollection services) + { + var umbracoRequestLifetime = new UmbracoRequestLifetime(); + + services.AddSingleton(umbracoRequestLifetime); + services.AddSingleton(umbracoRequestLifetime); + + return services; + } + + } + +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs new file mode 100644 index 0000000000..60b6463152 --- /dev/null +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Middleware +{ + public class UmbracoRequestMiddleware + { + private readonly RequestDelegate _next; + private readonly IUmbracoRequestLifetimeManager _umbracoRequestLifetimeManager; + public UmbracoRequestMiddleware(RequestDelegate next, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager) + { + _next = next; + _umbracoRequestLifetimeManager = umbracoRequestLifetimeManager; + } + + public async Task InvokeAsync(HttpContext context) + { + _umbracoRequestLifetimeManager.InitRequest(context); + await _next(context); + _umbracoRequestLifetimeManager.EndRequest(context); + } + } + + public interface IUmbracoRequestLifetime + { + event EventHandler RequestStart; + event EventHandler RequestEnd; + } + + public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager + { + public event EventHandler RequestStart; + public event EventHandler RequestEnd; + + public void InitRequest(HttpContext context) + { + RequestStart?.Invoke(this, context); + } + + public void EndRequest(HttpContext context) + { + RequestEnd?.Invoke(this, context); + } + } + + public interface IUmbracoRequestLifetimeManager + { + void InitRequest(HttpContext context); + void EndRequest(HttpContext context); + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs new file mode 100644 index 0000000000..e9fe7ea332 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -0,0 +1,83 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using StackExchange.Profiling; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfiler : IProfiler + { + private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebProfilerProvider _provider; + private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; + private int _first; + + //TODO register and inject WebProfilerProvider? + public WebProfiler(IHttpContextAccessor httpContextAccessor, IRequestCache requestCache) + { + // create our own provider, which can provide a profiler even during boot + _provider = new WebProfilerProvider(requestCache); + _httpContextAccessor = httpContextAccessor; + } + + public string Render() => MiniProfiler.Current + .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + + public IDisposable Step(string name) => MiniProfiler.Current?.Step(name); + + public void Start() + { + MiniProfiler.StartNew(); + } + + public void Stop(bool discardResults = false) + { + MiniProfiler.Current?.Stop(discardResults); + } + + public void UmbracoApplicationBeginRequest(HttpContext context) + { + // if this is the first request, notify our own provider that this request is the boot request + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + _provider.BeginBootRequest(); + context.Items[BootRequestItemKey] = true; + // and no need to start anything, profiler is already there + } + // else start a profiler, the normal way + else if (ShouldProfile(context.Request)) + Start(); + } + + public void UmbracoApplicationEndRequest(HttpContext context) + { + // if this is the boot request, or if we should profile this request, stop + // (the boot request is always profiled, no matter what) + if (context.Items.TryGetValue(BootRequestItemKey, out var isBoot) && isBoot.Equals(true)) + { + _provider.EndBootRequest(); + Stop(); + } + else if (ShouldProfile(context.Request)) + { + Stop(); + } + } + + private static bool ShouldProfile(HttpRequest request) + { + if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false; + if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; + if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; + return false; + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs new file mode 100644 index 0000000000..425996a765 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -0,0 +1,55 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Net; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + internal sealed class WebProfilerComponent : IComponent + { + private readonly bool _profile; + private readonly WebProfiler _profiler; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; + + public WebProfilerComponent(IProfiler profiler, ILogger logger, IUmbracoRequestLifetime umbracoRequestLifetime, + IUmbracoApplicationLifetime umbracoApplicationLifetime) + { + _umbracoRequestLifetime = umbracoRequestLifetime; + _umbracoApplicationLifetime = umbracoApplicationLifetime; + _profile = true; + + // although registered in WebRuntime.Compose, ensure that we have not + // been replaced by another component, and we are still "the" profiler + _profiler = profiler as WebProfiler; + if (_profiler != null) return; + + // if VoidProfiler was registered, let it be known + if (profiler is VoidProfiler) + logger.Info( + "Profiler is VoidProfiler, not profiling (must run debug mode to profile)."); + _profile = false; + } + + public void Initialize() + { + if (!_profile) return; + + // bind to ApplicationInit - ie execute the application initialization for *each* application + // it would be a mistake to try and bind to the current application events + _umbracoApplicationLifetime.ApplicationInit += InitializeApplication; + } + + public void Terminate() + { + } + + private void InitializeApplication(object sender, EventArgs args) + { + _umbracoRequestLifetime.RequestStart += + (sender, context) => _profiler.UmbracoApplicationBeginRequest(context); + _umbracoRequestLifetime.RequestEnd += (sender, context) => _profiler.UmbracoApplicationEndRequest(context); + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs new file mode 100644 index 0000000000..688a3e5c28 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs @@ -0,0 +1,8 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + internal class WebProfilerComposer : ComponentComposer, ICoreComposer + { + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs new file mode 100644 index 0000000000..3ba5f762fe --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs @@ -0,0 +1,122 @@ +using System; +using System.Threading; +using StackExchange.Profiling; +using StackExchange.Profiling.Internal; +using Umbraco.Core.Cache; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfilerProvider : DefaultProfilerProvider + { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private readonly IRequestCache _requestCache; + private volatile BootPhase _bootPhase; + private int _first; + private MiniProfiler _startupProfiler; + + public WebProfilerProvider(IRequestCache requestCache) + { + _requestCache = requestCache; + // booting... + _bootPhase = BootPhase.Boot; + } + + + /// + /// Gets the current profiler. + /// + /// + /// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise + /// returns the base class + /// + public override MiniProfiler CurrentProfiler + { + get + { + // if not booting then just use base (fast) + // no lock, _bootPhase is volatile + if (_bootPhase == BootPhase.Booted) + return base.CurrentProfiler; + + // else + try + { + var current = base.CurrentProfiler; + return current ?? _startupProfiler; + } + catch + { + return _startupProfiler; + } + } + } + + public void BeginBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.Boot) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.BootRequest; + + //TODO is this necessary? :mini-profiler: seems like a magic string + // assign the profiler to be the current MiniProfiler for the request + // is's already active, starting and all + _requestCache.Set(":mini-profiler:", _startupProfiler); + } + finally + { + _locker.ExitWriteLock(); + } + } + + public void EndBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.BootRequest) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.Booted; + + _startupProfiler = null; + } + finally + { + _locker.ExitWriteLock(); + } + } + + /// + /// Starts a new MiniProfiler. + /// + /// + /// + /// This is called when WebProfiler calls MiniProfiler.Start() so, + /// - as a result of WebRuntime starting the WebProfiler, and + /// - assuming profiling is enabled, on every BeginRequest that should be profiled, + /// - except for the very first one which is the boot request. + /// + /// + public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options) + { + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first == false) return base.Start(profilerName, options); + + _startupProfiler = new MiniProfiler("StartupProfiler", options); + CurrentProfiler = _startupProfiler; + return _startupProfiler; + } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj new file mode 100644 index 0000000000..cb9ca9156a --- /dev/null +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + Library + 8 + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 9ef4985aea..be81261d2a 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.Extensions; using Umbraco.Web.Website.AspNetCore; @@ -21,7 +22,9 @@ namespace Umbraco.Web.UI.BackOffice // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + services.AddUmbracoConfiguration(); + services.AddUmbracoRequest(); services.AddUmbracoWebsite(); services.AddUmbracoBackOffice(); } @@ -29,6 +32,7 @@ namespace Umbraco.Web.UI.BackOffice // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + app.UseUmbracoRequest(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index ca7c3e26fa..abfb172763 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index fe11a8d9aa..0c0e6aee50 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -101,7 +101,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + 3.4.0 diff --git a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs index 245e8ea374..f5ebc03cd0 100644 --- a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Web; using Umbraco.Net; @@ -11,6 +12,8 @@ namespace Umbraco.Web.AspNet public AspNetUmbracoApplicationLifetime(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; + + UmbracoApplicationBase.ApplicationInit += ApplicationInit; } public bool IsRestarting { get; set; } @@ -30,5 +33,7 @@ namespace Umbraco.Web.AspNet Thread.CurrentPrincipal = null; HttpRuntime.UnloadAppDomain(); } + + public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index f5a2ca8ac8..3f81c0d589 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -56,20 +56,7 @@ namespace Umbraco.Web.Install.InstallSteps throw new InvalidOperationException("Could not find the super user!"); } - var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); - var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); - if (membershipUser == null) - { - throw new InvalidOperationException($"No user found in membership provider with id of {Constants.Security.SuperUserId}."); - } - - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); - var resetResult = await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); - if (!resetResult.Succeeded) - { - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); - } + await ResetAdminPassword(user.Password); admin.Email = user.Email.Trim(); admin.Name = user.Name.Trim(); @@ -95,6 +82,26 @@ namespace Umbraco.Web.Install.InstallSteps return null; } + private async Task ResetAdminPassword(string clearTextPassword) + { + var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); + var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); + var resetResult = + await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, clearTextPassword.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); + } + } + /// /// Return a custom view model for this step /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e8aaecdf4d..b9892e1c31 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -92,7 +92,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/umbraco.sln b/src/umbraco.sln index be363ef2e6..11098abdec 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -123,6 +123,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "Umbr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Common", "Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.Common", "Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{839D3048-9718-4907-BDE0-7CD63D364383}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -195,6 +197,10 @@ Global {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.Build.0 = Debug|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.ActiveCfg = Release|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9339e1d999e00221925a8b8514ca21bb1d37c6e7 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 24 Mar 2020 18:18:25 +0100 Subject: [PATCH 13/34] AB#5820 - Introduced WebProfiler for asp.net core --- .../Net/IUmbracoApplicationLifetime.cs | 4 + src/Umbraco.Core/UriExtensions.cs | 2 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- .../AspNetCoreUmbracoApplicationLifetime.cs | 8 ++ ...racoRequestApplicationBuilderExtensions.cs | 21 ++++ ...bracoRequestServiceCollectionExtensions.cs | 20 +++ .../Middleware/UmbracoRequestMiddleware.cs | 52 ++++++++ .../Runtime/Profiler/WebProfiler.cs | 81 ++++++++++++ .../Runtime/Profiler/WebProfilerComponent.cs | 55 ++++++++ .../Runtime/Profiler/WebProfilerComposer.cs | 8 ++ .../Runtime/Profiler/WebProfilerProvider.cs | 119 ++++++++++++++++++ .../Umbraco.Web.Common.csproj | 22 ++++ src/Umbraco.Web.UI.NetCore/Startup.cs | 4 + .../Umbraco.Web.UI.NetCore.csproj | 1 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../AspNetUmbracoApplicationLifetime.cs | 5 + .../Install/InstallSteps/NewInstallStep.cs | 35 +++--- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- src/umbraco.sln | 6 + 19 files changed, 431 insertions(+), 18 deletions(-) create mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs create mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs create mode 100644 src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs create mode 100644 src/Umbraco.Web.Common/Umbraco.Web.Common.csproj diff --git a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs index 10d5b10955..4010252571 100644 --- a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs @@ -1,3 +1,5 @@ +using System; + namespace Umbraco.Net { public interface IUmbracoApplicationLifetime @@ -10,5 +12,7 @@ namespace Umbraco.Net /// Terminates the current application. The application restarts the next time a request is received for it. /// void Restart(); + + event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 4321aefd7f..e7be0b6500 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -146,7 +146,7 @@ namespace Umbraco.Core /// /// /// - internal static bool IsClientSideRequest(this Uri url) + public static bool IsClientSideRequest(this Uri url) { try { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 29d69db0d2..7dd0d6592b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -99,7 +99,7 @@ - + diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index 20cfef352d..f597c40252 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -14,6 +15,11 @@ namespace Umbraco.Web.AspNet { _httpContextAccessor = httpContextAccessor; _hostApplicationLifetime = hostApplicationLifetime; + + hostApplicationLifetime.ApplicationStarted.Register(() => + { + ApplicationInit?.Invoke(this, EventArgs.Empty); + }); } public bool IsRestarting { get; set; } @@ -32,5 +38,7 @@ namespace Umbraco.Web.AspNet Thread.CurrentPrincipal = null; _hostApplicationLifetime.StopApplication(); } + + public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..110c09e1a9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Extensions +{ + public static class UmbracoRequestApplicationBuilderExtensions + { + public static IApplicationBuilder UseUmbracoRequest(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + app.UseMiddleware(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs new file mode 100644 index 0000000000..2a16b8b4f9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Extensions +{ + public static class UmbracoRequestServiceCollectionExtensions + { + public static IServiceCollection AddUmbracoRequest(this IServiceCollection services) + { + var umbracoRequestLifetime = new UmbracoRequestLifetime(); + + services.AddSingleton(umbracoRequestLifetime); + services.AddSingleton(umbracoRequestLifetime); + + return services; + } + + } + +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs new file mode 100644 index 0000000000..60b6463152 --- /dev/null +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Middleware +{ + public class UmbracoRequestMiddleware + { + private readonly RequestDelegate _next; + private readonly IUmbracoRequestLifetimeManager _umbracoRequestLifetimeManager; + public UmbracoRequestMiddleware(RequestDelegate next, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager) + { + _next = next; + _umbracoRequestLifetimeManager = umbracoRequestLifetimeManager; + } + + public async Task InvokeAsync(HttpContext context) + { + _umbracoRequestLifetimeManager.InitRequest(context); + await _next(context); + _umbracoRequestLifetimeManager.EndRequest(context); + } + } + + public interface IUmbracoRequestLifetime + { + event EventHandler RequestStart; + event EventHandler RequestEnd; + } + + public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager + { + public event EventHandler RequestStart; + public event EventHandler RequestEnd; + + public void InitRequest(HttpContext context) + { + RequestStart?.Invoke(this, context); + } + + public void EndRequest(HttpContext context) + { + RequestEnd?.Invoke(this, context); + } + } + + public interface IUmbracoRequestLifetimeManager + { + void InitRequest(HttpContext context); + void EndRequest(HttpContext context); + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs new file mode 100644 index 0000000000..4e1a7c7d84 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using StackExchange.Profiling; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfiler : IProfiler + { + private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebProfilerProvider _provider; + private int _first; + + public WebProfiler(IHttpContextAccessor httpContextAccessor) + { + // create our own provider, which can provide a profiler even during boot + _provider = new WebProfilerProvider(); + _httpContextAccessor = httpContextAccessor; + } + + public string Render() => MiniProfiler.Current + .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + + public IDisposable Step(string name) => MiniProfiler.Current?.Step(name); + + public void Start() + { + MiniProfiler.StartNew(); + } + + public void Stop(bool discardResults = false) + { + MiniProfiler.Current?.Stop(discardResults); + } + + public void UmbracoApplicationBeginRequest(HttpContext context) + { + // if this is the first request, notify our own provider that this request is the boot request + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + _provider.BeginBootRequest(); + context.Items[BootRequestItemKey] = true; + // and no need to start anything, profiler is already there + } + // else start a profiler, the normal way + else if (ShouldProfile(context.Request)) + Start(); + } + + public void UmbracoApplicationEndRequest(HttpContext context) + { + // if this is the boot request, or if we should profile this request, stop + // (the boot request is always profiled, no matter what) + if (context.Items.TryGetValue(BootRequestItemKey, out var isBoot) && isBoot.Equals(true)) + { + _provider.EndBootRequest(); + Stop(); + } + else if (ShouldProfile(context.Request)) + { + Stop(); + } + } + + private static bool ShouldProfile(HttpRequest request) + { + if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false; + if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; + if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; + return false; + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs new file mode 100644 index 0000000000..425996a765 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -0,0 +1,55 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Net; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + internal sealed class WebProfilerComponent : IComponent + { + private readonly bool _profile; + private readonly WebProfiler _profiler; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; + + public WebProfilerComponent(IProfiler profiler, ILogger logger, IUmbracoRequestLifetime umbracoRequestLifetime, + IUmbracoApplicationLifetime umbracoApplicationLifetime) + { + _umbracoRequestLifetime = umbracoRequestLifetime; + _umbracoApplicationLifetime = umbracoApplicationLifetime; + _profile = true; + + // although registered in WebRuntime.Compose, ensure that we have not + // been replaced by another component, and we are still "the" profiler + _profiler = profiler as WebProfiler; + if (_profiler != null) return; + + // if VoidProfiler was registered, let it be known + if (profiler is VoidProfiler) + logger.Info( + "Profiler is VoidProfiler, not profiling (must run debug mode to profile)."); + _profile = false; + } + + public void Initialize() + { + if (!_profile) return; + + // bind to ApplicationInit - ie execute the application initialization for *each* application + // it would be a mistake to try and bind to the current application events + _umbracoApplicationLifetime.ApplicationInit += InitializeApplication; + } + + public void Terminate() + { + } + + private void InitializeApplication(object sender, EventArgs args) + { + _umbracoRequestLifetime.RequestStart += + (sender, context) => _profiler.UmbracoApplicationBeginRequest(context); + _umbracoRequestLifetime.RequestEnd += (sender, context) => _profiler.UmbracoApplicationEndRequest(context); + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs new file mode 100644 index 0000000000..688a3e5c28 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs @@ -0,0 +1,8 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + internal class WebProfilerComposer : ComponentComposer, ICoreComposer + { + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs new file mode 100644 index 0000000000..99ad6f724c --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading; +using StackExchange.Profiling; +using StackExchange.Profiling.Internal; +using Umbraco.Core.Cache; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfilerProvider : DefaultProfilerProvider + { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private volatile BootPhase _bootPhase; + private int _first; + private MiniProfiler _startupProfiler; + + public WebProfilerProvider() + { + // booting... + _bootPhase = BootPhase.Boot; + } + + + /// + /// Gets the current profiler. + /// + /// + /// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise + /// returns the base class + /// + public override MiniProfiler CurrentProfiler + { + get + { + // if not booting then just use base (fast) + // no lock, _bootPhase is volatile + if (_bootPhase == BootPhase.Booted) + return base.CurrentProfiler; + + // else + try + { + var current = base.CurrentProfiler; + return current ?? _startupProfiler; + } + catch + { + return _startupProfiler; + } + } + } + + public void BeginBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.Boot) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.BootRequest; + + // assign the profiler to be the current MiniProfiler for the request + // is's already active, starting and all + CurrentProfiler = _startupProfiler; + } + finally + { + _locker.ExitWriteLock(); + } + } + + public void EndBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.BootRequest) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.Booted; + + _startupProfiler = null; + } + finally + { + _locker.ExitWriteLock(); + } + } + + /// + /// Starts a new MiniProfiler. + /// + /// + /// + /// This is called when WebProfiler calls MiniProfiler.Start() so, + /// - as a result of WebRuntime starting the WebProfiler, and + /// - assuming profiling is enabled, on every BeginRequest that should be profiled, + /// - except for the very first one which is the boot request. + /// + /// + public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options) + { + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first == false) return base.Start(profilerName, options); + + _startupProfiler = new MiniProfiler("StartupProfiler", options); + CurrentProfiler = _startupProfiler; + return _startupProfiler; + } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj new file mode 100644 index 0000000000..cb9ca9156a --- /dev/null +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + Library + 8 + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 9ef4985aea..be81261d2a 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.Extensions; using Umbraco.Web.Website.AspNetCore; @@ -21,7 +22,9 @@ namespace Umbraco.Web.UI.BackOffice // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + services.AddUmbracoConfiguration(); + services.AddUmbracoRequest(); services.AddUmbracoWebsite(); services.AddUmbracoBackOffice(); } @@ -29,6 +32,7 @@ namespace Umbraco.Web.UI.BackOffice // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + app.UseUmbracoRequest(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index ca7c3e26fa..abfb172763 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index fe11a8d9aa..0c0e6aee50 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -101,7 +101,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + 3.4.0 diff --git a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs index 245e8ea374..f5ebc03cd0 100644 --- a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Web; using Umbraco.Net; @@ -11,6 +12,8 @@ namespace Umbraco.Web.AspNet public AspNetUmbracoApplicationLifetime(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; + + UmbracoApplicationBase.ApplicationInit += ApplicationInit; } public bool IsRestarting { get; set; } @@ -30,5 +33,7 @@ namespace Umbraco.Web.AspNet Thread.CurrentPrincipal = null; HttpRuntime.UnloadAppDomain(); } + + public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index f5a2ca8ac8..3f81c0d589 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -56,20 +56,7 @@ namespace Umbraco.Web.Install.InstallSteps throw new InvalidOperationException("Could not find the super user!"); } - var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); - var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); - if (membershipUser == null) - { - throw new InvalidOperationException($"No user found in membership provider with id of {Constants.Security.SuperUserId}."); - } - - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); - var resetResult = await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); - if (!resetResult.Succeeded) - { - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); - } + await ResetAdminPassword(user.Password); admin.Email = user.Email.Trim(); admin.Name = user.Name.Trim(); @@ -95,6 +82,26 @@ namespace Umbraco.Web.Install.InstallSteps return null; } + private async Task ResetAdminPassword(string clearTextPassword) + { + var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); + var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); + var resetResult = + await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, clearTextPassword.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); + } + } + /// /// Return a custom view model for this step /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e8aaecdf4d..b9892e1c31 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -92,7 +92,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/umbraco.sln b/src/umbraco.sln index be363ef2e6..11098abdec 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -123,6 +123,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "Umbr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Common", "Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.Common", "Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{839D3048-9718-4907-BDE0-7CD63D364383}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -195,6 +197,10 @@ Global {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.Build.0 = Debug|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.ActiveCfg = Release|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9cca16d31b574604b57814a0eb9233ed3967056a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 24 Mar 2020 19:37:04 +0100 Subject: [PATCH 14/34] Rollback of changes not related to this issue --- .../Install/InstallSteps/NewInstallStep.cs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index 3f81c0d589..fdec5bfa2e 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -56,7 +56,22 @@ namespace Umbraco.Web.Install.InstallSteps throw new InvalidOperationException("Could not find the super user!"); } - await ResetAdminPassword(user.Password); + var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); + var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); + if (membershipUser == null) + { + throw new InvalidOperationException( + $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); + } + + //To change the password here we actually need to reset it since we don't have an old one to use to change + var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); + var resetResult = + await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); + } admin.Email = user.Email.Trim(); admin.Name = user.Name.Trim(); @@ -82,26 +97,6 @@ namespace Umbraco.Web.Install.InstallSteps return null; } - private async Task ResetAdminPassword(string clearTextPassword) - { - var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); - var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); - if (membershipUser == null) - { - throw new InvalidOperationException( - $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); - } - - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); - var resetResult = - await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, clearTextPassword.Trim()); - if (!resetResult.Succeeded) - { - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); - } - } - /// /// Return a custom view model for this step /// From 92afbfc6fc7fc8e95569222e23022903f098354c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 25 Mar 2020 05:39:25 +0100 Subject: [PATCH 15/34] AB#5820 - Initialize the webprofiler in asp.net core --- .../Models/HostingSettings.cs | 2 +- .../DefaultUmbracoAssemblyProvider.cs | 3 +++ ...coBackOfficeServiceCollectionExtensions.cs | 20 ++++++++++++++-- .../Umbraco.Web.BackOffice.csproj | 1 + .../AspNetCoreUmbracoApplicationLifetime.cs | 2 +- ...bracoRequestServiceCollectionExtensions.cs | 20 ---------------- .../Runtime/Profiler/WebInitialComponent.cs | 18 ++++++++++++++ .../Runtime/Profiler/WebInitialComposer.cs | 24 +++++++++++++++++++ src/Umbraco.Web.UI.NetCore/Startup.cs | 1 - src/Umbraco.Web.UI.NetCore/appsettings.json | 3 +++ 10 files changed, 69 insertions(+), 25 deletions(-) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs (97%) delete mode 100644 src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs create mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs diff --git a/src/Umbraco.Configuration/Models/HostingSettings.cs b/src/Umbraco.Configuration/Models/HostingSettings.cs index 4a156cee6a..7cfc08d2d4 100644 --- a/src/Umbraco.Configuration/Models/HostingSettings.cs +++ b/src/Umbraco.Configuration/Models/HostingSettings.cs @@ -22,6 +22,6 @@ namespace Umbraco.Configuration.Models /// Gets a value indicating whether umbraco is running in [debug mode]. /// /// true if [debug mode]; otherwise, false. - public bool DebugMode => _configuration.GetValue(Prefix+":Debug", false); + public bool DebugMode => _configuration.GetValue(Prefix+"Debug", false); } } diff --git a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs index 61d7cff240..4d153d8922 100644 --- a/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs +++ b/src/Umbraco.Core/Composing/DefaultUmbracoAssemblyProvider.cs @@ -22,6 +22,9 @@ namespace Umbraco.Core.Composing "Umbraco.PublishedCache.NuCache", "Umbraco.ModelsBuilder.Embedded", "Umbraco.Examine.Lucene", + "Umbraco.Web.Common", + "Umbraco.Web.BackOffice", + "Umbraco.Web.Website", }; public DefaultUmbracoAssemblyProvider(Assembly entryPointAssembly) diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs index 135ba90b97..b63988d4e5 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; +using Umbraco.Web.Common.Runtime.Profiler; namespace Umbraco.Web.BackOffice.AspNetCore { @@ -118,7 +119,7 @@ namespace Umbraco.Web.BackOffice.AspNetCore private static void CreateCompositionRoot(IServiceCollection services) { // TODO: This isn't the best to have to resolve the services now but to avoid this will - // require quite a lot of re-work. + // require quite a lot of re-work. var serviceProvider = services.BuildServiceProvider(); var httpContextAccessor = serviceProvider.GetRequiredService(); @@ -142,11 +143,26 @@ namespace Umbraco.Web.BackOffice.AspNetCore new AspNetCoreMarchal()); var backOfficeInfo = new AspNetCoreBackOfficeInfo(globalSettings); - var profiler = new LogProfiler(logger); + var profiler = GetWebProfiler(hostingEnvironment, httpContextAccessor); Current.Initialize(logger, configs, ioHelper, hostingEnvironment, backOfficeInfo, profiler); } + private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment, IHttpContextAccessor httpContextAccessor) + { + // create and start asap to profile boot + if (!hostingEnvironment.IsDebugMode) + { + // should let it be null, that's how MiniProfiler is meant to work, + // but our own IProfiler expects an instance so let's get one + return new VoidProfiler(); + } + + var webProfiler = new WebProfiler(httpContextAccessor); + webProfiler.Start(); + + return webProfiler; + } private class AspNetCoreBootPermissionsChecker : IUmbracoBootPermissionChecker { public void ThrowIfNotPermissions() diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index bd20769d45..5b031c095e 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs similarity index 97% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index f597c40252..45cfb7d570 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using Umbraco.Net; -namespace Umbraco.Web.AspNet +namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime { diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs deleted file mode 100644 index 2a16b8b4f9..0000000000 --- a/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Web.Common.Middleware; - -namespace Umbraco.Web.Common.Extensions -{ - public static class UmbracoRequestServiceCollectionExtensions - { - public static IServiceCollection AddUmbracoRequest(this IServiceCollection services) - { - var umbracoRequestLifetime = new UmbracoRequestLifetime(); - - services.AddSingleton(umbracoRequestLifetime); - services.AddSingleton(umbracoRequestLifetime); - - return services; - } - - } - -} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs new file mode 100644 index 0000000000..2194b3c038 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs @@ -0,0 +1,18 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public sealed class WebInitialComponent : IComponent + { + + + public void Initialize() + { + } + + public void Terminate() + { + } + + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs new file mode 100644 index 0000000000..472d8bf8e9 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Net; +using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebInitialComposer : ComponentComposer, ICoreComposer + { + + public override void Compose(Composition composition) + { + base.Compose(composition); + + var umbracoRequestLifetime = new UmbracoRequestLifetime(); + + composition.RegisterUnique(factory => umbracoRequestLifetime); + composition.RegisterUnique(factory => umbracoRequestLifetime); + composition.RegisterUnique(); + } + } +} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 444b9a835d..8a04b0b17b 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -24,7 +24,6 @@ namespace Umbraco.Web.UI.BackOffice { services.AddUmbracoConfiguration(); - services.AddUmbracoRequest(); services.AddUmbracoCore(); services.AddUmbracoWebsite(); } diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 1c89647efa..2448ae0aa9 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -12,6 +12,9 @@ "AllowedHosts": "*", "Umbraco": { "CMS": { + "Hosting": { + "Debug": true + }, "Imaging": { "Resize": { "MaxWidth": 5000, From 681a25b861f632aae867c496de54ee35c0b774ea Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 27 Mar 2020 11:39:17 +0100 Subject: [PATCH 16/34] Moved files from backoffice into Common + Introduced AspNetCoreComponent to invoke the Umbraco Application Init event --- .../Net/IUmbracoApplicationLifetime.cs | 2 ++ .../Implementations/TestHelper.cs | 1 + .../Implementations/TestHostingEnvironment.cs | 4 +-- src/Umbraco.Tests.Integration/RuntimeTests.cs | 11 +++---- .../AspNetCoreApplicationShutdownRegistry.cs | 2 +- .../AspNetCore/AspNetCoreBackOfficeInfo.cs | 2 +- .../AspNetCoreHostingEnvironment.cs | 8 ++--- .../AspNetCore/AspNetCoreIpResolver.cs | 2 +- .../AspNetCore/AspNetCoreMarchal.cs | 2 +- .../AspNetCore/AspNetCoreSessionIdResolver.cs | 4 +-- .../AspNetCoreUmbracoApplicationLifetime.cs | 9 +++--- .../UmbracoCoreServiceCollectionExtensions.cs | 8 ++--- .../Lifetime/IUmbracoRequestLifetime.cs | 11 +++++++ .../IUmbracoRequestLifetimeManager.cs | 10 +++++++ .../Lifetime/UmbracoRequestLifetime.cs | 21 ++++++++++++++ .../Middleware/UmbracoRequestMiddleware.cs | 28 +----------------- .../Runtime/AspNetCoreComponent.cs | 29 +++++++++++++++++++ .../Runtime}/AspNetCoreComposer.cs | 15 ++++++++-- .../Runtime/Profiler/WebInitialComponent.cs | 18 ------------ .../Runtime/Profiler/WebInitialComposer.cs | 24 --------------- .../Runtime/Profiler/WebProfiler.cs | 9 ++++-- .../Runtime/Profiler/WebProfilerComponent.cs | 1 + .../Umbraco.Web.Common.csproj | 1 + src/Umbraco.Web.UI.NetCore/Startup.cs | 8 ++++- .../AspNetUmbracoApplicationLifetime.cs | 4 +++ src/Umbraco.Web/Logging/WebProfiler.cs | 2 +- 26 files changed, 134 insertions(+), 102 deletions(-) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs (97%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreBackOfficeInfo.cs (88%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreHostingEnvironment.cs (98%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreIpResolver.cs (91%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreMarchal.cs (88%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/AspNetCoreSessionIdResolver.cs (94%) rename src/{Umbraco.Web.BackOffice => Umbraco.Web.Common}/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs (97%) create mode 100644 src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs create mode 100644 src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs create mode 100644 src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs create mode 100644 src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs rename src/{Umbraco.Web.BackOffice/AspNetCore => Umbraco.Web.Common/Runtime}/AspNetCoreComposer.cs (54%) delete mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs delete mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs diff --git a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs index 8bf3f115cd..c84d475e90 100644 --- a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs @@ -15,5 +15,7 @@ namespace Umbraco.Net void Restart(); event EventHandler ApplicationInit; + + void InvokeApplicationInit(); } } diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs index 4a986fa35a..f10586b94b 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -16,6 +16,7 @@ using Umbraco.Core.Runtime; using Umbraco.Tests.Common; using Umbraco.Web.BackOffice; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.AspNetCore; using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; namespace Umbraco.Tests.Integration.Implementations diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs index 491b7e5480..652aad8928 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs @@ -1,11 +1,11 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Umbraco.Core.Configuration; -using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.AspNetCore; namespace Umbraco.Tests.Integration.Implementations { - + public class TestHostingEnvironment : AspNetCoreHostingEnvironment, Umbraco.Core.Hosting.IHostingEnvironment { public TestHostingEnvironment(IHostingSettings hostingSettings, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor) : base(hostingSettings, webHostEnvironment, httpContextAccessor) diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 0e11a29b95..c7bb22f135 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -22,6 +22,7 @@ using Umbraco.Tests.Common; using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.Integration.Testing; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.AspNetCore; using static Umbraco.Core.Migrations.Install.DatabaseBuilder; namespace Umbraco.Tests.Integration @@ -52,11 +53,11 @@ namespace Umbraco.Tests.Integration // LightInject / Umbraco var container = UmbracoServiceProviderFactory.CreateServiceContainer(); var serviceProviderFactory = new UmbracoServiceProviderFactory(container); - var umbracoContainer = serviceProviderFactory.GetContainer(); + var umbracoContainer = serviceProviderFactory.GetContainer(); // Special case since we are not using the Generic Host, we need to manually add an AspNetCore service to the container umbracoContainer.Register(x => Mock.Of()); - + var testHelper = new TestHelper(); // Create the core runtime @@ -70,7 +71,7 @@ namespace Umbraco.Tests.Integration Assert.IsTrue(coreRuntime.MainDom.IsMainDom); Assert.IsNull(coreRuntime.State.BootFailedException); - Assert.AreEqual(RuntimeLevel.Install, coreRuntime.State.Level); + Assert.AreEqual(RuntimeLevel.Install, coreRuntime.State.Level); Assert.IsTrue(MyComposer.IsComposed); Assert.IsFalse(MyComponent.IsInit); Assert.IsFalse(MyComponent.IsTerminated); @@ -88,7 +89,7 @@ namespace Umbraco.Tests.Integration } /// - /// Calling AddUmbracoCore to configure the container + /// Calling AddUmbracoCore to configure the container /// [Test] public async Task AddUmbracoCore() @@ -280,5 +281,5 @@ namespace Umbraco.Tests.Integration } } - + } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs similarity index 97% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs index 92af822836..57ad83d4ba 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreApplicationShutdownRegistry.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Hosting; using Umbraco.Core; using Umbraco.Core.Hosting; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreApplicationShutdownRegistry : IApplicationShutdownRegistry { diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreBackOfficeInfo.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs similarity index 88% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreBackOfficeInfo.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs index ba12b64dfe..375028132e 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreBackOfficeInfo.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs @@ -1,7 +1,7 @@ using Umbraco.Core; using Umbraco.Core.Configuration; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreBackOfficeInfo : IBackOfficeInfo { diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs similarity index 98% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 6f1298918d..c24c2c4698 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -6,16 +6,16 @@ using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Configuration; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreHostingEnvironment : Umbraco.Core.Hosting.IHostingEnvironment { - + private readonly IHostingSettings _hostingSettings; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; - + private string _localTempPath; public AspNetCoreHostingEnvironment(IHostingSettings hostingSettings, IWebHostEnvironment webHostEnvironment, IHttpContextAccessor httpContextAccessor) @@ -103,7 +103,7 @@ namespace Umbraco.Web.BackOffice.AspNetCore return applicationPath.Add(segment).Value; } - + } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs similarity index 91% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs index cee43757d8..863d545066 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreIpResolver.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreIpResolver.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; using Umbraco.Net; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { public class AspNetIpResolver : IIpResolver { diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreMarchal.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs similarity index 88% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreMarchal.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs index 247666090e..af23d092e9 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreMarchal.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreMarchal.cs @@ -2,7 +2,7 @@ using System; using System.Runtime.InteropServices; using Umbraco.Core.Diagnostics; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreMarchal : IMarchal diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs similarity index 94% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs index cafb02d367..818a39fac5 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreSessionIdResolver.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreSessionIdResolver.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Http.Features; using Umbraco.Net; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { internal class AspNetCoreSessionIdResolver : ISessionIdResolver { @@ -13,7 +13,7 @@ namespace Umbraco.Web.BackOffice.AspNetCore _httpContextAccessor = httpContextAccessor; } - + public string SessionId { get diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index 45cfb7d570..d73b9bebd4 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -15,11 +15,6 @@ namespace Umbraco.Web.Common.AspNetCore { _httpContextAccessor = httpContextAccessor; _hostApplicationLifetime = hostApplicationLifetime; - - hostApplicationLifetime.ApplicationStarted.Register(() => - { - ApplicationInit?.Invoke(this, EventArgs.Empty); - }); } public bool IsRestarting { get; set; } @@ -39,6 +34,10 @@ namespace Umbraco.Web.Common.AspNetCore _hostApplicationLifetime.StopApplication(); } + public void InvokeApplicationInit() + { + ApplicationInit?.Invoke(this, EventArgs.Empty); + } public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs similarity index 97% rename from src/Umbraco.Web.BackOffice/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs rename to src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs index c14b164e9c..eb320c459f 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs @@ -19,9 +19,8 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; using Umbraco.Web.Common.Runtime.Profiler; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { - // TODO: Move to Umbraco.Web.Common public static class UmbracoCoreServiceCollectionExtensions { /// @@ -71,7 +70,8 @@ namespace Umbraco.Web.BackOffice.AspNetCore public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment, IRegister umbContainer, Assembly entryAssembly) { if (services is null) throw new ArgumentNullException(nameof(services)); - if (umbContainer is null) throw new ArgumentNullException(nameof(umbContainer)); + var container = umbContainer; + if (container is null) throw new ArgumentNullException(nameof(container)); if (entryAssembly is null) throw new ArgumentNullException(nameof(entryAssembly)); // Special case! The generic host adds a few default services but we need to manually add this one here NOW because @@ -97,7 +97,7 @@ namespace Umbraco.Web.BackOffice.AspNetCore backOfficeInfo, typeFinder); - var factory = coreRuntime.Configure(umbContainer); + var factory = coreRuntime.Configure(container); return services; } diff --git a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs new file mode 100644 index 0000000000..616a75bfe7 --- /dev/null +++ b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs @@ -0,0 +1,11 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Lifetime +{ + public interface IUmbracoRequestLifetime + { + event EventHandler RequestStart; + event EventHandler RequestEnd; + } +} diff --git a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs new file mode 100644 index 0000000000..e4c671c2d3 --- /dev/null +++ b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Lifetime +{ + public interface IUmbracoRequestLifetimeManager + { + void InitRequest(HttpContext context); + void EndRequest(HttpContext context); + } +} diff --git a/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs b/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs new file mode 100644 index 0000000000..43810a9e66 --- /dev/null +++ b/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Lifetime +{ + public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager + { + public event EventHandler RequestStart; + public event EventHandler RequestEnd; + + public void InitRequest(HttpContext context) + { + RequestStart?.Invoke(this, context); + } + + public void EndRequest(HttpContext context) + { + RequestEnd?.Invoke(this, context); + } + } +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 60b6463152..93461fc1d5 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Umbraco.Web.Common.Lifetime; namespace Umbraco.Web.Common.Middleware { @@ -22,31 +23,4 @@ namespace Umbraco.Web.Common.Middleware } } - public interface IUmbracoRequestLifetime - { - event EventHandler RequestStart; - event EventHandler RequestEnd; - } - - public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager - { - public event EventHandler RequestStart; - public event EventHandler RequestEnd; - - public void InitRequest(HttpContext context) - { - RequestStart?.Invoke(this, context); - } - - public void EndRequest(HttpContext context) - { - RequestEnd?.Invoke(this, context); - } - } - - public interface IUmbracoRequestLifetimeManager - { - void InitRequest(HttpContext context); - void EndRequest(HttpContext context); - } } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs new file mode 100644 index 0000000000..04ae022fa5 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.Hosting; +using Umbraco.Core.Composing; +using Umbraco.Net; + +namespace Umbraco.Web.Common.Runtime +{ + public sealed class AspNetCoreComponent : IComponent + { + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + + public AspNetCoreComponent(IHostApplicationLifetime hostApplicationLifetime, IUmbracoApplicationLifetime umbracoApplicationLifetime) + { + _hostApplicationLifetime = hostApplicationLifetime; + _umbracoApplicationLifetime = umbracoApplicationLifetime; + } + + public void Initialize() + { + _hostApplicationLifetime.ApplicationStarted.Register(() => { + _umbracoApplicationLifetime.InvokeApplicationInit(); + }); + } + + public void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs similarity index 54% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreComposer.cs rename to src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 463af87849..f9a6a770e2 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -5,24 +5,33 @@ using Umbraco.Core.Hosting; using Umbraco.Net; using Umbraco.Core.Runtime; using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Lifetime; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.Runtime { /// /// Adds/replaces AspNetCore specific services /// [ComposeBefore(typeof(ICoreComposer))] [ComposeAfter(typeof(CoreInitialComposer))] - public class AspNetCoreComposer : IComposer + public class AspNetCoreComposer : ComponentComposer, IComposer { - public void Compose(Composition composition) + public new void Compose(Composition composition) { + base.Compose(composition); + // AspNetCore specific services composition.RegisterUnique(); // Our own netcore implementations composition.RegisterUnique(); composition.RegisterUnique(); + + // The umbraco request lifetime + var umbracoRequestLifetime = new UmbracoRequestLifetime(); + composition.RegisterUnique(factory => umbracoRequestLifetime); + composition.RegisterUnique(factory => umbracoRequestLifetime); + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs deleted file mode 100644 index 2194b3c038..0000000000 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Umbraco.Core.Composing; - -namespace Umbraco.Web.Common.Runtime.Profiler -{ - public sealed class WebInitialComponent : IComponent - { - - - public void Initialize() - { - } - - public void Terminate() - { - } - - } -} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs deleted file mode 100644 index 472d8bf8e9..0000000000 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebInitialComposer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Net; -using Umbraco.Web.Common.AspNetCore; -using Umbraco.Web.Common.Middleware; - -namespace Umbraco.Web.Common.Runtime.Profiler -{ - public class WebInitialComposer : ComponentComposer, ICoreComposer - { - - public override void Compose(Composition composition) - { - base.Compose(composition); - - var umbracoRequestLifetime = new UmbracoRequestLifetime(); - - composition.RegisterUnique(factory => umbracoRequestLifetime); - composition.RegisterUnique(factory => umbracoRequestLifetime); - composition.RegisterUnique(); - } - } -} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index 4e1a7c7d84..8baf8790c3 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -22,10 +22,15 @@ namespace Umbraco.Web.Common.Runtime.Profiler // create our own provider, which can provide a profiler even during boot _provider = new WebProfilerProvider(); _httpContextAccessor = httpContextAccessor; + + MiniProfiler.DefaultOptions.ProfilerProvider = _provider; } - public string Render() => MiniProfiler.Current - .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + public string Render() + { + return MiniProfiler.Current + .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + } public IDisposable Step(string name) => MiniProfiler.Current?.Step(name); diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs index 425996a765..9a656ae4f3 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Net; +using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.Common.Runtime.Profiler diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index cb9ca9156a..2cf6790484 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index fe77dfabda..b38333bb9a 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -8,7 +8,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Umbraco.Composing; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Extensions; using Umbraco.Web.Website.AspNetCore; @@ -52,6 +54,7 @@ namespace Umbraco.Web.UI.BackOffice app.UseDeveloperExceptionPage(); } + app.UseUmbracoCore(); app.UseUmbracoWebsite(); app.UseUmbracoBackOffice(); @@ -59,7 +62,10 @@ namespace Umbraco.Web.UI.BackOffice app.UseEndpoints(endpoints => { - endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); + }); }); } } diff --git a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs index f5ebc03cd0..9818a510b2 100644 --- a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs @@ -35,5 +35,9 @@ namespace Umbraco.Web.AspNet } public event EventHandler ApplicationInit; + public void InvokeApplicationInit() + { + + } } } diff --git a/src/Umbraco.Web/Logging/WebProfiler.cs b/src/Umbraco.Web/Logging/WebProfiler.cs index 512edb2296..e390950c0b 100755 --- a/src/Umbraco.Web/Logging/WebProfiler.cs +++ b/src/Umbraco.Web/Logging/WebProfiler.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.Logging MiniProfiler.Configure(new MiniProfilerOptions { SqlFormatter = new SqlServerFormatter(), - StackMaxLength = 5000, + StackMaxLength = 5000, ProfilerProvider = _provider }); } From 93fd2eb954a5bb8ba49ea91e80d75c07e9b690d2 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 28 Mar 2020 13:59:52 +0100 Subject: [PATCH 17/34] Fixed creation of image processing options in grid media controller. --- .../grid/editors/media.controller.js | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 1fb8cbe2f2..7c98fbfc5e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -2,7 +2,7 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", function ($scope, userService, editorService, mediaHelper) { - $scope.thumbnailUrl = getThumbnailUrl(); + setThumbnailUrl(); if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === true) { @@ -55,26 +55,24 @@ angular.module("umbraco") return; // simply skip that } - $scope.thumbnailUrl = getThumbnailUrl(); + setThumbnailUrl(); }, true); - function getThumbnailUrl() { + function setThumbnailUrl() { if ($scope.control.value && $scope.control.value.image) { - - var imageOptions; + var url = $scope.control.value.image; + var imageOptions = {}; if (url.indexOf('?') == -1) { // set default size if no crop present (moved from the view) - imageOptions = { - animationprocessmode : false, - width: 800 - } + imageOptions.animationprocessmode = false; + imageOptions.width = 800; } else { - imageOptions = { - animationprocessmode: "first", - height: $scope.control.editor.config.size.height, - width: $scope.control.editor.config.size.width - }; + if ($scope.control.editor.config && $scope.control.editor.config.size) { + imageOptions.animationprocessmode = "first"; + imageOptions.height = $scope.control.editor.config.size.height; + imageOptions.width = $scope.control.editor.config.size.width; + } if ($scope.control.value.focalPoint) { imageOptions.focalPoint = { @@ -85,13 +83,11 @@ angular.module("umbraco") } } - return mediaHelper.getProcessedImageUrl($scope.control.value.image, imageOptions) + mediaHelper.getProcessedImageUrl($scope.control.value.image, imageOptions) .then(function (url) { - return url; + $scope.thumbnailUrl = url; }); } - - return null; }; }); From 3e40389819c57a5edc98f592bf250ed0a53d10bc Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 28 Mar 2020 14:55:03 +0100 Subject: [PATCH 18/34] PR review amends on key value model and repository classes. --- src/Umbraco.Core/Models/IKeyValue.cs | 2 -- src/Umbraco.Core/Models/KeyValue.cs | 5 +++-- .../Implement/KeyValueRepository.cs | 18 +++--------------- .../Implement/RepositoryBaseOfTIdTEntity.cs | 2 +- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Core/Models/IKeyValue.cs b/src/Umbraco.Core/Models/IKeyValue.cs index 7396ae1b68..6025d4d37b 100644 --- a/src/Umbraco.Core/Models/IKeyValue.cs +++ b/src/Umbraco.Core/Models/IKeyValue.cs @@ -8,7 +8,5 @@ namespace Umbraco.Core.Models string Identifier { get; set; } string Value { get; set; } - - DateTime UpdateDate { get; set; } } } diff --git a/src/Umbraco.Core/Models/KeyValue.cs b/src/Umbraco.Core/Models/KeyValue.cs index ba98b577a4..2d47fcbfb3 100644 --- a/src/Umbraco.Core/Models/KeyValue.cs +++ b/src/Umbraco.Core/Models/KeyValue.cs @@ -9,11 +9,10 @@ namespace Umbraco.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class KeyValue : EntityBase, IKeyValue + public class KeyValue : EntityBase, IKeyValue, IEntity { private string _identifier; private string _value; - private DateTime _updateDate; /// public string Identifier @@ -28,5 +27,7 @@ namespace Umbraco.Core.Models get => _value; set => SetPropertyValueAndDetectChanges(value, ref _value, nameof(Value)); } + + bool IEntity.HasIdentity => !string.IsNullOrEmpty(Identifier); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index eab4049ec9..094c83b05f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -33,21 +33,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return database.FirstOrDefault(sql)?.Value; } - #region Overrides of IReadWriteQueryRepository - - public override void Save(IKeyValue entity) - { - if (Get(entity.Identifier) == null) - PersistNewItem(entity); - else - PersistUpdatedItem(entity); - } - - #endregion - #region Overrides of NPocoRepositoryBase - protected override Guid NodeObjectTypeId => throw new NotImplementedException(); + protected override Guid NodeObjectTypeId => throw new NotSupportedException(); protected override Sql GetBaseQuery(bool isCount) { @@ -70,7 +58,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IEnumerable GetDeleteClauses() { - return new List(); + return Enumerable.Empty(); } protected override IKeyValue PerformGet(string id) @@ -89,7 +77,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IEnumerable PerformGetByQuery(IQuery query) { - throw new NotImplementedException(); + throw new NotSupportedException(); } protected override void PersistNewItem(IKeyValue entity) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs index 6985bf78da..e8397ba22a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// /// This method is backed by an cache /// - public virtual void Save(TEntity entity) + public void Save(TEntity entity) { if (entity.HasIdentity == false) CachePolicy.Create(entity, PersistNewItem); From f01ccb49dd8d181a4ba33293163f7f83e6485da8 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 28 Mar 2020 15:04:36 +0100 Subject: [PATCH 19/34] Removed key value service initialization logic. --- .../CompositionExtensions/Repositories.cs | 2 - .../Custom/IKeyValueServiceInitialization.cs | 9 --- .../Custom/KeyValueServiceInitialization.cs | 80 ------------------- .../Runtime/CoreInitialComposer.cs | 14 ++-- .../Services/Implement/KeyValueService.cs | 56 +------------ src/Umbraco.Tests/TestHelpers/TestObjects.cs | 3 +- 6 files changed, 7 insertions(+), 157 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs delete mode 100644 src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs diff --git a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs index b8231241a1..9a23255cc4 100644 --- a/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Repositories.cs @@ -1,6 +1,5 @@ using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; -using Umbraco.Infrastructure.Migrations.Custom; namespace Umbraco.Core.Composing.CompositionExtensions { @@ -49,7 +48,6 @@ namespace Umbraco.Core.Composing.CompositionExtensions composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); diff --git a/src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs b/src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs deleted file mode 100644 index a863905dc7..0000000000 --- a/src/Umbraco.Infrastructure/Migrations/Custom/IKeyValueServiceInitialization.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Umbraco.Core.Persistence; - -namespace Umbraco.Infrastructure.Migrations.Custom -{ - public interface IKeyValueServiceInitialization - { - void PerformInitialMigration(IUmbracoDatabase database); - } -} diff --git a/src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs b/src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs deleted file mode 100644 index 488acf507a..0000000000 --- a/src/Umbraco.Infrastructure/Migrations/Custom/KeyValueServiceInitialization.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Infrastructure.Migrations.Custom -{ - public class KeyValueServiceInitialization : IKeyValueServiceInitialization - { - private readonly ILogger _logger; - - public KeyValueServiceInitialization(ILogger logger) - { - _logger = logger; - } - - public void PerformInitialMigration(IUmbracoDatabase database) - { - var context = new MigrationContext(database, _logger); - var initMigration = new InitialMigration(context); - initMigration.Migrate(); - } - - /// - /// A custom migration that executes standalone during the Initialize phase of the KeyValueService. - /// - internal class InitialMigration : MigrationBase - { - public InitialMigration(IMigrationContext context) - : base(context) - { } - - public override void Migrate() - { - // as long as we are still running 7 this migration will be invoked, - // but due to multiple restarts during upgrades, maybe the table - // exists already - if (TableExists(Constants.DatabaseSchema.Tables.KeyValue)) - return; - - Logger.Info("Creating KeyValue structure."); - - // the locks table was initially created with an identity (auto-increment) primary key, - // but we don't want this, especially as we are about to insert a new row into the table, - // so here we drop that identity - DropLockTableIdentity(); - - // insert the lock object for key/value - Insert.IntoTable(Constants.DatabaseSchema.Tables.Lock).Row(new { id = Constants.Locks.KeyValues, name = "KeyValues", value = 1 }).Do(); - - // create the key-value table - Create.Table().Do(); - } - - private void DropLockTableIdentity() - { - // one cannot simply drop an identity, that requires a bit of work - - // create a temp. id column and copy values - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("nid").AsInt32().Nullable().Do(); - Execute.Sql("update umbracoLock set nid = id").Do(); - - // drop the id column entirely (cannot just drop identity) - Delete.PrimaryKey("PK_umbracoLock").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - Delete.Column("id").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - - // recreate the id column without identity and copy values - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AddColumn("id").AsInt32().Nullable().Do(); - Execute.Sql("update umbracoLock set id = nid").Do(); - - // drop the temp. id column - Delete.Column("nid").FromTable(Constants.DatabaseSchema.Tables.Lock).Do(); - - // complete the primary key - Alter.Table(Constants.DatabaseSchema.Tables.Lock).AlterColumn("id").AsInt32().NotNullable().PrimaryKey("PK_umbracoLock").Do(); - } - } - } -} diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index f1f96a9f4a..aa9ccb958f 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Composing.CompositionExtensions; @@ -7,9 +6,8 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dashboards; -using Umbraco.Core.Hosting; using Umbraco.Core.Dictionary; -using Umbraco.Core.IO; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.Migrations; @@ -25,16 +23,15 @@ using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Core.Sync; -using Umbraco.Web.Models.PublishedContent; -using Umbraco.Web.PublishedCache; using Umbraco.Web; -using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Web.Install; -using Umbraco.Web.Trees; +using Umbraco.Web.Migrations.PostMigrations; +using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.PropertyEditors; +using Umbraco.Web.PublishedCache; using Umbraco.Web.Services; +using Umbraco.Web.Trees; using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidator; -using Umbraco.Infrastructure.Migrations.Custom; namespace Umbraco.Core.Runtime { @@ -149,7 +146,6 @@ namespace Umbraco.Core.Runtime .Append(); composition.RegisterUnique(factory => new MigrationBuilder(factory)); - composition.RegisterUnique(); // by default, register a noop factory composition.RegisterUnique(); diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs index 6835457f64..12ca34b020 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs @@ -1,74 +1,24 @@ using System; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; -using Umbraco.Infrastructure.Migrations.Custom; namespace Umbraco.Core.Services.Implement { internal class KeyValueService : IKeyValueService { - private readonly object _initialock = new object(); private readonly IScopeProvider _scopeProvider; private readonly IKeyValueRepository _repository; - private readonly IKeyValueServiceInitialization _initialization; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private bool _initialized; - public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository, IKeyValueServiceInitialization initialization, ILogger logger, IUmbracoVersion umbracoVersion) + public KeyValueService(IScopeProvider scopeProvider, IKeyValueRepository repository) { _scopeProvider = scopeProvider; _repository = repository; - _initialization = initialization; - _logger = logger; - _umbracoVersion = umbracoVersion; } - private void EnsureInitialized() - { - lock (_initialock) - { - if (_initialized) return; - Initialize(); - } - } - - private void Initialize() - { - // the key/value service is entirely self-managed, because it is used by the - // upgrader and anything we might change need to happen before everything else - - // if already running 8, either following an upgrade or an install, - // then everything should be ok (the table should exist, etc) - - if (_umbracoVersion.LocalVersion != null && _umbracoVersion.LocalVersion.Major >= 8) - { - _initialized = true; - return; - } - - // else we are upgrading from 7, we can assume that the locks table - // exists, but we need to create everything for key/value - - using (var scope = _scopeProvider.CreateScope()) - { - _initialization.PerformInitialMigration(scope.Database); - scope.Complete(); - } - - // but don't assume we are initializing - // we are upgrading from v7 and if anything goes wrong, - // the table and everything will be rolled back - } - /// public string GetValue(string key) { - EnsureInitialized(); - using (var scope = _scopeProvider.CreateScope()) { return _repository.Get(key)?.Value; @@ -78,8 +28,6 @@ namespace Umbraco.Core.Services.Implement /// public void SetValue(string key, string value) { - EnsureInitialized(); - using (var scope = _scopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.KeyValues); @@ -116,8 +64,6 @@ namespace Umbraco.Core.Services.Implement /// public bool TrySetValue(string key, string originalValue, string newValue) { - EnsureInitialized(); - using (var scope = _scopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.KeyValues); diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index ccbea01d0b..13d5cc0328 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -21,7 +21,6 @@ using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; -using Umbraco.Infrastructure.Migrations.Custom; using Umbraco.Tests.TestHelpers.Stubs; using Current = Umbraco.Web.Composing.Current; @@ -194,7 +193,7 @@ namespace Umbraco.Tests.TestHelpers var tagService = GetLazyService(factory, c => new TagService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var redirectUrlService = GetLazyService(factory, c => new RedirectUrlService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); var consentService = GetLazyService(factory, c => new ConsentService(scopeProvider, logger, eventMessagesFactory, GetRepo(c))); - var keyValueService = GetLazyService(factory, c => new KeyValueService(scopeProvider, GetRepo(c), Mock.Of(), logger, umbracoVersion)); + var keyValueService = GetLazyService(factory, c => new KeyValueService(scopeProvider, GetRepo(c))); var contentTypeServiceBaseFactory = GetLazyService(factory, c => new ContentTypeBaseServiceProvider(factory.GetInstance(),factory.GetInstance(),factory.GetInstance())); return new ServiceContext( From 8958cf90b978cbebc72a7f523ad61308dc31e189 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 28 Mar 2020 15:13:50 +0100 Subject: [PATCH 20/34] Moved non-scoped retrieval of key value into an extension method in IUmbracoDatabase. --- .../Repositories/Implement/KeyValueRepository.cs | 15 --------------- .../Persistence/UmbracoDatabaseExtensions.cs | 16 ++++++++++++++++ src/Umbraco.Infrastructure/RuntimeState.cs | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 094c83b05f..2b318b2056 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -18,21 +18,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement : base(scopeAccessor, AppCaches.NoCache, logger) { } - /// - /// Gets a value directly from the database, no scope, nothing. - /// - /// Used by to determine the runtime state. - internal static string GetValue(IUmbracoDatabase database, string key) - { - if (database is null) return null; - - var sql = database.SqlContext.Sql() - .Select() - .From() - .Where(x => x.Key == key); - return database.FirstOrDefault(sql)?.Value; - } - #region Overrides of NPocoRepositoryBase protected override Guid NodeObjectTypeId => throw new NotSupportedException(); diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index 249dd3dc73..4e5c3a113a 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Core.Persistence.Dtos; namespace Umbraco.Core.Persistence { @@ -10,5 +11,20 @@ namespace Umbraco.Core.Persistence if (asDatabase == null) throw new Exception("oops: database."); return asDatabase; } + + /// + /// Gets a key/value directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + public static string GetFromKeyValueTable(this IUmbracoDatabase database, string key) + { + if (database is null) return null; + + var sql = database.SqlContext.Sql() + .Select() + .From() + .Where(x => x.Key == key); + return database.FirstOrDefault(sql)?.Value; + } } } diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index 9e5cf96906..d6f674f028 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -225,7 +225,7 @@ namespace Umbraco.Core // no scope, no service - just directly accessing the database using (var database = databaseFactory.CreateDatabase()) { - CurrentMigrationState = KeyValueRepository.GetValue(database, stateValueKey); + CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey); FinalMigrationState = upgrader.Plan.FinalState; } From 5a29365063d1bd783137690edb86f6a7d96260bc Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 28 Mar 2020 15:20:17 +0100 Subject: [PATCH 21/34] Revert change to overridden save method on key value repository. --- .../Repositories/Implement/KeyValueRepository.cs | 12 ++++++++++++ .../Implement/RepositoryBaseOfTIdTEntity.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 2b318b2056..3c2da4d5b7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -18,6 +18,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement : base(scopeAccessor, AppCaches.NoCache, logger) { } + #region Overrides of IReadWriteQueryRepository + + public override void Save(IKeyValue entity) + { + if (Get(entity.Identifier) == null) + PersistNewItem(entity); + else + PersistUpdatedItem(entity); + } + + #endregion + #region Overrides of NPocoRepositoryBase protected override Guid NodeObjectTypeId => throw new NotSupportedException(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs index e8397ba22a..6985bf78da 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBaseOfTIdTEntity.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// /// This method is backed by an cache /// - public void Save(TEntity entity) + public virtual void Save(TEntity entity) { if (entity.HasIdentity == false) CachePolicy.Create(entity, PersistNewItem); From b2799ef90147f125061919bc3d462750bfcbfa4e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Sun, 29 Mar 2020 22:35:52 +0200 Subject: [PATCH 22/34] AB#5820 - Added simple controller and view to test the miniprofiler --- .../Controllers/BackOfficeController.cs | 14 +++ .../AspNetCore/AspNetCoreBackOfficeInfo.cs | 2 +- .../UmbracoCoreServiceCollectionExtensions.cs | 2 +- ...racoRequestApplicationBuilderExtensions.cs | 5 +- .../Runtime/Profiler/WebProfiler.cs | 104 +++++++++++---- .../Runtime/Profiler/WebProfilerComponent.cs | 3 + .../Runtime/Profiler/WebProfilerProvider.cs | 119 ------------------ .../Properties/launchSettings.json | 11 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 45 ++++++- .../Views/BackOffice/Index.cshtml | 20 +++ .../Editors/BackOfficeController.cs | 7 +- 11 files changed, 176 insertions(+), 156 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs delete mode 100644 src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs create mode 100644 src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs new file mode 100644 index 0000000000..83f6eda24a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Composing; + +namespace Umbraco.Web.BackOffice.Controllers +{ + public class BackOfficeController : Controller + { + // GET + public IActionResult Index() + { + return View(); + } + } +} diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs index 375028132e..39b1d7ff4e 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreBackOfficeInfo.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Common.AspNetCore GetAbsoluteUrl = globalSettings.UmbracoPath; } - public string GetAbsoluteUrl { get; } + public string GetAbsoluteUrl { get; } // TODO make absolute } } diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs index eb320c459f..99a274364b 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs @@ -169,7 +169,7 @@ namespace Umbraco.Web.Common.AspNetCore } var webProfiler = new WebProfiler(httpContextAccessor); - webProfiler.Start(); + webProfiler.StartBoot(); return webProfiler; } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs index 110c09e1a9..8278c99bf5 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Builder; +using StackExchange.Profiling; using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.Common.Extensions @@ -13,9 +14,11 @@ namespace Umbraco.Web.Common.Extensions throw new ArgumentNullException(nameof(app)); } - app.UseMiddleware(); + app.UseMiddleware(); + app.UseMiddleware(); return app; } } + } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index 8baf8790c3..a05483633d 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -1,76 +1,118 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using StackExchange.Profiling; +using StackExchange.Profiling.Internal; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Logging; -using Umbraco.Web.Common.Middleware; namespace Umbraco.Web.Common.Runtime.Profiler { public class WebProfiler : IProfiler { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private volatile BootPhase _bootPhase; + private MiniProfiler _startupProfiler; + + private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly WebProfilerProvider _provider; private int _first; public WebProfiler(IHttpContextAccessor httpContextAccessor) { // create our own provider, which can provide a profiler even during boot - _provider = new WebProfilerProvider(); _httpContextAccessor = httpContextAccessor; - - MiniProfiler.DefaultOptions.ProfilerProvider = _provider; + _bootPhase = BootPhase.Boot; } + /// + /// + /// + /// + /// Normally we would call MiniProfiler.Current.RenderIncludes(...), but because the requeststate is not set, this method does not work. + /// We fake the requestIds from the RequestState here. + /// public string Render() { - return MiniProfiler.Current - .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + + var profiler = MiniProfiler.Current; + if (profiler == null) return string.Empty; + + var context = _httpContextAccessor.HttpContext; + + var path = (profiler.Options as MiniProfilerOptions)?.RouteBasePath.Value.EnsureTrailingSlash(); + + var result = StackExchange.Profiling.Internal.Render.Includes( + profiler, + path: context.Request.PathBase + path, + isAuthorized: true, + requestIDs: new List{ profiler.Id }, + position: RenderPosition.Right, + showTrivial: profiler.Options.PopupShowTrivial, + showTimeWithChildren: profiler.Options.PopupShowTimeWithChildren, + maxTracesToShow: profiler.Options.PopupMaxTracesToShow, + showControls:profiler.Options.ShowControls, + startHidden: profiler.Options.PopupStartHidden); + + return result; } - public IDisposable Step(string name) => MiniProfiler.Current?.Step(name); + public IDisposable Step(string name) + { + return MiniProfiler.Current?.Step(name); + } public void Start() { MiniProfiler.StartNew(); } + public void StartBoot() + { + _startupProfiler = MiniProfiler.StartNew("Startup Profiler"); + } + + public void StopBoot() + { + _startupProfiler.Stop(); + } + public void Stop(bool discardResults = false) { MiniProfiler.Current?.Stop(discardResults); } + public void UmbracoApplicationBeginRequest(HttpContext context) { - // if this is the first request, notify our own provider that this request is the boot request - var first = Interlocked.Exchange(ref _first, 1) == 0; - if (first) + if (ShouldProfile(context.Request)) { - _provider.BeginBootRequest(); - context.Items[BootRequestItemKey] = true; - // and no need to start anything, profiler is already there - } - // else start a profiler, the normal way - else if (ShouldProfile(context.Request)) Start(); + } } public void UmbracoApplicationEndRequest(HttpContext context) { - // if this is the boot request, or if we should profile this request, stop - // (the boot request is always profiled, no matter what) - if (context.Items.TryGetValue(BootRequestItemKey, out var isBoot) && isBoot.Equals(true)) - { - _provider.EndBootRequest(); - Stop(); - } - else if (ShouldProfile(context.Request)) + if (ShouldProfile(context.Request)) { Stop(); + + // if this is the first request, append the startup profiler + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + + var startupDuration = _startupProfiler.Root.DurationMilliseconds.GetValueOrDefault(); + MiniProfiler.Current.DurationMilliseconds += startupDuration; + MiniProfiler.Current.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; + MiniProfiler.Current.Root.AddChild(_startupProfiler.Root); + + _startupProfiler = null; + } } } @@ -82,5 +124,15 @@ namespace Umbraco.Web.Common.Runtime.Profiler if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; return false; } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs index 9a656ae4f3..a36753e634 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -51,6 +51,9 @@ namespace Umbraco.Web.Common.Runtime.Profiler _umbracoRequestLifetime.RequestStart += (sender, context) => _profiler.UmbracoApplicationBeginRequest(context); _umbracoRequestLifetime.RequestEnd += (sender, context) => _profiler.UmbracoApplicationEndRequest(context); + + // Stop the profiling of the booting process + _profiler.StopBoot(); } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs deleted file mode 100644 index 99ad6f724c..0000000000 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Threading; -using StackExchange.Profiling; -using StackExchange.Profiling.Internal; -using Umbraco.Core.Cache; - -namespace Umbraco.Web.Common.Runtime.Profiler -{ - public class WebProfilerProvider : DefaultProfilerProvider - { - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - private volatile BootPhase _bootPhase; - private int _first; - private MiniProfiler _startupProfiler; - - public WebProfilerProvider() - { - // booting... - _bootPhase = BootPhase.Boot; - } - - - /// - /// Gets the current profiler. - /// - /// - /// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise - /// returns the base class - /// - public override MiniProfiler CurrentProfiler - { - get - { - // if not booting then just use base (fast) - // no lock, _bootPhase is volatile - if (_bootPhase == BootPhase.Booted) - return base.CurrentProfiler; - - // else - try - { - var current = base.CurrentProfiler; - return current ?? _startupProfiler; - } - catch - { - return _startupProfiler; - } - } - } - - public void BeginBootRequest() - { - _locker.EnterWriteLock(); - try - { - if (_bootPhase != BootPhase.Boot) - throw new InvalidOperationException("Invalid boot phase."); - _bootPhase = BootPhase.BootRequest; - - // assign the profiler to be the current MiniProfiler for the request - // is's already active, starting and all - CurrentProfiler = _startupProfiler; - } - finally - { - _locker.ExitWriteLock(); - } - } - - public void EndBootRequest() - { - _locker.EnterWriteLock(); - try - { - if (_bootPhase != BootPhase.BootRequest) - throw new InvalidOperationException("Invalid boot phase."); - _bootPhase = BootPhase.Booted; - - _startupProfiler = null; - } - finally - { - _locker.ExitWriteLock(); - } - } - - /// - /// Starts a new MiniProfiler. - /// - /// - /// - /// This is called when WebProfiler calls MiniProfiler.Start() so, - /// - as a result of WebRuntime starting the WebProfiler, and - /// - assuming profiling is enabled, on every BeginRequest that should be profiled, - /// - except for the very first one which is the boot request. - /// - /// - public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options) - { - var first = Interlocked.Exchange(ref _first, 1) == 0; - if (first == false) return base.Start(profilerName, options); - - _startupProfiler = new MiniProfiler("StartupProfiler", options); - CurrentProfiler = _startupProfiler; - return _startupProfiler; - } - - /// - /// Indicates the boot phase. - /// - private enum BootPhase - { - Boot = 0, // boot phase (before the 1st request even begins) - BootRequest = 1, // request boot phase (during the 1st request) - Booted = 2 // done booting - } - } -} diff --git a/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json b/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json index b145249bb5..27f36121ac 100644 --- a/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json +++ b/src/Umbraco.Web.UI.NetCore/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, + "windowsAuthentication": false, + "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:36804", "sslPort": 44354 @@ -15,10 +15,11 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Umbraco.Web.UI.BackOffice": { + "Umbraco.Web.UI.NetCore": { "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", + "launchBrowser": false, + "applicationUrl": "https://localhost:44354;http://localhost:9000", + "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index b38333bb9a..e0440e39ea 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -5,13 +5,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using StackExchange.Profiling; using Umbraco.Composing; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Web.BackOffice.AspNetCore; using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Runtime.Profiler; using Umbraco.Web.Website.AspNetCore; @@ -43,11 +49,19 @@ namespace Umbraco.Web.UI.BackOffice services.AddUmbracoConfiguration(_config); services.AddUmbracoCore(_webHostEnvironment); services.AddUmbracoWebsite(); + + services.AddMvc(); + services.AddMiniProfiler(options => + { + options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + + // app.UseMiniProfiler(); app.UseUmbracoRequest(); if (env.IsDevelopment()) { @@ -59,9 +73,17 @@ namespace Umbraco.Web.UI.BackOffice app.UseUmbracoBackOffice(); app.UseRouting(); - app.UseEndpoints(endpoints => { + endpoints.MapControllerRoute( + "backoffice-spa", + "/umbraco", + new + { + Controller = "BackOffice", + Action = "Index" + } + ); endpoints.MapGet("/", async context => { await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); @@ -69,4 +91,25 @@ namespace Umbraco.Web.UI.BackOffice }); } } + + public class UmbracoMiniProfilerOptions : MiniProfilerOptions + { + public UmbracoMiniProfilerOptions() + { + } + + public new MiniProfiler StartProfiler(string profilerName = null) + { + return base.StartProfiler(); + } + + public new Func ShouldProfile => (request) => + { + if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false; + if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; + if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; + return false; + }; + } } diff --git a/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml b/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml new file mode 100644 index 0000000000..1b5007c005 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml @@ -0,0 +1,20 @@ +@using Umbraco.Composing + +@{ + Layout = null; +} + + + + + + title + + +
+ Hello World + @Html.Raw(Current.Profiler.Render()) +
+ + + diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index e39d1658a2..09076400de 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -104,8 +104,11 @@ namespace Umbraco.Web.Editors public async Task Default() { return await RenderDefaultOrProcessExternalLoginAsync( - () => View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings,_ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings)), - () => View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings))); + () => + View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings,_ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings)), + () => + View(_ioHelper.BackOfficePath.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings, _umbracoVersion, _contentSettings, _ioHelper, _treeCollection, _httpContextAccessor, _hostingEnvironment, _runtimeSettings, _securitySettings)) + ); } [HttpGet] From ff5d6770d929e52e7fb2d756f213b8610f1e9902 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Mar 2020 06:34:56 +0200 Subject: [PATCH 23/34] AB#5820 - Clean up and remove unnecessary stuff for this task --- .../Controllers/BackOfficeController.cs | 14 ------------- src/Umbraco.Web.UI.NetCore/Startup.cs | 9 --------- .../Umbraco.Web.UI.NetCore.csproj | 1 + .../Views/BackOffice/Index.cshtml | 20 ------------------- 4 files changed, 1 insertion(+), 43 deletions(-) delete mode 100644 src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs delete mode 100644 src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs deleted file mode 100644 index 83f6eda24a..0000000000 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Composing; - -namespace Umbraco.Web.BackOffice.Controllers -{ - public class BackOfficeController : Controller - { - // GET - public IActionResult Index() - { - return View(); - } - } -} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index e0440e39ea..f674832952 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -75,15 +75,6 @@ namespace Umbraco.Web.UI.BackOffice app.UseRouting(); app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute( - "backoffice-spa", - "/umbraco", - new - { - Controller = "BackOffice", - Action = "Index" - } - ); endpoints.MapGet("/", async context => { await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index abfb172763..0e6f2cf6b0 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -18,6 +18,7 @@ <_ContentIncludedByDefault Remove="wwwroot\~\App_Data\TEMP\TypesCache\umbraco-types.DESKTOP-2016.hash" /> <_ContentIncludedByDefault Remove="wwwroot\~\App_Data\TEMP\TypesCache\umbraco-types.DESKTOP-2016.list" /> + <_ContentIncludedByDefault Remove="Views\BackOffice\Index.cshtml" /> diff --git a/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml b/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml deleted file mode 100644 index 1b5007c005..0000000000 --- a/src/Umbraco.Web.UI.NetCore/Views/BackOffice/Index.cshtml +++ /dev/null @@ -1,20 +0,0 @@ -@using Umbraco.Composing - -@{ - Layout = null; -} - - - - - - title - - -
- Hello World - @Html.Raw(Current.Profiler.Render()) -
- - - From 8ed7ae470ad541986c9dff64ede3c099f4d19701 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Mar 2020 06:38:39 +0200 Subject: [PATCH 24/34] AB#5820 - Clean up --- src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index a05483633d..431c195d90 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -13,12 +13,8 @@ namespace Umbraco.Web.Common.Runtime.Profiler { public class WebProfiler : IProfiler { - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - private volatile BootPhase _bootPhase; private MiniProfiler _startupProfiler; - - private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; private readonly IHttpContextAccessor _httpContextAccessor; private int _first; @@ -26,7 +22,6 @@ namespace Umbraco.Web.Common.Runtime.Profiler { // create our own provider, which can provide a profiler even during boot _httpContextAccessor = httpContextAccessor; - _bootPhase = BootPhase.Boot; } /// From 2fc21667672d66ad755e8291af058deecaca3a97 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Mar 2020 07:14:22 +0200 Subject: [PATCH 25/34] AB#5820 - Clean up --- src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs | 4 ++++ .../AspNetCoreUmbracoApplicationLifetime.cs | 2 +- .../Lifetime/IUmbracoRequestLifetimeManager.cs | 2 +- .../Runtime/AspNetCoreComponent.cs | 8 ++++---- .../Runtime/AspNetCoreComposer.cs | 13 ++++++++----- .../Runtime/Profiler/WebProfilerComponent.cs | 4 ++-- .../AspNet/AspNetUmbracoApplicationLifetime.cs | 4 ++-- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs index c84d475e90..f594e45652 100644 --- a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs @@ -15,7 +15,11 @@ namespace Umbraco.Net void Restart(); event EventHandler ApplicationInit; + } + + public interface IUmbracoApplicationLifetimeManager: IUmbracoApplicationLifetime + { void InvokeApplicationInit(); } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index d73b9bebd4..38d78860b0 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -6,7 +6,7 @@ using Umbraco.Net; namespace Umbraco.Web.Common.AspNetCore { - public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime + public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime, IUmbracoApplicationLifetimeManager { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHostApplicationLifetime _hostApplicationLifetime; diff --git a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs index e4c671c2d3..7ee8fac1e9 100644 --- a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs +++ b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs @@ -2,7 +2,7 @@ namespace Umbraco.Web.Common.Lifetime { - public interface IUmbracoRequestLifetimeManager + public interface IUmbracoRequestLifetimeManager : IUmbracoRequestLifetime { void InitRequest(HttpContext context); void EndRequest(HttpContext context); diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs index 04ae022fa5..991dce55e6 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComponent.cs @@ -7,18 +7,18 @@ namespace Umbraco.Web.Common.Runtime public sealed class AspNetCoreComponent : IComponent { private readonly IHostApplicationLifetime _hostApplicationLifetime; - private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoApplicationLifetimeManager _umbracoApplicationLifetimeManager; - public AspNetCoreComponent(IHostApplicationLifetime hostApplicationLifetime, IUmbracoApplicationLifetime umbracoApplicationLifetime) + public AspNetCoreComponent(IHostApplicationLifetime hostApplicationLifetime, IUmbracoApplicationLifetimeManager umbracoApplicationLifetimeManager) { _hostApplicationLifetime = hostApplicationLifetime; - _umbracoApplicationLifetime = umbracoApplicationLifetime; + _umbracoApplicationLifetimeManager = umbracoApplicationLifetimeManager; } public void Initialize() { _hostApplicationLifetime.ApplicationStarted.Register(() => { - _umbracoApplicationLifetime.InvokeApplicationInit(); + _umbracoApplicationLifetimeManager.InvokeApplicationInit(); }); } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index f9a6a770e2..a0b2fb9dd8 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -24,14 +24,17 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); // Our own netcore implementations - composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterUnique(); // The umbraco request lifetime - var umbracoRequestLifetime = new UmbracoRequestLifetime(); - composition.RegisterUnique(factory => umbracoRequestLifetime); - composition.RegisterUnique(factory => umbracoRequestLifetime); - composition.RegisterUnique(); + composition.RegisterUnique(); + composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs index a36753e634..7f0b5071c5 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -11,11 +11,11 @@ namespace Umbraco.Web.Common.Runtime.Profiler { private readonly bool _profile; private readonly WebProfiler _profiler; - private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoApplicationLifetimeManager _umbracoApplicationLifetime; private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; public WebProfilerComponent(IProfiler profiler, ILogger logger, IUmbracoRequestLifetime umbracoRequestLifetime, - IUmbracoApplicationLifetime umbracoApplicationLifetime) + IUmbracoApplicationLifetimeManager umbracoApplicationLifetime) { _umbracoRequestLifetime = umbracoRequestLifetime; _umbracoApplicationLifetime = umbracoApplicationLifetime; diff --git a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs index 9818a510b2..f0ff6e3cad 100644 --- a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs @@ -5,7 +5,7 @@ using Umbraco.Net; namespace Umbraco.Web.AspNet { - public class AspNetUmbracoApplicationLifetime : IUmbracoApplicationLifetime + public class AspNetUmbracoApplicationLifetime : IUmbracoApplicationLifetime, IUmbracoApplicationLifetimeManager { private readonly IHttpContextAccessor _httpContextAccessor; @@ -37,7 +37,7 @@ namespace Umbraco.Web.AspNet public event EventHandler ApplicationInit; public void InvokeApplicationInit() { - + ApplicationInit?.Invoke(this, EventArgs.Empty); } } } From 49da01c65579e1aec575a30bd27c81b8e937c59c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Mar 2020 07:16:55 +0200 Subject: [PATCH 26/34] AB#5820 - Clean up --- src/Umbraco.Web.UI.NetCore/Startup.cs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index f674832952..3906f1c8e9 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -82,25 +82,4 @@ namespace Umbraco.Web.UI.BackOffice }); } } - - public class UmbracoMiniProfilerOptions : MiniProfilerOptions - { - public UmbracoMiniProfilerOptions() - { - } - - public new MiniProfiler StartProfiler(string profilerName = null) - { - return base.StartProfiler(); - } - - public new Func ShouldProfile => (request) => - { - if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false; - if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; - if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; - if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; - return false; - }; - } } From 1e33b3df7a5558d8f9cfb253afc71631b3a6a33f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 30 Mar 2020 07:17:56 +0200 Subject: [PATCH 27/34] AB#5820 - Clean up --- src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 0e6f2cf6b0..abfb172763 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -18,7 +18,6 @@ <_ContentIncludedByDefault Remove="wwwroot\~\App_Data\TEMP\TypesCache\umbraco-types.DESKTOP-2016.hash" /> <_ContentIncludedByDefault Remove="wwwroot\~\App_Data\TEMP\TypesCache\umbraco-types.DESKTOP-2016.list" /> - <_ContentIncludedByDefault Remove="Views\BackOffice\Index.cshtml" /> From 9ed925941f0a4100808f97b0a2250abd4ed4627a Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2020 17:25:29 +1100 Subject: [PATCH 28/34] Gets DB installation test working with runtime level checking --- .../Models/GlobalSettings.cs | 2 +- .../Umbraco.Configuration.csproj | 6 + .../Configuration/UmbracoVersion.cs | 10 +- .../Install/DatabaseSchemaCreator.cs | 8 +- .../Persistence/LocalDb.cs | 5 + .../Persistence/UmbracoDatabase.cs | 43 +++ .../Persistence/UmbracoDatabaseFactory.cs | 32 +- .../Runtime/SqlMainDomLock.cs | 8 +- .../GlobalSetupTeardown.cs | 29 ++ .../ApplicationBuilderExtensions.cs | 91 +++++ .../Implementations/HostBuilderExtensions.cs | 42 --- src/Umbraco.Tests.Integration/RuntimeTests.cs | 29 +- .../Testing/LocalDbTestDatabase.cs | 332 ++++++++++++++++++ .../Testing/TestLocalDb.cs | 49 --- .../Persistence/DatabaseContextTests.cs | 2 +- .../FaultHandling/ConnectionRetryTest.cs | 4 +- src/Umbraco.Tests/TestHelpers/TestObjects.cs | 5 +- .../TestHelpers/TestWithDatabaseBase.cs | 2 +- src/Umbraco.Tests/Testing/UmbracoTestBase.cs | 6 +- 19 files changed, 552 insertions(+), 153 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs create mode 100644 src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs delete mode 100644 src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs create mode 100644 src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs delete mode 100644 src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs diff --git a/src/Umbraco.Configuration/Models/GlobalSettings.cs b/src/Umbraco.Configuration/Models/GlobalSettings.cs index 4dc764a974..ed1b4e58a2 100644 --- a/src/Umbraco.Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Configuration/Models/GlobalSettings.cs @@ -12,7 +12,7 @@ namespace Umbraco.Configuration.Models /// internal class GlobalSettings : IGlobalSettings { - private const string Prefix = Constants.Configuration.ConfigPrefix + "Global:"; + public const string Prefix = Constants.Configuration.ConfigPrefix + "Global:"; internal const string StaticReservedPaths = "~/app_plugins/,~/install/,~/mini-profiler-resources/,"; //must end with a comma! diff --git a/src/Umbraco.Configuration/Umbraco.Configuration.csproj b/src/Umbraco.Configuration/Umbraco.Configuration.csproj index 88bb3639d0..2337ea24f8 100644 --- a/src/Umbraco.Configuration/Umbraco.Configuration.csproj +++ b/src/Umbraco.Configuration/Umbraco.Configuration.csproj @@ -32,4 +32,10 @@ + + + <_Parameter1>Umbraco.Tests.Integration + + + diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index dd96e6edd7..35154a9f50 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Configuration private readonly IGlobalSettings _globalSettings; public UmbracoVersion(IGlobalSettings globalSettings) - :this() + : this() { _globalSettings = globalSettings; } @@ -55,7 +55,7 @@ namespace Umbraco.Core.Configuration /// Is the one that the CLR checks for compatibility. Therefore, it changes only on /// hard-breaking changes (for instance, on new major versions). /// - public Version AssemblyVersion {get; } + public Version AssemblyVersion { get; } /// /// Gets the assembly file version of the Umbraco code. @@ -83,11 +83,13 @@ namespace Umbraco.Core.Configuration /// and changes during an upgrade. The executing code version changes when new code is /// deployed. The site/files version changes during an upgrade. /// - public SemVersion LocalVersion { + public SemVersion LocalVersion + { get { var value = _globalSettings.ConfigurationStatus; return value.IsNullOrWhiteSpace() ? null : SemVersion.TryParse(value, out var semver) ? semver : null; - } } + } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index 8a79cca403..921ba0b3d5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -130,7 +130,7 @@ namespace Umbraco.Core.Migrations.Install if (e.Cancel == false) { - var dataCreation = new DatabaseDataCreator(_database, _logger,_umbracoVersion, _globalSettings); + var dataCreation = new DatabaseDataCreator(_database, _logger, _umbracoVersion, _globalSettings); foreach (var table in OrderedTables) CreateTable(false, table, dataCreation); } @@ -451,7 +451,7 @@ namespace Umbraco.Core.Migrations.Install //Execute the Create Table sql var created = _database.Execute(new Sql(createSql)); - _logger.Info("Create Table {TableName} ({Created}): \n {Sql}", tableName, created, createSql); + _logger.Info("Create Table {TableName} ({Created}): \n {Sql}", tableName, created, createSql); //If any statements exists for the primary key execute them here if (string.IsNullOrEmpty(createPrimaryKeySql) == false) @@ -487,11 +487,11 @@ namespace Umbraco.Core.Migrations.Install if (overwrite) { - _logger.Info("Table {TableName} was recreated", tableName); + _logger.Info("Table {TableName} was recreated", tableName); } else { - _logger.Info("New table {TableName} was created", tableName); + _logger.Info("New table {TableName} was created", tableName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 55d6565344..4ec233e17f 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -253,6 +253,11 @@ namespace Umbraco.Core.Persistence _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; } + public static string GetConnectionString(string instanceName, string databaseName) + { + return $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; + } + /// /// Gets a LocalDb connection string. /// diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index ea3d603f95..a4b4afbe25 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -28,6 +28,7 @@ namespace Umbraco.Core.Persistence private readonly RetryPolicy _connectionRetryPolicy; private readonly RetryPolicy _commandRetryPolicy; private readonly Guid _instanceGuid = Guid.NewGuid(); + private List _commands; #region Ctor @@ -162,6 +163,14 @@ namespace Umbraco.Core.Persistence /// public int SqlCount { get; private set; } + internal bool LogCommands + { + get => _commands != null; + set => _commands = value ? new List() : null; + } + + internal IEnumerable Commands => _commands; + public int BulkInsertRecords(IEnumerable records) { return _bulkSqlInsertProvider.BulkInsertRecords(this, records); @@ -267,9 +276,43 @@ namespace Umbraco.Core.Persistence if (_enableCount) SqlCount++; + _commands?.Add(new CommandInfo(cmd)); + base.OnExecutedCommand(cmd); } #endregion + + // used for tracking commands + public class CommandInfo + { + public CommandInfo(IDbCommand cmd) + { + Text = cmd.CommandText; + var parameters = new List(); + foreach (IDbDataParameter parameter in cmd.Parameters) parameters.Add(new ParameterInfo(parameter)); + Parameters = parameters.ToArray(); + } + + public string Text { get; } + public ParameterInfo[] Parameters { get; } + } + + // used for tracking commands + public class ParameterInfo + { + public ParameterInfo(IDbDataParameter parameter) + { + Name = parameter.ParameterName; + Value = parameter.Value; + DbType = parameter.DbType; + Size = parameter.Size; + } + + public string Name { get; } + public object Value { get; } + public DbType DbType { get; } + public int Size { get; } + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index a93c8db409..837fe6cba3 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -29,7 +29,6 @@ namespace Umbraco.Core.Persistence { private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IGlobalSettings _globalSettings; - private readonly IConnectionStrings _connectionStrings; private readonly Lazy _mappers; private readonly ILogger _logger; @@ -37,7 +36,6 @@ namespace Umbraco.Core.Persistence private DatabaseFactory _npocoDatabaseFactory; private IPocoDataFactory _pocoDataFactory; - private string _connectionString; private string _providerName; private DatabaseType _databaseType; private ISqlSyntaxProvider _sqlSyntax; @@ -73,7 +71,7 @@ namespace Umbraco.Core.Persistence /// /// Used by core runtime. public UmbracoDatabaseFactory(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, Lazy mappers,IDbProviderFactoryCreator dbProviderFactoryCreator) - : this(Constants.System.UmbracoConnectionName, globalSettings, connectionStrings, logger, mappers, dbProviderFactoryCreator) + : this(logger, globalSettings, connectionStrings, Constants.System.UmbracoConnectionName, mappers, dbProviderFactoryCreator) { } @@ -82,13 +80,12 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory(string connectionStringName, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, ILogger logger, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) + public UmbracoDatabaseFactory(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, string connectionStringName, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) { if (connectionStringName == null) throw new ArgumentNullException(nameof(connectionStringName)); if (string.IsNullOrWhiteSpace(connectionStringName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(connectionStringName)); _globalSettings = globalSettings; - _connectionStrings = connectionStrings; _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -118,7 +115,7 @@ namespace Umbraco.Core.Persistence /// Initializes a new instance of the . /// /// Used in tests. - public UmbracoDatabaseFactory(string connectionString, string providerName, ILogger logger, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) + public UmbracoDatabaseFactory(ILogger logger, string connectionString, string providerName, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator) { _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -142,7 +139,7 @@ namespace Umbraco.Core.Persistence { lock (_lock) { - return !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); + return !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace(); } } } @@ -151,13 +148,13 @@ namespace Umbraco.Core.Persistence public bool Initialized => Volatile.Read(ref _initialized); /// - public string ConnectionString => _connectionString; + public string ConnectionString { get; private set; } /// public bool CanConnect => // actually tries to connect to the database (regardless of configured/initialized) - !_connectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && - DbConnectionExtensions.IsConnectionAvailable(_connectionString, DbProviderFactory); + !ConnectionString.IsNullOrWhiteSpace() && !_providerName.IsNullOrWhiteSpace() && + DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); private void UpdateSqlServerDatabaseType() { @@ -169,7 +166,7 @@ namespace Umbraco.Core.Persistence if (setting.IsNullOrWhiteSpace() || !setting.StartsWith("SqlServer.") || !Enum.TryParse(setting.Substring("SqlServer.".Length), out var versionName, true)) { - versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(_connectionString, _providerName, _logger).ProductVersionName; + versionName = ((SqlServerSyntaxProvider) _sqlSyntax).GetSetVersion(ConnectionString, _providerName, _logger).ProductVersionName; } else { @@ -233,7 +230,7 @@ namespace Umbraco.Core.Persistence if (Volatile.Read(ref _initialized)) throw new InvalidOperationException("Already initialized."); - _connectionString = connectionString; + ConnectionString = connectionString; _providerName = providerName; } @@ -249,18 +246,19 @@ namespace Umbraco.Core.Persistence { _logger.Debug("Initializing."); - if (_connectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); + if (ConnectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name."); if (DbProviderFactory == null) throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); // cannot initialize without being able to talk to the database - if (!DbConnectionExtensions.IsConnectionAvailable(_connectionString, DbProviderFactory)) + // TODO: Why not? + if (!DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory)) throw new Exception("Cannot connect to the database."); - _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(_connectionString); - _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(_connectionString); + _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(ConnectionString); + _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(ConnectionString); _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, _providerName); @@ -313,7 +311,7 @@ namespace Umbraco.Core.Persistence // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() { - return new UmbracoDatabase(_connectionString, SqlContext, DbProviderFactory, _logger, _bulkSqlInsertProvider, _connectionRetryPolicy, _commandRetryPolicy); + return new UmbracoDatabase(ConnectionString, SqlContext, DbProviderFactory, _logger, _bulkSqlInsertProvider, _connectionRetryPolicy, _commandRetryPolicy); } protected override void DisposeResources() diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 6d73934387..99f38f0486 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -33,14 +33,12 @@ namespace Umbraco.Core.Runtime // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; - _dbFactory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, + _dbFactory = new UmbracoDatabaseFactory(_logger, globalSettings, connectionStrings, - _logger, + Constants.System.UmbracoConnectionName, new Lazy(() => new MapperCollection(Enumerable.Empty())), - dbProviderFactoryCreator - ); + dbProviderFactoryCreator); } public async Task AcquireLockAsync(int millisecondsTimeout) diff --git a/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs new file mode 100644 index 0000000000..fe1d604dd9 --- /dev/null +++ b/src/Umbraco.Tests.Integration/GlobalSetupTeardown.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using NUnit.Framework; +using Umbraco.Tests.Integration.Testing; + +// this class has NO NAMESPACE +// it applies to the whole assembly + +[SetUpFixture] +// ReSharper disable once CheckNamespace +public class TestsSetup +{ + private Stopwatch _stopwatch; + + [OneTimeSetUp] + public void SetUp() + { + _stopwatch = Stopwatch.StartNew(); + } + + [OneTimeTearDown] + public void TearDown() + { + LocalDbTestDatabase.KillLocalDb(); + Console.WriteLine("TOTAL TESTS DURATION: {0}", _stopwatch.Elapsed); + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..abb22c0bd7 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NUnit.Framework; +using Umbraco.Configuration.Models; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Tests.Integration.Testing; + +namespace Umbraco.Tests.Integration.Implementations +{ + public static class ApplicationBuilderExtensions + { + /// + /// Creates a LocalDb instance to use for the test + /// + /// + /// + /// + /// Default is true - meaning a brand new database is created for this test. If this is false it will try to + /// re-use an existing database that was already created as part of this test fixture. + /// // TODO Implement the 'false' behavior + /// + /// + /// Default is true - meaning a database schema will be created for this test if it's a new database. If this is false + /// it will just create an empty database. + /// + /// + public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, + string dbFilePath, + bool createNewDb = true, + bool initializeSchema = true) + { + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + + if (!Directory.Exists(dbFilePath)) + Directory.CreateDirectory(dbFilePath); + + var db = LocalDbTestDatabase.Get(dbFilePath, + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService()); + + if (initializeSchema) + { + // New DB + Schema + db.AttachSchema(); + + // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that + // the runtime state is configured correctly so we'll force update the configuration flag and re-run the + // runtime state checker. + // TODO: This wouldn't be required if we don't store the Umbraco version in config + + // right now we are an an 'Install' state + var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + // dynamically change the config status + var umbVersion = app.ApplicationServices.GetRequiredService(); + var config = app.ApplicationServices.GetRequiredService(); + config[GlobalSettings.Prefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); + + // re-run the runtime level check + var dbFactory = app.ApplicationServices.GetRequiredService(); + var profilingLogger = app.ApplicationServices.GetRequiredService(); + runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); + + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + } + else + { + db.AttachEmpty(); + } + + return app; + } + + } + + +} diff --git a/src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs deleted file mode 100644 index 60cac26349..0000000000 --- a/src/Umbraco.Tests.Integration/Implementations/HostBuilderExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Data.SqlClient; -using System.IO; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Umbraco.Core; -using Umbraco.Tests.Integration.Testing; - -namespace Umbraco.Tests.Integration.Implementations -{ - public static class HostBuilderExtensions - { - - public static IHostBuilder UseLocalDb(this IHostBuilder hostBuilder, string dbFilePath) - { - // Need to register SqlClient manually - // TODO: Move this to someplace central - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - hostBuilder.ConfigureAppConfiguration(x => - { - if (!Directory.Exists(dbFilePath)) - Directory.CreateDirectory(dbFilePath); - - var dbName = Guid.NewGuid().ToString("N"); - var instance = TestLocalDb.EnsureLocalDbInstanceAndDatabase(dbName, dbFilePath); - - x.AddInMemoryCollection(new[] - { - new KeyValuePair($"ConnectionStrings:{Constants.System.UmbracoConnectionName}", instance.GetConnectionString(dbName)) - }); - }); - return hostBuilder; - } - - - } - - -} diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 0e11a29b95..814687eb7f 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -11,9 +11,11 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Umbraco.Configuration.Models; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Composing.LightInject; +using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Core.Persistence; @@ -40,7 +42,7 @@ namespace Umbraco.Tests.Integration [OneTimeTearDown] public void FixtureTearDown() { - TestLocalDb.Cleanup(); + } /// @@ -174,8 +176,6 @@ namespace Umbraco.Tests.Integration var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() - //TODO: Need to have a configured umb version for the runtime state - .UseLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb")) .UseUmbraco(serviceProviderFactory) .ConfigureServices((hostContext, services) => { @@ -190,26 +190,13 @@ namespace Umbraco.Tests.Integration var host = await hostBuilder.StartAsync(); var app = new ApplicationBuilder(host.Services); + // This will create a db, install the schema and ensure the app is configured to run + app.UseTestLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb")); + app.UseUmbracoCore(); - - var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - var dbBuilder = app.ApplicationServices.GetRequiredService(); - Assert.IsNotNull(dbBuilder); - - var canConnect = dbBuilder.CanConnectToDatabase; - Assert.IsTrue(canConnect); - - var dbResult = dbBuilder.CreateSchemaAndData(); - Assert.IsTrue(dbResult.Success); - - // TODO: Get this to work ... but to do that we need to mock or pass in a current umbraco version - //var dbFactory = app.ApplicationServices.GetRequiredService(); - //var profilingLogger = app.ApplicationServices.GetRequiredService(); - //runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); - //Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + var runtimeState = app.ApplicationServices.GetRequiredService(); + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); } internal static LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs new file mode 100644 index 0000000000..63b336701c --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Concurrent; +using System.Configuration; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using System.Linq; +using System.Threading; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Integration.Testing +{ + /// + /// Manages a pool of LocalDb databases for integration testing + /// + internal class LocalDbTestDatabase + { + public static LocalDbTestDatabase Get(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) + { + var localDb = new LocalDb(); + if (localDb.IsAvailable == false) + throw new InvalidOperationException("LocalDB is not available."); + return new LocalDbTestDatabase(logger, globalSettings, localDb, filesPath, dbFactory); + } + + public const string InstanceName = "UmbracoTests"; + public const string DatabaseName = "UmbracoTests"; + + private readonly ILogger _logger; + private readonly IGlobalSettings _globalSettings; + private readonly LocalDb _localDb; + private readonly IUmbracoVersion _umbracoVersion; + private static LocalDb.Instance _instance; + private static string _filesPath; + private readonly IUmbracoDatabaseFactory _dbFactory; + private UmbracoDatabase.CommandInfo[] _dbCommands; + private string _currentCstr; + private static DatabasePool _emptyPool; + private static DatabasePool _schemaPool; + private DatabasePool _currentPool; + + public LocalDbTestDatabase(ILogger logger, IGlobalSettings globalSettings, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) + { + _umbracoVersion = new UmbracoVersion(); + _logger = logger; + _globalSettings = globalSettings; + _localDb = localDb; + _filesPath = filesPath; + _dbFactory = dbFactory; + + _instance = _localDb.GetInstance(InstanceName); + if (_instance != null) return; + + if (_localDb.CreateInstance(InstanceName) == false) + throw new Exception("Failed to create a LocalDb instance."); + _instance = _localDb.GetInstance(InstanceName); + } + + public string ConnectionString => _currentCstr ?? _instance.GetAttachedConnectionString("XXXXXX", _filesPath); + + private void Create() + { + var tempName = Guid.NewGuid().ToString("N"); + _instance.CreateDatabase(tempName, _filesPath); + _instance.DetachDatabase(tempName); + + // there's probably a sweet spot to be found for size / parallel... + + var s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.EmptyPoolSize"]; + var emptySize = s == null ? 2 : int.Parse(s); + s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.EmptyPoolThreadCount"]; + var emptyParallel = s == null ? 1 : int.Parse(s); + s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.SchemaPoolSize"]; + var schemaSize = s == null ? 2 : int.Parse(s); + s = ConfigurationManager.AppSettings["Umbraco.Tests.LocalDbTestDatabase.SchemaPoolThreadCount"]; + var schemaParallel = s == null ? 1 : int.Parse(s); + + _emptyPool = new DatabasePool(_localDb, _instance, DatabaseName + "-Empty", tempName, _filesPath, emptySize, emptyParallel); + _schemaPool = new DatabasePool(_localDb, _instance, DatabaseName + "-Schema", tempName, _filesPath, schemaSize, schemaParallel, delete: true, prepare: RebuildSchema); + } + + public void AttachEmpty() + { + if (_emptyPool == null) + Create(); + + _currentCstr = _emptyPool.AttachDatabase(); + _currentPool = _emptyPool; + } + + public void AttachSchema() + { + if (_schemaPool == null) + Create(); + + _currentCstr = _schemaPool.AttachDatabase(); + _currentPool = _schemaPool; + } + + public void Detach() + { + _currentPool.DetachDatabase(); + } + + private void RebuildSchema(DbConnection conn, IDbCommand cmd) + { + + if (_dbCommands != null) + { + foreach (var dbCommand in _dbCommands) + { + + if (dbCommand.Text.StartsWith("SELECT ")) continue; + + cmd.CommandText = dbCommand.Text; + cmd.Parameters.Clear(); + foreach (var parameterInfo in dbCommand.Parameters) + AddParameter(cmd, parameterInfo); + cmd.ExecuteNonQuery(); + } + } + else + { + _dbFactory.Configure(conn.ConnectionString, Umbraco.Core.Constants.DatabaseProviders.SqlServer); + + using var database = (UmbracoDatabase)_dbFactory.CreateDatabase(); + // track each db command ran as part of creating the database so we can replay these + database.LogCommands = true; + + using var trans = database.GetTransaction(); + + var creator = new DatabaseSchemaCreator(database, _logger, _umbracoVersion, _globalSettings); + creator.InitializeDatabaseSchema(); + + trans.Complete(); // commit it + + _dbCommands = database.Commands.ToArray(); + } + + } + + private static void AddParameter(IDbCommand cmd, UmbracoDatabase.ParameterInfo parameterInfo) + { + var p = cmd.CreateParameter(); + p.ParameterName = parameterInfo.Name; + p.Value = parameterInfo.Value; + p.DbType = parameterInfo.DbType; + p.Size = parameterInfo.Size; + cmd.Parameters.Add(p); + } + + public void Clear() + { + var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); + + foreach (var database in _instance.GetDatabases()) + { + if (database.StartsWith(filename)) + _instance.DropDatabase(database); + } + + foreach (var file in Directory.EnumerateFiles(_filesPath)) + { + if (file.EndsWith(".mdf") == false && file.EndsWith(".ldf") == false) continue; + File.Delete(file); + } + } + + private static void ResetLocalDb(IDbCommand cmd) + { + // https://stackoverflow.com/questions/536350 + + cmd.CommandType = CommandType.Text; + cmd.CommandText = @" + declare @n char(1); + set @n = char(10); + declare @stmt nvarchar(max); + -- check constraints + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.check_constraints; + -- foreign keys + select @stmt = isnull( @stmt + @n, '' ) + + 'alter table [' + schema_name(schema_id) + '].[' + object_name( parent_object_id ) + '] drop constraint [' + name + ']' + from sys.foreign_keys; + -- tables + select @stmt = isnull( @stmt + @n, '' ) + + 'drop table [' + schema_name(schema_id) + '].[' + name + ']' + from sys.tables; + exec sp_executesql @stmt; + "; + cmd.ExecuteNonQuery(); + } + + public static void KillLocalDb() + { + _emptyPool?.Stop(); + _schemaPool?.Stop(); + + if (_filesPath == null) + return; + + var filename = Path.Combine(_filesPath, DatabaseName).ToUpper(); + + foreach (var database in _instance.GetDatabases()) + { + if (database.StartsWith(filename)) + _instance.DropDatabase(database); + } + + foreach (var file in Directory.EnumerateFiles(_filesPath)) + { + if (file.EndsWith(".mdf") == false && file.EndsWith(".ldf") == false) continue; + try + { + File.Delete(file); + } + catch (IOException) + { + // ignore, must still be in use but nothing we can do + } + } + } + + private class DatabasePool + { + private readonly LocalDb _localDb; + private readonly LocalDb.Instance _instance; + private readonly string _filesPath; + private readonly string _name; + private readonly int _size; + private readonly string[] _cstrs; + private readonly BlockingCollection _prepareQueue, _readyQueue; + private readonly Action _prepare; + private int _current; + + public DatabasePool(LocalDb localDb, LocalDb.Instance instance, string name, string tempName, string filesPath, int size, int parallel = 1, Action prepare = null, bool delete = false) + { + _localDb = localDb; + _instance = instance; + _filesPath = filesPath; + _name = name; + _size = size; + _prepare = prepare; + _prepareQueue = new BlockingCollection(); + _readyQueue = new BlockingCollection(); + _cstrs = new string[_size]; + + for (var i = 0; i < size; i++) + localDb.CopyDatabaseFiles(tempName, filesPath, targetDatabaseName: name + "-" + i, overwrite: true, delete: delete && i == size - 1); + + if (prepare == null) + { + for (var i = 0; i < size; i++) + _readyQueue.Add(i); + } + else + { + for (var i = 0; i < size; i++) + _prepareQueue.Add(i); + } + + for (var i = 0; i < parallel; i++) + { + var thread = new Thread(PrepareThread); + thread.Start(); + } + } + + public string AttachDatabase() + { + try + { + _current = _readyQueue.Take(); + } + catch (InvalidOperationException) + { + _current = 0; + return null; + } + return ConnectionString(_current); + } + + public void DetachDatabase() + { + _prepareQueue.Add(_current); + } + + private string ConnectionString(int i) + { + return _cstrs[i] ?? (_cstrs[i] = _instance.GetAttachedConnectionString(_name + "-" + i, _filesPath)); + } + + private void PrepareThread() + { + while (_prepareQueue.IsCompleted == false) + { + int i; + try + { + i = _prepareQueue.Take(); + } + catch (InvalidOperationException) + { + continue; + } + using (var conn = new SqlConnection(ConnectionString(i))) + using (var cmd = conn.CreateCommand()) + { + conn.Open(); + ResetLocalDb(cmd); + _prepare?.Invoke(conn, cmd); + } + _readyQueue.Add(i); + } + } + + public void Stop() + { + int i; + _prepareQueue.CompleteAdding(); + while (_prepareQueue.TryTake(out i)) { } + _readyQueue.CompleteAdding(); + while (_readyQueue.TryTake(out i)) { } + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs b/src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs deleted file mode 100644 index 8ee326783b..0000000000 --- a/src/Umbraco.Tests.Integration/Testing/TestLocalDb.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Umbraco.Core.Persistence; - -namespace Umbraco.Tests.Integration.Testing -{ - public static class TestLocalDb - { - private const string LocalDbInstanceName = "UmbTests"; - - private static LocalDb LocalDb { get; } = new LocalDb(); - - // TODO: We need to borrow logic from this old branch, this is the latest commit at the old branch where we had LocalDb - // working for tests. There's a lot of hoops to jump through to make it work 'fast'. Turns out it didn't actually run as - // fast as SqlCe due to the dropping/creating of DB instances since that is faster in SqlCe but this code was all heavily - // optimized to go as fast as possible. - // see https://github.com/umbraco/Umbraco-CMS/blob/3a8716ac7b1c48b51258724337086cd0712625a1/src/Umbraco.Tests/TestHelpers/LocalDbTestDatabase.cs - internal static LocalDb.Instance EnsureLocalDbInstanceAndDatabase(string dbName, string dbFilePath) - { - if (!LocalDb.InstanceExists(LocalDbInstanceName) && !LocalDb.CreateInstance(LocalDbInstanceName)) - { - throw new InvalidOperationException( - $"Failed to create LocalDb instance {LocalDbInstanceName}, assuming LocalDb is not really available."); - } - - var instance = LocalDb.GetInstance(LocalDbInstanceName); - - if (instance == null) - { - throw new InvalidOperationException( - $"Failed to get LocalDb instance {LocalDbInstanceName}, assuming LocalDb is not really available."); - } - - instance.CreateDatabase(dbName, dbFilePath); - - return instance; - } - - public static void Cleanup() - { - var instance = LocalDb.GetInstance(LocalDbInstanceName); - if (instance != null) - { - instance.DropDatabases(); - } - } - } -} diff --git a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs index 52f2365cbc..d964ab972b 100644 --- a/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs +++ b/src/Umbraco.Tests/Persistence/DatabaseContextTests.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Persistence } // re-create the database factory and database context with proper connection string - _databaseFactory = new UmbracoDatabaseFactory(connString, Constants.DbProviderNames.SqlCe, _logger, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); + _databaseFactory = new UmbracoDatabaseFactory(_logger, connString, Constants.DbProviderNames.SqlCe, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); // test get database type (requires an actual database) using (var database = _databaseFactory.CreateDatabase()) diff --git a/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs b/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs index 4a9e8e2b26..bab0617ec6 100644 --- a/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs +++ b/src/Umbraco.Tests/Persistence/FaultHandling/ConnectionRetryTest.cs @@ -20,7 +20,7 @@ namespace Umbraco.Tests.Persistence.FaultHandling { const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=x;password=umbraco"; const string providerName = Constants.DbProviderNames.SqlServer; - var factory = new UmbracoDatabaseFactory(connectionString, providerName, Mock.Of(), new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); + var factory = new UmbracoDatabaseFactory(Mock.Of(), connectionString, providerName, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); using (var database = factory.CreateDatabase()) { @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Persistence.FaultHandling { const string connectionString = @"server=.\SQLEXPRESS;database=EmptyForTest;user id=umbraco;password=umbraco"; const string providerName = Constants.DbProviderNames.SqlServer; - var factory = new UmbracoDatabaseFactory(connectionString, providerName, Mock.Of(), new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); + var factory = new UmbracoDatabaseFactory(Mock.Of(), connectionString, providerName, new Lazy(() => Mock.Of()), TestHelper.DbProviderFactoryCreator); using (var database = factory.CreateDatabase()) { diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index 33e477df2c..fa037fbb50 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -244,11 +244,10 @@ namespace Umbraco.Tests.TestHelpers // mappersBuilder.AddCore(); // var mappers = mappersBuilder.CreateCollection(); var mappers = Current.Factory.GetInstance(); - databaseFactory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, + databaseFactory = new UmbracoDatabaseFactory(logger, SettingsForTests.GetDefaultGlobalSettings(), new ConnectionStrings(), - logger, + Constants.System.UmbracoConnectionName, new Lazy(() => mappers), TestHelper.DbProviderFactoryCreator); } diff --git a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs index fbfada118a..b4db4f551c 100644 --- a/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs +++ b/src/Umbraco.Tests/TestHelpers/TestWithDatabaseBase.cs @@ -89,7 +89,7 @@ namespace Umbraco.Tests.TestHelpers return TestObjects.GetDatabaseFactoryMock(); var lazyMappers = new Lazy(f.GetInstance); - var factory = new UmbracoDatabaseFactory(GetDbConnectionString(), GetDbProviderName(), f.GetInstance(), lazyMappers, TestHelper.DbProviderFactoryCreator); + var factory = new UmbracoDatabaseFactory(f.GetInstance(), GetDbConnectionString(), GetDbProviderName(), lazyMappers, TestHelper.DbProviderFactoryCreator); factory.ResetForTests(); return factory; }); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 0345aab2da..c3505a23fa 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -462,13 +462,13 @@ namespace Umbraco.Tests.Testing var globalSettings = TestHelper.GetConfigs().Global(); var connectionStrings = TestHelper.GetConfigs().ConnectionStrings(); - Composition.RegisterUnique(f => new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, + Composition.RegisterUnique(f => new UmbracoDatabaseFactory(Logger, globalSettings, connectionStrings, - Logger, + Constants.System.UmbracoConnectionName, new Lazy(f.GetInstance), TestHelper.DbProviderFactoryCreator)); + Composition.RegisterUnique(f => f.TryGetInstance().SqlContext); Composition.WithCollectionBuilder(); // empty From 3757f722a0895390fe163052fe3e82f96ae42549 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2020 20:55:13 +1100 Subject: [PATCH 29/34] Gets first integration test moved, moves the test options to the common project, ensures that the LocalDb pool is updated on each test --- .../Testing/TestOptionAttributeBase.cs | 6 +- .../Testing/UmbracoTestAttribute.cs | 0 .../Testing/UmbracoTestOptions.cs | 61 +++++++ .../ContainerTests.cs | 3 +- .../ApplicationBuilderExtensions.cs | 117 ++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 23 +++ .../ApplicationBuilderExtensions.cs | 91 ----------- .../Repositories/AuditRepositoryTest.cs | 54 +++---- src/Umbraco.Tests.Integration/RuntimeTests.cs | 78 ++------- .../Testing/LocalDbTestDatabase.cs | 14 +- .../Testing/UmbracoIntegrationTest.cs | 150 ++++++++++++++++++ src/Umbraco.Tests/TestHelpers/TestObjects.cs | 4 +- .../Testing/UmbracoTestOptions.cs | 39 ----- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 - 14 files changed, 401 insertions(+), 243 deletions(-) rename src/{Umbraco.Tests => Umbraco.Tests.Common}/Testing/TestOptionAttributeBase.cs (88%) rename src/{Umbraco.Tests => Umbraco.Tests.Common}/Testing/UmbracoTestAttribute.cs (100%) create mode 100644 src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs create mode 100644 src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs create mode 100644 src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs delete mode 100644 src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs rename src/{Umbraco.Tests => Umbraco.Tests.Integration}/Persistence/Repositories/AuditRepositoryTest.cs (72%) create mode 100644 src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs delete mode 100644 src/Umbraco.Tests/Testing/UmbracoTestOptions.cs diff --git a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs b/src/Umbraco.Tests.Common/Testing/TestOptionAttributeBase.cs similarity index 88% rename from src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs rename to src/Umbraco.Tests.Common/Testing/TestOptionAttributeBase.cs index 2fb4660481..bbfa68778f 100644 --- a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs +++ b/src/Umbraco.Tests.Common/Testing/TestOptionAttributeBase.cs @@ -31,10 +31,14 @@ namespace Umbraco.Tests.Testing var test = TestContext.CurrentContext.Test; var typeName = test.ClassName; var methodName = test.MethodName; + // this will only get types from whatever is already loaded in the app domain var type = Type.GetType(typeName, false); if (type == null) { - type = ScanAssemblies + // automatically add the executing and calling assemblies to the list to scan for this type + var scanAssemblies = ScanAssemblies.Union(new[] {Assembly.GetExecutingAssembly(), Assembly.GetCallingAssembly()}).ToList(); + + type = scanAssemblies .Select(assembly => assembly.GetType(typeName, false)) .FirstOrDefault(x => x != null); if (type == null) diff --git a/src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs b/src/Umbraco.Tests.Common/Testing/UmbracoTestAttribute.cs similarity index 100% rename from src/Umbraco.Tests/Testing/UmbracoTestAttribute.cs rename to src/Umbraco.Tests.Common/Testing/UmbracoTestAttribute.cs diff --git a/src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs b/src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs new file mode 100644 index 0000000000..eef6e45d7e --- /dev/null +++ b/src/Umbraco.Tests.Common/Testing/UmbracoTestOptions.cs @@ -0,0 +1,61 @@ +namespace Umbraco.Tests.Testing +{ + public static class UmbracoTestOptions + { + public enum Logger + { + /// + /// pure mocks + /// + Mock, + /// + /// Serilog for tests + /// + Serilog, + /// + /// console logger + /// + Console + } + + public enum Database + { + /// + /// no database + /// + None, + /// + /// new empty database file for the entire fixture + /// + NewEmptyPerFixture, + /// + /// new empty database file per test + /// + NewEmptyPerTest, + /// + /// new database file with schema for the entire fixture + /// + NewSchemaPerFixture, + /// + /// new database file with schema per test + /// + NewSchemaPerTest + } + + public enum TypeLoader + { + /// + /// the default, global type loader for tests + /// + Default, + /// + /// create one type loader for the feature + /// + PerFixture, + /// + /// create one type loader for each test + /// + PerTest + } + } +} diff --git a/src/Umbraco.Tests.Integration/ContainerTests.cs b/src/Umbraco.Tests.Integration/ContainerTests.cs index 945eeda2f0..34a32a38a8 100644 --- a/src/Umbraco.Tests.Integration/ContainerTests.cs +++ b/src/Umbraco.Tests.Integration/ContainerTests.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Persistence; using Umbraco.Tests.Common; using Umbraco.Tests.Integration.Implementations; +using Umbraco.Tests.Integration.Testing; namespace Umbraco.Tests.Integration { @@ -82,7 +83,7 @@ namespace Umbraco.Tests.Integration // it means the container won't be disposed, and maybe other services? not sure. // In cases where we use it can we use IConfigureOptions? https://andrewlock.net/access-services-inside-options-and-startup-using-configureoptions/ - var umbracoContainer = RuntimeTests.GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); IHostApplicationLifetime lifetime1 = null; diff --git a/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..553aa4fe85 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,117 @@ +using System; +using System.Data.Common; +using System.Data.SqlClient; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Configuration.Models; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Tests.Integration.Testing; +using Umbraco.Tests.Testing; + +namespace Umbraco.Tests.Integration.Extensions +{ + public static class ApplicationBuilderExtensions + { + /// + /// Creates a LocalDb instance to use for the test + /// + /// + /// + /// + /// + public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, + string dbFilePath, + UmbracoIntegrationTest integrationTest) + { + // get the currently set db options + var testOptions = TestOptionAttributeBase.GetTestOptions(); + + if (testOptions.Database == UmbracoTestOptions.Database.None) + return app; + + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + + if (!Directory.Exists(dbFilePath)) + Directory.CreateDirectory(dbFilePath); + + var db = UmbracoIntegrationTest.GetOrCreate(dbFilePath, + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService(), + app.ApplicationServices.GetRequiredService()); + + switch (testOptions.Database) + { + case UmbracoTestOptions.Database.NewSchemaPerTest: + + // Add teardown callback + integrationTest.OnTestTearDown(() => db.Detach()); + + // New DB + Schema + db.AttachSchema(); + + // We must re-configure our current factory since attaching a new LocalDb from the pool changes connection strings + var dbFactory = app.ApplicationServices.GetRequiredService(); + if (!dbFactory.Configured) + { + dbFactory.Configure(db.ConnectionString, Umbraco.Core.Constants.DatabaseProviders.SqlServer); + } + + // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that + // the runtime state is configured correctly so we'll force update the configuration flag and re-run the + // runtime state checker. + // TODO: This wouldn't be required if we don't store the Umbraco version in config + + // right now we are an an 'Install' state + var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); + Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); + + // dynamically change the config status + var umbVersion = app.ApplicationServices.GetRequiredService(); + var config = app.ApplicationServices.GetRequiredService(); + config[GlobalSettings.Prefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); + + // re-run the runtime level check + var profilingLogger = app.ApplicationServices.GetRequiredService(); + runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); + + Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); + + break; + case UmbracoTestOptions.Database.NewEmptyPerTest: + + // Add teardown callback + integrationTest.OnTestTearDown(() => db.Detach()); + + db.AttachEmpty(); + + break; + case UmbracoTestOptions.Database.NewSchemaPerFixture: + + // Add teardown callback + integrationTest.OnFixtureTearDown(() => db.Detach()); + + break; + case UmbracoTestOptions.Database.NewEmptyPerFixture: + + // Add teardown callback + integrationTest.OnFixtureTearDown(() => db.Detach()); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(testOptions), testOptions, null); + } + + return app; + } + + } + + +} diff --git a/src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..9dde20036b --- /dev/null +++ b/src/Umbraco.Tests.Integration/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Tests.Integration.Implementations; + +namespace Umbraco.Tests.Integration.Extensions +{ + public static class ServiceCollectionExtensions + { + /// + /// These services need to be manually added because they do not get added by the generic host + /// + /// + /// + /// + public static void AddRequiredNetCoreServices(this IServiceCollection services, TestHelper testHelper, IWebHostEnvironment webHostEnvironment) + { + services.AddSingleton(x => testHelper.GetHttpContextAccessor()); + // the generic host does add IHostEnvironment but not this one because we are not actually in a web context + services.AddSingleton(x => webHostEnvironment); + } + } +} diff --git a/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs b/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs deleted file mode 100644 index abb22c0bd7..0000000000 --- a/src/Umbraco.Tests.Integration/Implementations/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Data.SqlClient; -using System.IO; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using NUnit.Framework; -using Umbraco.Configuration.Models; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Tests.Integration.Testing; - -namespace Umbraco.Tests.Integration.Implementations -{ - public static class ApplicationBuilderExtensions - { - /// - /// Creates a LocalDb instance to use for the test - /// - /// - /// - /// - /// Default is true - meaning a brand new database is created for this test. If this is false it will try to - /// re-use an existing database that was already created as part of this test fixture. - /// // TODO Implement the 'false' behavior - /// - /// - /// Default is true - meaning a database schema will be created for this test if it's a new database. If this is false - /// it will just create an empty database. - /// - /// - public static IApplicationBuilder UseTestLocalDb(this IApplicationBuilder app, - string dbFilePath, - bool createNewDb = true, - bool initializeSchema = true) - { - // need to manually register this factory - DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); - - if (!Directory.Exists(dbFilePath)) - Directory.CreateDirectory(dbFilePath); - - var db = LocalDbTestDatabase.Get(dbFilePath, - app.ApplicationServices.GetRequiredService(), - app.ApplicationServices.GetRequiredService(), - app.ApplicationServices.GetRequiredService()); - - if (initializeSchema) - { - // New DB + Schema - db.AttachSchema(); - - // In the case that we've initialized the schema, it means that we are installed so we'll want to ensure that - // the runtime state is configured correctly so we'll force update the configuration flag and re-run the - // runtime state checker. - // TODO: This wouldn't be required if we don't store the Umbraco version in config - - // right now we are an an 'Install' state - var runtimeState = (RuntimeState)app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Install, runtimeState.Level); - - // dynamically change the config status - var umbVersion = app.ApplicationServices.GetRequiredService(); - var config = app.ApplicationServices.GetRequiredService(); - config[GlobalSettings.Prefix + "ConfigurationStatus"] = umbVersion.SemanticVersion.ToString(); - - // re-run the runtime level check - var dbFactory = app.ApplicationServices.GetRequiredService(); - var profilingLogger = app.ApplicationServices.GetRequiredService(); - runtimeState.DetermineRuntimeLevel(dbFactory, profilingLogger); - - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - - } - else - { - db.AttachEmpty(); - } - - return app; - } - - } - - -} diff --git a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/AuditRepositoryTest.cs similarity index 72% rename from src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs rename to src/Umbraco.Tests.Integration/Persistence/Repositories/AuditRepositoryTest.cs index 49e48e0a2f..e92653fb98 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/AuditRepositoryTest.cs +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/AuditRepositoryTest.cs @@ -1,29 +1,29 @@ using System.Linq; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; -using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; -using Umbraco.Core; -namespace Umbraco.Tests.Persistence.Repositories +namespace Umbraco.Tests.Integration.Persistence.Repositories { [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)] - public class AuditRepositoryTest : TestWithDatabaseBase + public class AuditRepositoryTest : UmbracoIntegrationTest { [Test] public void Can_Add_Audit_Entry() { - var sp = TestObjects.GetScopeProvider(Logger); - using (var scope = sp.CreateScope()) + var sp = ScopeProvider; + using (var scope = ScopeProvider.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); - repo.Save(new AuditItem(-1, AuditType.System, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "This is a System audit trail")); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); + repo.Save(new AuditItem(-1, AuditType.System, -1, UmbracoObjectTypes.Document.GetName(), "This is a System audit trail")); var dtos = scope.Database.Fetch("WHERE id > -1"); @@ -35,15 +35,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} published")); } scope.Complete(); @@ -51,7 +51,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); var page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 10, out var total, Direction.Descending, null, null); @@ -63,15 +63,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items_By_User_Id_With_Query_And_Filter() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} published")); } scope.Complete(); @@ -106,15 +106,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items_With_AuditType_Filter() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), $"Content {i} published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), $"Content {i} published")); } scope.Complete(); @@ -122,10 +122,10 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); var page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 9, out var total, Direction.Descending, - new[] {AuditType.Publish}, null) + new[] { AuditType.Publish }, null) .ToArray(); Assert.AreEqual(9, page.Length); @@ -137,15 +137,15 @@ namespace Umbraco.Tests.Persistence.Repositories [Test] public void Get_Paged_Items_With_Custom_Filter() { - var sp = TestObjects.GetScopeProvider(Logger); + var sp = ScopeProvider; using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); for (var i = 0; i < 100; i++) { - repo.Save(new AuditItem(i, AuditType.New, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "Content created")); - repo.Save(new AuditItem(i, AuditType.Publish, -1, ObjectTypes.GetName(UmbracoObjectTypes.Document), "Content published")); + repo.Save(new AuditItem(i, AuditType.New, -1, UmbracoObjectTypes.Document.GetName(), "Content created")); + repo.Save(new AuditItem(i, AuditType.Publish, -1, UmbracoObjectTypes.Document.GetName(), "Content published")); } scope.Complete(); @@ -153,7 +153,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var scope = sp.CreateScope()) { - var repo = new AuditRepository((IScopeAccessor) sp, Logger); + var repo = new AuditRepository((IScopeAccessor)sp, Logger); var page = repo.GetPagedResultsByQuery(sp.SqlContext.Query(), 0, 8, out var total, Direction.Descending, null, sp.SqlContext.Query().Where(item => item.Comment == "Content created")) diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 814687eb7f..210f767de1 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -1,30 +1,21 @@ -using LightInject; -using LightInject.Microsoft.DependencyInjection; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Umbraco.Configuration.Models; using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Composing.LightInject; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Migrations.Install; -using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; using Umbraco.Tests.Common; +using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.Integration.Testing; using Umbraco.Web.BackOffice.AspNetCore; -using static Umbraco.Core.Migrations.Install.DatabaseBuilder; namespace Umbraco.Tests.Integration { @@ -39,10 +30,11 @@ namespace Umbraco.Tests.Integration MyComposer.Reset(); } - [OneTimeTearDown] - public void FixtureTearDown() + [SetUp] + public void Setup() { - + MyComponent.Reset(); + MyComposer.Reset(); } /// @@ -95,7 +87,7 @@ namespace Umbraco.Tests.Integration [Test] public async Task AddUmbracoCore() { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() @@ -103,7 +95,7 @@ namespace Umbraco.Tests.Integration .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); - AddRequiredNetCoreServices(services, testHelper, webHostEnvironment); + services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); @@ -134,7 +126,7 @@ namespace Umbraco.Tests.Integration [Test] public async Task UseUmbracoCore() { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); + var umbracoContainer = UmbracoIntegrationTest.GetUmbracoContainer(out var serviceProviderFactory); var testHelper = new TestHelper(); var hostBuilder = new HostBuilder() @@ -142,7 +134,7 @@ namespace Umbraco.Tests.Integration .ConfigureServices((hostContext, services) => { var webHostEnvironment = testHelper.GetWebHostEnvironment(); - AddRequiredNetCoreServices(services, testHelper, webHostEnvironment); + services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); // Add it! services.AddUmbracoConfiguration(hostContext.Configuration); @@ -169,56 +161,6 @@ namespace Umbraco.Tests.Integration Assert.IsTrue(MyComponent.IsTerminated); } - [Test] - public async Task Install_Database() - { - var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); - var testHelper = new TestHelper(); - - var hostBuilder = new HostBuilder() - .UseUmbraco(serviceProviderFactory) - .ConfigureServices((hostContext, services) => - { - var webHostEnvironment = testHelper.GetWebHostEnvironment(); - AddRequiredNetCoreServices(services, testHelper, webHostEnvironment); - - // Add it! - services.AddUmbracoConfiguration(hostContext.Configuration); - services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly); - }); - - var host = await hostBuilder.StartAsync(); - var app = new ApplicationBuilder(host.Services); - - // This will create a db, install the schema and ensure the app is configured to run - app.UseTestLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb")); - - app.UseUmbracoCore(); - - var runtimeState = app.ApplicationServices.GetRequiredService(); - Assert.AreEqual(RuntimeLevel.Run, runtimeState.Level); - } - - internal static LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) - { - var container = UmbracoServiceProviderFactory.CreateServiceContainer(); - serviceProviderFactory = new UmbracoServiceProviderFactory(container); - var umbracoContainer = serviceProviderFactory.GetContainer(); - return umbracoContainer; - } - - /// - /// These services need to be manually added because they do not get added by the generic host - /// - /// - /// - /// - private void AddRequiredNetCoreServices(IServiceCollection services, TestHelper testHelper, IWebHostEnvironment webHostEnvironment) - { - services.AddSingleton(x => testHelper.GetHttpContextAccessor()); - // the generic host does add IHostEnvironment but not this one because we are not actually in a web context - services.AddSingleton(x => webHostEnvironment); - } [RuntimeLevel(MinLevel = RuntimeLevel.Install)] public class MyComposer : IUserComposer diff --git a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs index 63b336701c..da2b83bc39 100644 --- a/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs +++ b/src/Umbraco.Tests.Integration/Testing/LocalDbTestDatabase.cs @@ -17,16 +17,8 @@ namespace Umbraco.Tests.Integration.Testing /// /// Manages a pool of LocalDb databases for integration testing /// - internal class LocalDbTestDatabase + public class LocalDbTestDatabase { - public static LocalDbTestDatabase Get(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) - { - var localDb = new LocalDb(); - if (localDb.IsAvailable == false) - throw new InvalidOperationException("LocalDB is not available."); - return new LocalDbTestDatabase(logger, globalSettings, localDb, filesPath, dbFactory); - } - public const string InstanceName = "UmbracoTests"; public const string DatabaseName = "UmbracoTests"; @@ -43,7 +35,8 @@ namespace Umbraco.Tests.Integration.Testing private static DatabasePool _schemaPool; private DatabasePool _currentPool; - public LocalDbTestDatabase(ILogger logger, IGlobalSettings globalSettings, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) + //It's internal because `Umbraco.Core.Persistence.LocalDb` is internal + internal LocalDbTestDatabase(ILogger logger, IGlobalSettings globalSettings, LocalDb localDb, string filesPath, IUmbracoDatabaseFactory dbFactory) { _umbracoVersion = new UmbracoVersion(); _logger = logger; @@ -328,5 +321,6 @@ namespace Umbraco.Tests.Integration.Testing while (_readyQueue.TryTake(out i)) { } } } + } } diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs new file mode 100644 index 0000000000..90a6e7bdf8 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NUnit.Framework; +using Umbraco.Core.Composing; +using Umbraco.Core.Composing.LightInject; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Scoping; +using Umbraco.Tests.Integration.Extensions; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Tests.Testing; +using Umbraco.Web.BackOffice.AspNetCore; + +namespace Umbraco.Tests.Integration.Testing +{ + /// + /// Abstract class for integration tests + /// + /// + /// This will use a Host Builder to boot and install Umbraco ready for use + /// + [SingleThreaded] + [NonParallelizable] + public abstract class UmbracoIntegrationTest + { + public static LightInjectContainer GetUmbracoContainer(out UmbracoServiceProviderFactory serviceProviderFactory) + { + var container = UmbracoServiceProviderFactory.CreateServiceContainer(); + serviceProviderFactory = new UmbracoServiceProviderFactory(container); + var umbracoContainer = serviceProviderFactory.GetContainer(); + return umbracoContainer; + } + + /// + /// Get or create an instance of + /// + /// + /// + /// + /// + /// + /// + /// There must only be ONE instance shared between all tests in a session + /// + public static LocalDbTestDatabase GetOrCreate(string filesPath, ILogger logger, IGlobalSettings globalSettings, IUmbracoDatabaseFactory dbFactory) + { + lock (_dbLocker) + { + if (_dbInstance != null) return _dbInstance; + + var localDb = new LocalDb(); + if (localDb.IsAvailable == false) + throw new InvalidOperationException("LocalDB is not available."); + _dbInstance = new LocalDbTestDatabase(logger, globalSettings, localDb, filesPath, dbFactory); + return _dbInstance; + } + } + + private static readonly object _dbLocker = new object(); + private static LocalDbTestDatabase _dbInstance; + + private readonly List _testTeardown = new List(); + private readonly List _fixtureTeardown = new List(); + + public void OnTestTearDown(Action tearDown) + { + _testTeardown.Add(tearDown); + } + + public void OnFixtureTearDown(Action tearDown) + { + _fixtureTeardown.Add(tearDown); + } + + [OneTimeTearDown] + public void FixtureTearDown() + { + // call all registered callbacks + foreach (var action in _fixtureTeardown) + { + action(); + } + } + + [TearDown] + public void TearDown() + { + // call all registered callbacks + foreach (var action in _testTeardown) + { + action(); + } + } + + [SetUp] + public async Task Setup() + { + var umbracoContainer = GetUmbracoContainer(out var serviceProviderFactory); + var testHelper = new TestHelper(); + + var hostBuilder = new HostBuilder() + .UseUmbraco(serviceProviderFactory) + .ConfigureServices((hostContext, services) => + { + var webHostEnvironment = testHelper.GetWebHostEnvironment(); + services.AddRequiredNetCoreServices(testHelper, webHostEnvironment); + + // Add it! + services.AddUmbracoConfiguration(hostContext.Configuration); + services.AddUmbracoCore(webHostEnvironment, umbracoContainer, GetType().Assembly); + }); + + var host = await hostBuilder.StartAsync(); + var app = new ApplicationBuilder(host.Services); + Services = app.ApplicationServices; + + // This will create a db, install the schema and ensure the app is configured to run + app.UseTestLocalDb(Path.Combine(testHelper.CurrentAssemblyDirectory, "LocalDb"), this); + + app.UseUmbracoCore(); + } + + /// + /// Returns the DI container + /// + protected IServiceProvider Services { get; private set; } + + /// + /// Returns the + /// + protected IScopeProvider ScopeProvider => Services.GetRequiredService(); + + /// + /// Returns the + /// + protected IScopeAccessor ScopeAccessor => Services.GetRequiredService(); + + /// + /// Returns the + /// + protected ILogger Logger => Services.GetRequiredService(); + } +} diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects.cs b/src/Umbraco.Tests/TestHelpers/TestObjects.cs index fa037fbb50..4609d8a59c 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects.cs @@ -252,8 +252,8 @@ namespace Umbraco.Tests.TestHelpers TestHelper.DbProviderFactoryCreator); } - typeFinder = typeFinder ?? new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(GetType().Assembly)); - fileSystems = fileSystems ?? new FileSystems(Current.Factory, logger, TestHelper.IOHelper, SettingsForTests.GenerateMockGlobalSettings()); + typeFinder ??= new TypeFinder(logger, new DefaultUmbracoAssemblyProvider(GetType().Assembly)); + fileSystems ??= new FileSystems(Current.Factory, logger, TestHelper.IOHelper, SettingsForTests.GenerateMockGlobalSettings()); var coreDebug = TestHelper.CoreDebugSettings; var mediaFileSystem = Mock.Of(); var scopeProvider = new ScopeProvider(databaseFactory, fileSystems, coreDebug, mediaFileSystem, logger, typeFinder, NoAppCache.Instance); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs b/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs deleted file mode 100644 index da3ffccc55..0000000000 --- a/src/Umbraco.Tests/Testing/UmbracoTestOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Tests.Testing -{ - public static class UmbracoTestOptions - { - public enum Logger - { - // pure mocks - Mock, - // Serilog for tests - Serilog, - // console logger - Console - } - - public enum Database - { - // no database - None, - // new empty database file for the entire feature - NewEmptyPerFixture, - // new empty database file per test - NewEmptyPerTest, - // new database file with schema for the entire feature - NewSchemaPerFixture, - // new database file with schema per test - NewSchemaPerTest - } - - public enum TypeLoader - { - // the default, global type loader for tests - Default, - // create one type loader for the feature - PerFixture, - // create one type loader for each test - PerTest - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 06f6a98573..f667b11aa3 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -231,11 +231,8 @@ - - - @@ -267,7 +264,6 @@ - From 7617027c499518778ae5040e1737e5dae2ab77e5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2020 21:53:30 +1100 Subject: [PATCH 30/34] Moves most of the UserRepositoryTest --- .../Implement/UserGroupRepository.cs | 2 +- .../Builders/UserBuilder.cs | 106 +++++ .../Builders/UserGroupBuilder.cs | 65 +++ .../Repositories/UserRepositoryTest.cs | 393 ++++++++++++++++++ .../Testing/UmbracoIntegrationTest.cs | 23 + .../Repositories/UserRepositoryTest.cs | 343 +-------------- .../TestHelpers/TestObjects-Mocks.cs | 5 - src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- 8 files changed, 591 insertions(+), 348 deletions(-) create mode 100644 src/Umbraco.Tests.Common/Builders/UserBuilder.cs create mode 100644 src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs create mode 100644 src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 07cfbb05a8..863f3dc455 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -319,7 +319,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private void PersistAllowedSections(IUserGroup entity) { - var userGroup = (UserGroup) entity; + var userGroup = entity; // First delete all Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); diff --git a/src/Umbraco.Tests.Common/Builders/UserBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs new file mode 100644 index 0000000000..07ab8ef3f7 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/UserBuilder.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq; +using Moq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class UserBuilder + : BuilderBase, + IWithIdBuilder + { + private int? _id; + private string _language; + private bool? _approved; + private string _name; + private string _rawPassword; + private bool? _isLockedOut; + private string _email; + private string _username; + private string _defaultLang; + private string _suffix = string.Empty; + + public UserBuilder WithDefaultUILanguage(string defaultLang) + { + _defaultLang = defaultLang; + return this; + } + + public UserBuilder WithLanguage(string language) + { + _language = language; + return this; + } + + public UserBuilder WithApproved(bool approved) + { + _approved = approved; + return this; + } + + public UserBuilder WithRawPassword(string rawPassword) + { + _rawPassword = rawPassword; + return this; + } + + public UserBuilder WithEmail(string email) + { + _email = email; + return this; + } + + public UserBuilder WithUsername(string username) + { + _username = username; + return this; + } + + public UserBuilder WithLockedOut(bool isLockedOut) + { + _isLockedOut = isLockedOut; + return this; + } + + public UserBuilder WithName(string name) + { + _name = name; + return this; + } + + /// + /// Will suffix the name, email and username for testing + /// + /// + /// + public UserBuilder WithSuffix(string suffix) + { + _suffix = suffix; + return this; + } + + public override User Build() + { + var globalSettings = Mock.Of(x => x.DefaultUILanguage == (_defaultLang ?? "en-US")); + return new User(globalSettings, + _name ?? "TestUser" + _suffix, + _email ?? "test" + _suffix + "@test.com", + _username ?? "TestUser" + _suffix, + _rawPassword ?? "abcdefghijklmnopqrstuvwxyz") + { + Language = _language ?? _defaultLang ?? "en-US", + IsLockedOut = _isLockedOut ?? false, + IsApproved = _approved ?? true + }; + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs new file mode 100644 index 0000000000..d3ce5e71a8 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/UserGroupBuilder.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Moq; +using Umbraco.Core.Models.Membership; +using Umbraco.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Tests.Common.Builders +{ + public class UserGroupBuilder + : BuilderBase, + IWithIdBuilder + { + private int? _startContentId; + private int? _startMediaId; + private string _alias; + private string _icon; + private string _name; + private IEnumerable _permissions = Enumerable.Empty(); + private IEnumerable _sectionCollection = Enumerable.Empty(); + private string _suffix; + private int? _id; + + /// + /// Will suffix the name and alias for testing + /// + /// + /// + public UserGroupBuilder WithSuffix(string suffix) + { + _suffix = suffix; + return this; + } + + public IReadOnlyUserGroup BuildReadOnly(IUserGroup userGroup) + { + return Mock.Of(x => + x.Permissions == userGroup.Permissions && + x.Alias == userGroup.Alias && + x.Icon == userGroup.Icon && + x.Name == userGroup.Name && + x.StartContentId == userGroup.StartContentId && + x.StartMediaId == userGroup.StartMediaId && + x.AllowedSections == userGroup.AllowedSections && + x.Id == userGroup.Id); + } + + public override IUserGroup Build() + { + return Mock.Of(x => + x.StartContentId == _startContentId && + x.StartMediaId == _startMediaId && + x.Name == (_name ?? ("TestUserGroup" + _suffix)) && + x.Alias == (_alias ?? ("testUserGroup" + _suffix)) && + x.Icon == _icon && + x.Permissions == _permissions && + x.AllowedSections == _sectionCollection); + } + + int? IWithIdBuilder.Id + { + get => _id; + set => _id = value; + } + } +} diff --git a/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs new file mode 100644 index 0000000000..7feb69f0c6 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Persistence/Repositories/UserRepositoryTest.cs @@ -0,0 +1,393 @@ +using System.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Repositories; +using Umbraco.Core.Persistence.Repositories.Implement; +using Umbraco.Core.Scoping; +using Umbraco.Tests.Testing; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Services.Implement; +using Umbraco.Tests.Integration.Testing; + +namespace Umbraco.Tests.Persistence.Repositories +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] + public class UserRepositoryTest : UmbracoIntegrationTest + { + private UserRepository CreateRepository(IScopeProvider provider) + { + var accessor = (IScopeAccessor) provider; + var repository = new UserRepository(accessor, AppCaches.Disabled, Logger, Mappers, GlobalSettings, Mock.Of()); + return repository; + } + + private UserGroupRepository CreateUserGroupRepository(IScopeProvider provider) + { + var accessor = (IScopeAccessor) provider; + return new UserGroupRepository(accessor, AppCaches.Disabled, Logger, ShortStringHelper); + } + + [Test] + public void Can_Perform_Add_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user = UserBuilder.Build(); + + // Act + repository.Save(user); + + + // Assert + Assert.That(user.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Perform_Multiple_Adds_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user1 = UserBuilder.WithSuffix("1").Build(); + var use2 = UserBuilder.WithSuffix("2").Build(); + + // Act + repository.Save(user1); + + repository.Save(use2); + + + // Assert + Assert.That(user1.HasIdentity, Is.True); + Assert.That(use2.HasIdentity, Is.True); + } + } + + [Test] + public void Can_Verify_Fresh_Entity_Is_Not_Dirty() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user = UserBuilder.Build(); + repository.Save(user); + + + // Act + var resolved = repository.Get((int)user.Id); + bool dirty = ((User)resolved).IsDirty(); + + // Assert + Assert.That(dirty, Is.False); + } + } + + [Test] + public void Can_Perform_Delete_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var user = UserBuilder.Build(); + + // Act + repository.Save(user); + + var id = user.Id; + + var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of(),GlobalSettings, Mock.Of()); + + repository2.Delete(user); + + + var resolved = repository2.Get((int) id); + + // Assert + Assert.That(resolved, Is.Null); + } + } + + [Test] + public void Can_Perform_Get_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + + // Act + var updatedItem = repository.Get(user.Id); + + // FIXME: this test cannot work, user has 2 sections but the way it's created, + // they don't show, so the comparison with updatedItem fails - fix! + + // Assert + AssertPropertyValues(updatedItem, user); + } + } + + [Test] + public void Can_Perform_GetByQuery_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + CreateAndCommitMultipleUsers(repository); + + // Act + var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1"); + var result = repository.Get(query); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(1)); + } + } + + [Test] + public void Can_Perform_GetAll_By_Param_Ids_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var result = repository.GetMany((int) users[0].Id, (int) users[1].Id); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.EqualTo(2)); + } + } + + [Test] + public void Can_Perform_GetAll_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + CreateAndCommitMultipleUsers(repository); + + // Act + var result = repository.GetMany(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Any(), Is.True); + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(3)); + } + } + + [Test] + public void Can_Perform_Exists_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var exists = repository.Exists(users[0].Id); + + // Assert + Assert.That(exists, Is.True); + } + } + + [Test] + public void Can_Perform_Count_On_UserRepository() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + // Act + var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + var result = repository.Count(query); + + // Assert + Assert.AreEqual(2, result); + } + } + + [Test] + public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups() + { + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + var query = provider.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + // Act + var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: provider.SqlContext.Query().Where(x => x.Id > -1)); + + // Assert + Assert.AreEqual(2, totalRecs); + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + + } + + [Test] + public void Can_Get_Paged_Results_With_Filter_And_Groups() + { + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + + var users = CreateAndCommitMultipleUsers(repository); + + try + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; + scope.Database.AsUmbracoDatabase().EnableSqlCount = true; + + // Act + var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, + includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, + excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, + filter: provider.SqlContext.Query().Where(x => x.Id == -1)); + + // Assert + Assert.AreEqual(1, totalRecs); + } + finally + { + scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; + scope.Database.AsUmbracoDatabase().EnableSqlCount = false; + } + } + } + + [Test] + public void Can_Invalidate_SecurityStamp_On_Username_Change() + { + // Arrange + var provider = ScopeProvider; + using (var scope = provider.CreateScope()) + { + var repository = CreateRepository(provider); + var userGroupRepository = CreateUserGroupRepository(provider); + + var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); + var originalSecurityStamp = user.SecurityStamp; + + // Ensure when user generated a security stamp is present + Assert.That(user.SecurityStamp, Is.Not.Null); + Assert.That(user.SecurityStamp, Is.Not.Empty); + + // Update username + user.Username = user.Username + "UPDATED"; + repository.Save(user); + + // Get the user + var updatedUser = repository.Get(user.Id); + + // Ensure the Security Stamp is invalidated & no longer the same + Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); + } + } + + private void AssertPropertyValues(IUser updatedItem, IUser originalUser) + { + Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); + Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); + Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); + Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); + Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); + Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); + Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); + Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); + Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); + Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); + Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(originalUser.AllowedSections.Count())); + foreach (var allowedSection in originalUser.AllowedSections) + Assert.IsTrue(updatedItem.AllowedSections.Contains(allowedSection)); + } + + private User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository) + { + var user = UserBuilder.Build(); + repository.Save(user); + + + var group = UserGroupBuilder.Build(); + userGroupRepository.AddOrUpdateGroupWithUsers(@group, new[] { user.Id }); + + user.AddGroup(UserGroupBuilder.BuildReadOnly(group)); + + return user; + } + + private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository) + { + var user1 = UserBuilder.WithSuffix("1").Build(); + var user2 = UserBuilder.WithSuffix("2").Build(); + var user3 = UserBuilder.WithSuffix("3").Build(); + repository.Save(user1); + repository.Save(user2); + repository.Save(user3); + return new IUser[] { user1, user2, user3 }; + } + } +} diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 90a6e7bdf8..3d94e52860 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -7,12 +7,17 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NUnit.Framework; +using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Composing.LightInject; using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Scoping; +using Umbraco.Core.Strings; +using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.Testing; @@ -127,6 +132,8 @@ namespace Umbraco.Tests.Integration.Testing app.UseUmbracoCore(); } + #region Common services + /// /// Returns the DI container /// @@ -146,5 +153,21 @@ namespace Umbraco.Tests.Integration.Testing /// Returns the /// protected ILogger Logger => Services.GetRequiredService(); + + protected AppCaches AppCaches => Services.GetRequiredService(); + protected IIOHelper IOHelper => Services.GetRequiredService(); + protected IShortStringHelper ShortStringHelper => Services.GetRequiredService(); + protected IGlobalSettings GlobalSettings => Services.GetRequiredService(); + protected IMapperCollection Mappers => Services.GetRequiredService(); + + #endregion + + #region Builders + + protected GlobalSettingsBuilder GlobalSettingsBuilder = new GlobalSettingsBuilder(); + protected UserBuilder UserBuilder = new UserBuilder(); + protected UserGroupBuilder UserGroupBuilder = new UserGroupBuilder(); + + #endregion } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index 201b84f29a..8938a69579 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -5,21 +5,20 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; -using Umbraco.Core.Persistence; using Umbraco.Core.PropertyEditors; using System; using Umbraco.Core.Configuration; namespace Umbraco.Tests.Persistence.Repositories { + // TODO: Move the remaining parts to Integration tests + [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] public class UserRepositoryTest : TestWithDatabaseBase @@ -78,72 +77,6 @@ namespace Umbraco.Tests.Persistence.Repositories return new UserGroupRepository(accessor, AppCaches.Disabled, Logger, ShortStringHelper); } - [Test] - public void Can_Perform_Add_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user = MockedUser.CreateUser(); - - // Act - repository.Save(user); - - - // Assert - Assert.That(user.HasIdentity, Is.True); - } - } - - [Test] - public void Can_Perform_Multiple_Adds_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user1 = MockedUser.CreateUser("1"); - var use2 = MockedUser.CreateUser("2"); - - // Act - repository.Save(user1); - - repository.Save(use2); - - - // Assert - Assert.That(user1.HasIdentity, Is.True); - Assert.That(use2.HasIdentity, Is.True); - } - } - - [Test] - public void Can_Verify_Fresh_Entity_Is_Not_Dirty() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user = MockedUser.CreateUser(); - repository.Save(user); - - - // Act - var resolved = repository.Get((int)user.Id); - bool dirty = ((User)resolved).IsDirty(); - - // Assert - Assert.That(dirty, Is.False); - } - } - [Test] public void Can_Perform_Update_On_UserRepository() { @@ -206,268 +139,6 @@ namespace Umbraco.Tests.Persistence.Repositories } } - [Test] - public void Can_Perform_Delete_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var user = MockedUser.CreateUser(); - - // Act - repository.Save(user); - - var id = user.Id; - - var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of(),TestObjects.GetGlobalSettings(), Mock.Of()); - - repository2.Delete(user); - - - var resolved = repository2.Get((int) id); - - // Assert - Assert.That(resolved, Is.Null); - } - } - - [Test] - public void Can_Perform_Get_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - var userGroupRepository = CreateUserGroupRepository(provider); - - var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); - - // Act - var updatedItem = repository.Get(user.Id); - - // FIXME: this test cannot work, user has 2 sections but the way it's created, - // they don't show, so the comparison with updatedItem fails - fix! - - // Assert - AssertPropertyValues(updatedItem, user); - } - } - - [Test] - public void Can_Perform_GetByQuery_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - CreateAndCommitMultipleUsers(repository); - - // Act - var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1"); - var result = repository.Get(query); - - // Assert - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(1)); - } - } - - [Test] - public void Can_Perform_GetAll_By_Param_Ids_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - // Act - var result = repository.GetMany((int) users[0].Id, (int) users[1].Id); - - // Assert - Assert.That(result, Is.Not.Null); - Assert.That(result.Any(), Is.True); - Assert.That(result.Count(), Is.EqualTo(2)); - } - } - - [Test] - public void Can_Perform_GetAll_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - CreateAndCommitMultipleUsers(repository); - - // Act - var result = repository.GetMany(); - - // Assert - Assert.That(result, Is.Not.Null); - Assert.That(result.Any(), Is.True); - Assert.That(result.Count(), Is.GreaterThanOrEqualTo(3)); - } - } - - [Test] - public void Can_Perform_Exists_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - // Act - var exists = repository.Exists(users[0].Id); - - // Assert - Assert.That(exists, Is.True); - } - } - - [Test] - public void Can_Perform_Count_On_UserRepository() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - // Act - var query = scope.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); - var result = repository.Count(query); - - // Assert - Assert.AreEqual(2, result); - } - } - - [Test] - public void Can_Get_Paged_Results_By_Query_And_Filter_And_Groups() - { - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - var query = provider.SqlContext.Query().Where(x => x.Username == "TestUser1" || x.Username == "TestUser2"); - - try - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; - - // Act - var result = repository.GetPagedResultsByQuery(query, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, - excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, - filter: provider.SqlContext.Query().Where(x => x.Id > -1)); - - // Assert - Assert.AreEqual(2, totalRecs); - } - finally - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; - } - } - - } - - [Test] - public void Can_Get_Paged_Results_With_Filter_And_Groups() - { - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - - var users = CreateAndCommitMultipleUsers(repository); - - try - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = true; - scope.Database.AsUmbracoDatabase().EnableSqlCount = true; - - // Act - var result = repository.GetPagedResultsByQuery(null, 0, 10, out var totalRecs, user => user.Id, Direction.Ascending, - includeUserGroups: new[] { Constants.Security.AdminGroupAlias, Constants.Security.SensitiveDataGroupAlias }, - excludeUserGroups: new[] { Constants.Security.TranslatorGroupAlias }, - filter: provider.SqlContext.Query().Where(x => x.Id == -1)); - - // Assert - Assert.AreEqual(1, totalRecs); - } - finally - { - scope.Database.AsUmbracoDatabase().EnableSqlTrace = false; - scope.Database.AsUmbracoDatabase().EnableSqlCount = false; - } - } - } - - [Test] - public void Can_Invalidate_SecurityStamp_On_Username_Change() - { - // Arrange - var provider = TestObjects.GetScopeProvider(Logger); - using (var scope = provider.CreateScope()) - { - var repository = CreateRepository(provider); - var userGroupRepository = CreateUserGroupRepository(provider); - - var user = CreateAndCommitUserWithGroup(repository, userGroupRepository); - var originalSecurityStamp = user.SecurityStamp; - - // Ensure when user generated a security stamp is present - Assert.That(user.SecurityStamp, Is.Not.Null); - Assert.That(user.SecurityStamp, Is.Not.Empty); - - // Update username - user.Username = user.Username + "UPDATED"; - repository.Save(user); - - // Get the user - var updatedUser = repository.Get(user.Id); - - // Ensure the Security Stamp is invalidated & no longer the same - Assert.AreNotEqual(originalSecurityStamp, updatedUser.SecurityStamp); - } - } - - private void AssertPropertyValues(IUser updatedItem, IUser originalUser) - { - Assert.That(updatedItem.Id, Is.EqualTo(originalUser.Id)); - Assert.That(updatedItem.Name, Is.EqualTo(originalUser.Name)); - Assert.That(updatedItem.Language, Is.EqualTo(originalUser.Language)); - Assert.That(updatedItem.IsApproved, Is.EqualTo(originalUser.IsApproved)); - Assert.That(updatedItem.RawPasswordValue, Is.EqualTo(originalUser.RawPasswordValue)); - Assert.That(updatedItem.IsLockedOut, Is.EqualTo(originalUser.IsLockedOut)); - Assert.IsTrue(updatedItem.StartContentIds.UnsortedSequenceEqual(originalUser.StartContentIds)); - Assert.IsTrue(updatedItem.StartMediaIds.UnsortedSequenceEqual(originalUser.StartMediaIds)); - Assert.That(updatedItem.Email, Is.EqualTo(originalUser.Email)); - Assert.That(updatedItem.Username, Is.EqualTo(originalUser.Username)); - Assert.That(updatedItem.AllowedSections.Count(), Is.EqualTo(originalUser.AllowedSections.Count())); - foreach (var allowedSection in originalUser.AllowedSections) - Assert.IsTrue(updatedItem.AllowedSections.Contains(allowedSection)); - } private static User CreateAndCommitUserWithGroup(IUserRepository repository, IUserGroupRepository userGroupRepository) { @@ -483,15 +154,5 @@ namespace Umbraco.Tests.Persistence.Repositories return user; } - private IUser[] CreateAndCommitMultipleUsers(IUserRepository repository) - { - var user1 = MockedUser.CreateUser("1"); - var user2 = MockedUser.CreateUser("2"); - var user3 = MockedUser.CreateUser("3"); - repository.Save(user1); - repository.Save(user2); - repository.Save(user3); - return new IUser[] { user1, user2, user3 }; - } } } diff --git a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs index a07be868a5..49444f8b68 100644 --- a/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs +++ b/src/Umbraco.Tests/TestHelpers/TestObjects-Mocks.cs @@ -4,24 +4,19 @@ using System.Data; using System.Data.Common; using System.Linq; using System.Linq.Expressions; -using System.Web; using Moq; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Services; using Umbraco.Tests.Common; using Umbraco.Web; using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; namespace Umbraco.Tests.TestHelpers { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f667b11aa3..a1d086f94d 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -148,6 +148,7 @@ + @@ -340,7 +341,6 @@ - From 75b216bca569ed4d9498dac2fd39d801870d54e5 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 30 Mar 2020 14:30:50 +0200 Subject: [PATCH 31/34] Removed registerFileResolver methods on image cropper and file upload property editors. --- .../fileupload/fileupload.controller.js | 34 +------------ .../imagecropper/imagecropper.controller.js | 51 ------------------- 2 files changed, 1 insertion(+), 84 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index ee71a0d2f2..15f8dbf11c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -41,37 +41,5 @@ }; angular.module("umbraco") - .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) - .run(function (mediaHelper, umbRequestHelper, assetsService) { - if (mediaHelper && mediaHelper.registerFileResolver) { - // NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource - // they contain different data structures so if we need to query against it we need to be aware of this. - mediaHelper.registerFileResolver("Umbraco.UploadField", function (property, entity, thumbnail) { - if (thumbnail) { - if (mediaHelper.detectIfImageByExtension(property.value)) { - - // Get default big thumbnail from image processor - return mediaHelper.getProcessedImageUrl(property.value, - { - animationprocessmode: "first", - cacheBusterValue: moment(entity.updateDate).format("YYYYMMDDHHmmss"), - width: 500 - }) - .then(function (url) { - return url; - }); - } - else { - return null; - } - } - else { - return property.value; - } - }); - - } - }); - - + .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index c483e83845..cfb15dc8b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -214,55 +214,4 @@ angular.module('umbraco') $scope.$on('$destroy', function () { unsubscribe(); }); - }) - .run(function (mediaHelper, umbRequestHelper) { - if (mediaHelper && mediaHelper.registerFileResolver) { - - //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource - // they contain different data structures so if we need to query against it we need to be aware of this. - mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { - if (property.value && property.value.src) { - if (thumbnail === true) { - return mediaHelper.getProcessedImageUrl(property.value.src, - { - animationprocessmode: "first", - mode: "max", - width: 500 - }) - .then(function (url) { - return url; - }); - - } - else { - return property.value.src; - } - - //this is a fallback in case the cropper has been asssigned a upload field - } - else if (angular.isString(property.value)) { - if (thumbnail) { - - if (mediaHelper.detectIfImageByExtension(property.value)) { - - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: property.value }]); - - return thumbnailUrl; - } - else { - return null; - } - - } - else { - return property.value; - } - } - - return null; - }); - } }); From 4ac8ae3b1520acd0f7281239d8dc7296ca3b3892 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska Date: Mon, 30 Mar 2020 15:29:53 +0200 Subject: [PATCH 32/34] Removing specific to each machine file paths from project --- src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index abfb172763..dc148d8d1f 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -15,9 +15,4 @@ - - <_ContentIncludedByDefault Remove="wwwroot\~\App_Data\TEMP\TypesCache\umbraco-types.DESKTOP-2016.hash" /> - <_ContentIncludedByDefault Remove="wwwroot\~\App_Data\TEMP\TypesCache\umbraco-types.DESKTOP-2016.list" /> - - From 2a2903f932645256ad36fdaa90fec547a8f9a8b1 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 31 Mar 2020 12:22:11 +0200 Subject: [PATCH 33/34] AB#5820 - Review fixes / Clean up --- src/Umbraco.Tests.Integration/RuntimeTests.cs | 7 ++----- .../Testing/UmbracoIntegrationTest.cs | 3 +-- .../Umbraco.Tests.Integration.csproj | 1 + .../AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs | 2 +- .../UmbracoCoreServiceCollectionExtensions.cs | 3 ++- .../Lifetime/UmbracoRequestLifetime.cs | 2 +- src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs | 10 ---------- .../AspNet/AspNetUmbracoApplicationLifetime.cs | 2 +- 8 files changed, 9 insertions(+), 21 deletions(-) rename src/Umbraco.Web.Common/{AspNetCore => Extensions}/UmbracoCoreServiceCollectionExtensions.cs (99%) diff --git a/src/Umbraco.Tests.Integration/RuntimeTests.cs b/src/Umbraco.Tests.Integration/RuntimeTests.cs index 52a4ac1062..2974a01669 100644 --- a/src/Umbraco.Tests.Integration/RuntimeTests.cs +++ b/src/Umbraco.Tests.Integration/RuntimeTests.cs @@ -1,10 +1,7 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; -using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Umbraco.Core; @@ -16,7 +13,7 @@ using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; using Umbraco.Tests.Integration.Testing; using Umbraco.Web.BackOffice.AspNetCore; -using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Extensions; namespace Umbraco.Tests.Integration { diff --git a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 3d94e52860..43504e908a 100644 --- a/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/src/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Data.Common; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -20,8 +19,8 @@ using Umbraco.Core.Strings; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Integration.Extensions; using Umbraco.Tests.Integration.Implementations; -using Umbraco.Tests.Testing; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.Extensions; namespace Umbraco.Tests.Integration.Testing { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 320db33568..59b7de4cf4 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -4,6 +4,7 @@ Exe netcoreapp3.1 false + 8 diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index 38d78860b0..c8d8f40a00 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -6,7 +6,7 @@ using Umbraco.Net; namespace Umbraco.Web.Common.AspNetCore { - public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime, IUmbracoApplicationLifetimeManager + public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetimeManager { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHostApplicationLifetime _hostApplicationLifetime; diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs similarity index 99% rename from src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs rename to src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 99a274364b..6f2b31bac1 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -17,9 +17,10 @@ using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Persistence; using Umbraco.Core.Runtime; +using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Runtime.Profiler; -namespace Umbraco.Web.Common.AspNetCore +namespace Umbraco.Web.Common.Extensions { public static class UmbracoCoreServiceCollectionExtensions { diff --git a/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs b/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs index 43810a9e66..0d858679a6 100644 --- a/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs +++ b/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Http; namespace Umbraco.Web.Common.Lifetime { - public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager + public class UmbracoRequestLifetime : IUmbracoRequestLifetimeManager { public event EventHandler RequestStart; public event EventHandler RequestEnd; diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs index 431c195d90..bdbc6f164d 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -119,15 +119,5 @@ namespace Umbraco.Web.Common.Runtime.Profiler if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; return false; } - - /// - /// Indicates the boot phase. - /// - private enum BootPhase - { - Boot = 0, // boot phase (before the 1st request even begins) - BootRequest = 1, // request boot phase (during the 1st request) - Booted = 2 // done booting - } } } diff --git a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs index f0ff6e3cad..8c43293ad7 100644 --- a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs @@ -5,7 +5,7 @@ using Umbraco.Net; namespace Umbraco.Web.AspNet { - public class AspNetUmbracoApplicationLifetime : IUmbracoApplicationLifetime, IUmbracoApplicationLifetimeManager + public class AspNetUmbracoApplicationLifetime : IUmbracoApplicationLifetimeManager { private readonly IHttpContextAccessor _httpContextAccessor; From b9ad6a0b7c386bdeac51c9aabd7bf6a01bb8f1a4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 31 Mar 2020 15:02:02 +0200 Subject: [PATCH 34/34] AB#5820 - Cleanup --- src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs | 2 +- .../AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs | 2 +- .../Lifetime/IUmbracoRequestLifetimeManager.cs | 2 +- src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs | 2 +- src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs | 1 - .../Runtime/Profiler/WebProfilerComponent.cs | 4 ++-- src/Umbraco.Web/Logging/WebProfilerComposer.cs | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs index f594e45652..a032720d46 100644 --- a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs @@ -18,7 +18,7 @@ namespace Umbraco.Net } - public interface IUmbracoApplicationLifetimeManager: IUmbracoApplicationLifetime + public interface IUmbracoApplicationLifetimeManager { void InvokeApplicationInit(); } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index c8d8f40a00..38d78860b0 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -6,7 +6,7 @@ using Umbraco.Net; namespace Umbraco.Web.Common.AspNetCore { - public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetimeManager + public class AspNetCoreUmbracoApplicationLifetime : IUmbracoApplicationLifetime, IUmbracoApplicationLifetimeManager { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHostApplicationLifetime _hostApplicationLifetime; diff --git a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs index 7ee8fac1e9..e4c671c2d3 100644 --- a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs +++ b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetimeManager.cs @@ -2,7 +2,7 @@ namespace Umbraco.Web.Common.Lifetime { - public interface IUmbracoRequestLifetimeManager : IUmbracoRequestLifetime + public interface IUmbracoRequestLifetimeManager { void InitRequest(HttpContext context); void EndRequest(HttpContext context); diff --git a/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs b/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs index 0d858679a6..43810a9e66 100644 --- a/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs +++ b/src/Umbraco.Web.Common/Lifetime/UmbracoRequestLifetime.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Http; namespace Umbraco.Web.Common.Lifetime { - public class UmbracoRequestLifetime : IUmbracoRequestLifetimeManager + public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager { public event EventHandler RequestStart; public event EventHandler RequestEnd; diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index a0b2fb9dd8..6c5aeb1b56 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -34,7 +34,6 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); composition.RegisterUnique(factory => factory.GetInstance()); composition.RegisterUnique(factory => factory.GetInstance()); - composition.RegisterUnique(); } } } diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs index 7f0b5071c5..a36753e634 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -11,11 +11,11 @@ namespace Umbraco.Web.Common.Runtime.Profiler { private readonly bool _profile; private readonly WebProfiler _profiler; - private readonly IUmbracoApplicationLifetimeManager _umbracoApplicationLifetime; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; public WebProfilerComponent(IProfiler profiler, ILogger logger, IUmbracoRequestLifetime umbracoRequestLifetime, - IUmbracoApplicationLifetimeManager umbracoApplicationLifetime) + IUmbracoApplicationLifetime umbracoApplicationLifetime) { _umbracoRequestLifetime = umbracoRequestLifetime; _umbracoApplicationLifetime = umbracoApplicationLifetime; diff --git a/src/Umbraco.Web/Logging/WebProfilerComposer.cs b/src/Umbraco.Web/Logging/WebProfilerComposer.cs index 5834dd9dd4..283c519b44 100644 --- a/src/Umbraco.Web/Logging/WebProfilerComposer.cs +++ b/src/Umbraco.Web/Logging/WebProfilerComposer.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Composing; + using Umbraco.Core.Composing; namespace Umbraco.Web.Logging {