Initialize important services before unattended installs (#17366)

* Added new notification to hook in after the premigrations and use this to init different services.

* Force MaxDegreeOfParallelism to 1, while investigating scopes

* Tried some more workarounds

* Updated scopes and changed parallel to non parallel to ensure migration works

* Missing scope

* Make it parallel again - The secret is, the SuppressFlow needs to be when you create the task, but not on the await!.

* Fixed issue when DEBUG_SCOPES is not added to tests.

* Remove test exception

* Try build on ubuntu again, even that we know it can be stuck. Just a test to see if all tests pass

* Updated comment

---------

Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
Bjarke Berg
2024-10-28 12:10:38 +01:00
committed by GitHub
parent 5cc0a35665
commit 935c3b8e42
35 changed files with 300 additions and 225 deletions

View File

@@ -71,7 +71,7 @@ stages:
- job: A
displayName: Build Umbraco CMS
pool:
vmImage: 'windows-latest'
vmImage: 'ubuntu-latest'
steps:
- checkout: self
submodules: true

View File

@@ -5,36 +5,45 @@ namespace Umbraco.Cms.Persistence.EFCore.Scoping;
public class AmbientEFCoreScopeStack<TDbContext> : IAmbientEFCoreScopeStack<TDbContext> where TDbContext : DbContext
{
private static Lock _lock = new();
private static AsyncLocal<ConcurrentStack<IEfCoreScope<TDbContext>>> _stack = new();
public IEfCoreScope<TDbContext>? AmbientScope
{
get
{
if (_stack.Value?.TryPeek(out IEfCoreScope<TDbContext>? ambientScope) ?? false)
lock (_lock)
{
return ambientScope;
}
if (_stack.Value?.TryPeek(out IEfCoreScope<TDbContext>? ambientScope) ?? false)
{
return ambientScope;
}
return null;
return null;
}
}
}
public IEfCoreScope<TDbContext> Pop()
{
if (_stack.Value?.TryPop(out IEfCoreScope<TDbContext>? ambientScope) ?? false)
lock (_lock)
{
return ambientScope;
}
if (_stack.Value?.TryPop(out IEfCoreScope<TDbContext>? ambientScope) ?? false)
{
return ambientScope;
}
throw new InvalidOperationException("No AmbientScope was found.");
throw new InvalidOperationException("No AmbientScope was found.");
}
}
public void Push(IEfCoreScope<TDbContext> scope)
{
_stack.Value ??= new ConcurrentStack<IEfCoreScope<TDbContext>>();
lock (_lock)
{
_stack.Value ??= new ConcurrentStack<IEfCoreScope<TDbContext>>();
_stack.Value.Push(scope);
_stack.Value.Push(scope);
}
}
}

View File

@@ -12,7 +12,7 @@ public static partial class Constants
{
public static readonly string[] UmbracoCoreAssemblyNames =
{
"Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.PublishedCache.NuCache", "Umbraco.Examine.Lucene",
"Umbraco.Core", "Umbraco.Infrastructure", "Umbraco.Examine.Lucene",
"Umbraco.Web.Common", "Umbraco.Cms.Api.Common","Umbraco.Cms.Api.Delivery","Umbraco.Cms.Api.Management", "Umbraco.Web.Website",
};
}

View File

@@ -416,7 +416,8 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Routing
Services.AddUnique<IDocumentUrlService, DocumentUrlService>();
Services.AddHostedService<DocumentUrlServiceInitializer>();
Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlServiceInitializerNotificationHandler>();
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Core.Notifications;
public class PostRuntimePremigrationsUpgradeNotification : INotification
{
}

View File

@@ -1,53 +0,0 @@
using Microsoft.Extensions.Hosting;
namespace Umbraco.Cms.Core.Services;
public class DocumentUrlServiceInitializer : IHostedLifecycleService
{
private readonly IDocumentUrlService _documentUrlService;
private readonly IRuntimeState _runtimeState;
public DocumentUrlServiceInitializer(IDocumentUrlService documentUrlService, IRuntimeState runtimeState)
{
_documentUrlService = documentUrlService;
_runtimeState = runtimeState;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (_runtimeState.Level == RuntimeLevel.Upgrade)
{
//Special case on the first upgrade, as the database is not ready yet.
return;
}
await _documentUrlService.InitAsync(
_runtimeState.Level <= RuntimeLevel.Install,
cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StartingAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StartedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StoppingAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task StoppedAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,29 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
namespace Umbraco.Cms.Core.Services;
public class DocumentUrlServiceInitializerNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartingNotification>
{
private readonly IDocumentUrlService _documentUrlService;
private readonly IRuntimeState _runtimeState;
public DocumentUrlServiceInitializerNotificationHandler(IDocumentUrlService documentUrlService, IRuntimeState runtimeState)
{
_documentUrlService = documentUrlService;
_runtimeState = runtimeState;
}
public async Task HandleAsync(UmbracoApplicationStartingNotification notification, CancellationToken cancellationToken)
{
if (_runtimeState.Level == RuntimeLevel.Upgrade)
{
//Special case on the first upgrade, as the database is not ready yet.
return;
}
await _documentUrlService.InitAsync(
_runtimeState.Level <= RuntimeLevel.Install,
cancellationToken);
}
}

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
namespace Umbraco.Cms.Core.Services.Navigation;
@@ -6,13 +8,13 @@ namespace Umbraco.Cms.Core.Services.Navigation;
/// Responsible for seeding the in-memory navigation structures at application's startup
/// by rebuild the navigation structures.
/// </summary>
public sealed class NavigationInitializationHostedService : IHostedLifecycleService
public sealed class NavigationInitializationNotificationHandler : INotificationAsyncHandler<PostRuntimePremigrationsUpgradeNotification>
{
private readonly IRuntimeState _runtimeState;
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
public NavigationInitializationHostedService(
public NavigationInitializationNotificationHandler(
IRuntimeState runtimeState,
IDocumentNavigationManagementService documentNavigationManagementService,
IMediaNavigationManagementService mediaNavigationManagementService)
@@ -22,7 +24,7 @@ public sealed class NavigationInitializationHostedService : IHostedLifecycleServ
_mediaNavigationManagementService = mediaNavigationManagementService;
}
public async Task StartingAsync(CancellationToken cancellationToken)
public async Task HandleAsync(PostRuntimePremigrationsUpgradeNotification notification, CancellationToken cancellationToken)
{
if(_runtimeState.Level < RuntimeLevel.Upgrade)
{
@@ -34,14 +36,4 @@ public sealed class NavigationInitializationHostedService : IHostedLifecycleServ
await _mediaNavigationManagementService.RebuildAsync();
await _mediaNavigationManagementService.RebuildBinAsync();
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
namespace Umbraco.Cms.Core.Services.Navigation;
@@ -6,12 +8,12 @@ namespace Umbraco.Cms.Core.Services.Navigation;
/// Responsible for seeding the in-memory publish status cache at application's startup
/// by loading all data from the database.
/// </summary>
public sealed class PublishStatusInitializationHostedService : IHostedLifecycleService
public sealed class PublishStatusInitializationNotificationHandler : INotificationAsyncHandler<PostRuntimePremigrationsUpgradeNotification>
{
private readonly IRuntimeState _runtimeState;
private readonly IPublishStatusManagementService _publishStatusManagementService;
public PublishStatusInitializationHostedService(
public PublishStatusInitializationNotificationHandler(
IRuntimeState runtimeState,
IPublishStatusManagementService publishStatusManagementService
)
@@ -20,7 +22,7 @@ public sealed class PublishStatusInitializationHostedService : IHostedLifecycleS
_publishStatusManagementService = publishStatusManagementService;
}
public async Task StartingAsync(CancellationToken cancellationToken)
public async Task HandleAsync(PostRuntimePremigrationsUpgradeNotification notification, CancellationToken cancellationToken)
{
if(_runtimeState.Level < RuntimeLevel.Upgrade)
{
@@ -29,14 +31,4 @@ public sealed class PublishStatusInitializationHostedService : IHostedLifecycleS
await _publishStatusManagementService.InitializeAsync(cancellationToken);
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -11,7 +11,7 @@ using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Infrastructure.Install;
/// <summary>
/// Handles <see cref="RuntimeUnattendedUpgradeNotification" /> to execute the unattended Umbraco upgrader
/// Handles <see cref="RuntimePremigrationsUpgradeNotification" /> to execute the unattended Umbraco upgrader
/// or the unattended Package migrations runner.
/// </summary>
public class PremigrationUpgrader : INotificationAsyncHandler<RuntimePremigrationsUpgradeNotification>

View File

@@ -66,14 +66,14 @@ public class UmbracoPlan : MigrationPlan
To<V_13_5_0.ChangeRedirectUrlToNvarcharMax>("{CC47C751-A81B-489A-A2BC-0240245DB687}");
// To 14.0.0
To<V_14_0_0.AddPropertyEditorUiAliasColumn>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");
To<NoopMigration>("{419827A0-4FCE-464B-A8F3-247C6092AF55}");
To<NoopMigration>("{69E12556-D9B3-493A-8E8A-65EC89FB658D}");
To<NoopMigration>("{F2B16CD4-F181-4BEE-81C9-11CF384E6025}");
To<NoopMigration>("{A8E01644-9F2E-4988-8341-587EF5B7EA69}");
To<V_14_0_0.UpdateDefaultGuidsOfCreatedPackages>("{E073DBC0-9E8E-4C92-8210-9CB18364F46E}");
To<V_14_0_0.RenameTechnologyLeakingPropertyEditorAliases>("{80D282A4-5497-47FF-991F-BC0BCE603121}");
To<V_14_0_0.MigrateSchduledPublishesToUtc>("{96525697-E9DC-4198-B136-25AD033442B8}");
To<V_14_0_0.AddListViewKeysToDocumentTypes>("{7FC5AC9B-6F56-415B-913E-4A900629B853}");
To<NoopMigration>("{7FC5AC9B-6F56-415B-913E-4A900629B853}");
To<V_14_0_0.MigrateDataTypeConfigurations>("{1539A010-2EB5-4163-8518-4AE2AA98AFC6}");
To<NoopMigration>("{C567DE81-DF92-4B99-BEA8-CD34EF99DA5D}");
To<V_14_0_0.DeleteMacroTables>("{0D82C836-96DD-480D-A924-7964E458BD34}");

View File

@@ -65,5 +65,7 @@ public class UmbracoPremigrationPlan : MigrationPlan
To<V_14_0_0.MigrateTours>("{A08254B6-D9E7-4207-A496-2ED0A87FB4FD}");
To<V_15_0_0.AddKindToUser>("{69AA6889-8B67-42B4-AA4F-114704487A45}");
To<V_15_0_0.AddDocumentUrl>("{B9133686-B758-404D-AF12-708AA80C7E44}");
To<V_14_0_0.AddPropertyEditorUiAliasColumn>("{EEB1F012-B44D-4AB4-8756-F7FB547345B4}");
To<V_14_0_0.AddListViewKeysToDocumentTypes>("{0F49E1A4-AFD8-4673-A91B-F64E78C48174}");
}
}

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
@@ -23,6 +24,7 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
private readonly IJsonSerializer _jsonSerializer;
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly ILanguageService _languageService;
private readonly ICoreScopeProvider _coreScopeProvider;
protected abstract IEnumerable<string> PropertyEditorAliases { get; }
@@ -44,7 +46,8 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
IDataTypeService dataTypeService,
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService)
ILanguageService languageService,
ICoreScopeProvider coreScopeProvider)
: base(context)
{
_logger = logger;
@@ -53,6 +56,7 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
_jsonSerializer = jsonSerializer;
_umbracoContextFactory = umbracoContextFactory;
_languageService = languageService;
_coreScopeProvider = coreScopeProvider;
}
protected override void Migrate()
@@ -71,6 +75,7 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
.GroupBy(pt => pt.PropertyEditorAlias)
.ToDictionary(group => group.Key, group => group.ToArray());
foreach (var propertyEditorAlias in PropertyEditorAliases)
{
if (allPropertyTypesByEditor.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false)
@@ -98,11 +103,17 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
{
var success = true;
foreach (IPropertyType propertyType in propertyTypes)
var propertyTypeCount = propertyTypes.Length;
for (var propertyTypeIndex = 0; propertyTypeIndex < propertyTypeCount; propertyTypeIndex++)
{
IPropertyType propertyType = propertyTypes[propertyTypeIndex];
try
{
_logger.LogInformation("- starting property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias})...", propertyType.Name, propertyType.Id, propertyType.Alias);
_logger.LogInformation(
"- starting property type {propertyTypeIndex}/{propertyTypeCount} : {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias})...",
propertyTypeIndex+1,
propertyTypeCount,
propertyType.Name, propertyType.Id, propertyType.Alias);
IDataType dataType = _dataTypeService.GetAsync(propertyType.DataTypeKey).GetAwaiter().GetResult()
?? throw new InvalidOperationException("The data type could not be fetched.");
@@ -113,7 +124,8 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
}
IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor()
?? throw new InvalidOperationException("The data type value editor could not be fetched.");
?? throw new InvalidOperationException(
"The data type value editor could not be fetched.");
Sql<ISqlContext> sql = Sql()
.Select<PropertyDataDto>()
@@ -132,15 +144,24 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
var progress = 0;
ExecutionContext.SuppressFlow();
Parallel.ForEach(updateBatch, update =>
Parallel.ForEachAsync(updateBatch, async (update, token) =>
{
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
//Foreach here, but we need to suppress the flow before each task, but not the actuall await of the task
Task task;
using (ExecutionContext.SuppressFlow())
{
task = Task.Run(() =>
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
scope.Complete();
using UmbracoContextReference umbracoContextReference =
_umbracoContextFactory.EnsureUmbracoContext();
progress++;
if (progress % 100 == 0)
{
_logger.LogInformation(" - finíshed {progress} of {total} properties", progress, updateBatch.Count);
_logger.LogInformation(" - finíshed {progress} of {total} properties", progress,
updateBatch.Count);
}
PropertyDataDto propertyDataDto = update.Poco;
@@ -148,7 +169,8 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
// NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies
var culture = propertyType.VariesByCulture()
&& propertyDataDto.LanguageId.HasValue
&& languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language)
&& languagesById.TryGetValue(propertyDataDto.LanguageId.Value,
out ILanguage? language)
? language.IsoCode
: null;
@@ -211,6 +233,7 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
default:
throw new ArgumentOutOfRangeException();
}
break;
}
@@ -233,8 +256,11 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
stringValue = UpdateDatabaseValue(stringValue);
propertyDataDto.TextValue = stringValue;
});
ExecutionContext.RestoreFlow();
}, token);
}
await task;
}).GetAwaiter().GetResult();
updateBatch.RemoveAll(updatesToSkip.Contains);
@@ -248,7 +274,8 @@ public abstract class ConvertBlockEditorPropertiesBase : MigrationBase
var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
if (result != updateBatch.Count)
{
throw new InvalidOperationException($"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries.");
throw new InvalidOperationException(
$"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries.");
}
_logger.LogDebug(

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
@@ -19,8 +20,9 @@ public class ConvertBlockGridEditorProperties : ConvertBlockEditorPropertiesBase
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService,
IOptions<ConvertBlockEditorPropertiesOptions> options)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService)
IOptions<ConvertBlockEditorPropertiesOptions> options,
ICoreScopeProvider coreScopeProvider)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService, coreScopeProvider)
=> SkipMigration = options.Value.SkipBlockGridEditors;
protected override IEnumerable<string> PropertyEditorAliases
@@ -40,8 +42,9 @@ public class ConvertBlockGridEditorProperties : ConvertBlockEditorPropertiesBase
IDataTypeService dataTypeService,
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService)
ILanguageService languageService,
ICoreScopeProvider coreScopeProvider)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService, coreScopeProvider)
{
}
}

View File

@@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
@@ -19,8 +20,9 @@ public class ConvertBlockListEditorProperties : ConvertBlockEditorPropertiesBase
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService,
IOptions<ConvertBlockEditorPropertiesOptions> options)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService)
IOptions<ConvertBlockEditorPropertiesOptions> options,
ICoreScopeProvider coreScopeProvider)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService, coreScopeProvider)
=> SkipMigration = options.Value.SkipBlockListEditors;
protected override IEnumerable<string> PropertyEditorAliases
@@ -40,8 +42,9 @@ public class ConvertBlockListEditorProperties : ConvertBlockEditorPropertiesBase
IDataTypeService dataTypeService,
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService)
ILanguageService languageService,
ICoreScopeProvider coreScopeProvider)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService, coreScopeProvider)
{
}
}

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
@@ -21,8 +22,9 @@ public partial class ConvertRichTextEditorProperties : ConvertBlockEditorPropert
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService,
IOptions<ConvertBlockEditorPropertiesOptions> options)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService)
IOptions<ConvertBlockEditorPropertiesOptions> options,
ICoreScopeProvider coreScopeProvider)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService, coreScopeProvider)
=> SkipMigration = options.Value.SkipRichTextEditors;
protected override IEnumerable<string> PropertyEditorAliases
@@ -60,8 +62,9 @@ public partial class ConvertRichTextEditorProperties : ConvertBlockEditorPropert
IDataTypeService dataTypeService,
IJsonSerializer jsonSerializer,
IUmbracoContextFactory umbracoContextFactory,
ILanguageService languageService)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService)
ILanguageService languageService,
ICoreScopeProvider coreScopeProvider)
: base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService, coreScopeProvider)
{
}

View File

@@ -199,6 +199,10 @@ public class CoreRuntime : IRuntime
break;
}
//
var postRuntimePremigrationsUpgradeNotification = new PostRuntimePremigrationsUpgradeNotification();
await _eventAggregator.PublishAsync(postRuntimePremigrationsUpgradeNotification, cancellationToken);
// If level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade
var unattendedUpgradeNotification = new RuntimeUnattendedUpgradeNotification();
await _eventAggregator.PublishAsync(unattendedUpgradeNotification, cancellationToken);

View File

@@ -5,35 +5,46 @@ namespace Umbraco.Cms.Infrastructure.Scoping;
internal class AmbientScopeContextStack : IAmbientScopeContextStack
{
private static Lock _lock = new();
private static AsyncLocal<ConcurrentStack<IScopeContext>> _stack = new();
public IScopeContext? AmbientContext
{
get
{
if (_stack.Value?.TryPeek(out IScopeContext? ambientContext) ?? false)
lock (_lock)
{
return ambientContext;
if (_stack.Value?.TryPeek(out IScopeContext? ambientContext) ?? false)
{
return ambientContext;
}
return null;
}
return null;
}
}
public IScopeContext Pop()
{
if (_stack.Value?.TryPop(out IScopeContext? ambientContext) ?? false)
lock (_lock)
{
return ambientContext;
}
if (_stack.Value?.TryPop(out IScopeContext? ambientContext) ?? false)
{
return ambientContext;
}
throw new InvalidOperationException("No AmbientContext was found.");
throw new InvalidOperationException("No AmbientContext was found.");
}
}
public void Push(IScopeContext scope)
{
_stack.Value ??= new ConcurrentStack<IScopeContext>();
lock (_lock)
{
_stack.Value ??= new ConcurrentStack<IScopeContext>();
_stack.Value.Push(scope);
_stack.Value.Push(scope);
}
}
}

View File

@@ -4,36 +4,46 @@ namespace Umbraco.Cms.Infrastructure.Scoping
{
internal class AmbientScopeStack : IAmbientScopeStack
{
private static Lock _lock = new();
private static AsyncLocal<ConcurrentStack<IScope>> _stack = new ();
public IScope? AmbientScope
{
get
{
if (_stack.Value?.TryPeek(out IScope? ambientScope) ?? false)
lock (_lock)
{
return ambientScope;
}
if (_stack.Value?.TryPeek(out IScope? ambientScope) ?? false)
{
return ambientScope;
}
return null;
return null;
}
}
}
public IScope Pop()
{
if (_stack.Value?.TryPop(out IScope? ambientScope) ?? false)
lock (_lock)
{
return ambientScope;
}
throw new InvalidOperationException("No AmbientScope was found.");
if (_stack.Value?.TryPop(out IScope? ambientScope) ?? false)
{
return ambientScope;
}
throw new InvalidOperationException("No AmbientScope was found.");
}
}
public void Push(IScope scope)
{
_stack.Value ??= new ConcurrentStack<IScope>();
_stack.Value.Push(scope);
lock (_lock)
{
(_stack.Value ??= new ConcurrentStack<IScope>()).Push(scope);
}
}
}
}

View File

@@ -367,7 +367,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
$"The {nameof(Scope)} {InstanceId} being disposed is not the Ambient {nameof(Scope)} {_scopeProvider.AmbientScope?.InstanceId.ToString() ?? "NULL"}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow().";
#if DEBUG_SCOPES
Scope ambient = _scopeProvider.AmbientScope;
Scope? ambient = _scopeProvider.AmbientScope;
_logger.LogWarning("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
if (ambient == null)
{
@@ -377,8 +377,8 @@ namespace Umbraco.Cms.Infrastructure.Scoping
ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient);
ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this);
throw new InvalidOperationException($"{failedMessage} (see ctor stack traces).\r\n"
+ "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n"
+ "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n");
+ "- ambient ->\r\n" + ambientInfos + "\r\n"
+ "- dispose ->\r\n" + disposeInfos + "\r\n");
#else
throw new InvalidOperationException(failedMessage);
#endif

View File

@@ -267,7 +267,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
{
lock (s_staticScopeInfosLock)
{
return s_staticScopeInfos.TryGetValue(scope, out ScopeInfo scopeInfo) ? scopeInfo : null;
return s_staticScopeInfos.TryGetValue(scope, out ScopeInfo? scopeInfo) ? scopeInfo : null!;
}
}
@@ -302,7 +302,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
lock (s_staticScopeInfosLock)
{
if (s_staticScopeInfos.TryGetValue(scope, out ScopeInfo info) == false)
if (s_staticScopeInfos.TryGetValue(scope, out ScopeInfo? info) == false)
{
info = null;
}
@@ -327,7 +327,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
sb.Append(s.InstanceId.ToString("N").Substring(0, 8));
var ss = s as Scope;
s = ss?.ParentScope;
s = ss?.ParentScope!;
}
_logger.LogTrace("Register " + (context ?? "null") + " context " + sb);
@@ -339,7 +339,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
_logger.LogTrace("At:\r\n" + Head(Environment.StackTrace, 16));
info.Context = context;
info.Context = context!;
}
}
@@ -431,15 +431,15 @@ namespace Umbraco.Cms.Infrastructure.Scoping
public IScope Scope { get; } // the scope itself
// the scope's parent identifier
public Guid Parent => ((Scope)Scope).ParentScope == null ? Guid.Empty : ((Scope)Scope).ParentScope.InstanceId;
public Guid Parent => ((Scope)Scope).ParentScope == null ? Guid.Empty : ((Scope)Scope).ParentScope!.InstanceId;
public DateTime Created { get; } // the date time the scope was created
public bool Disposed { get; set; } // whether the scope has been disposed already
public string Context { get; set; } // the current 'context' that contains the scope (null, "http" or "lcc")
public string Context { get; set; }= string.Empty; // the current 'context' that contains the scope (null, "http" or "lcc")
public string CtorStack { get; } // the stacktrace of the scope ctor
//public string DisposedStack { get; set; } // the stacktrace when disposed
public string NullStack { get; set; } // the stacktrace when the 'context' that contains the scope went null
public string NullStack { get; set; } = string.Empty; // the stacktrace when the 'context' that contains the scope went null
public override string ToString() => new StringBuilder()
.AppendLine("ScopeInfo:")

View File

@@ -32,9 +32,7 @@ internal class SeedingNotificationHandler : INotificationAsyncHandler<UmbracoApp
return;
}
await Task.WhenAll(
_documentCacheService.SeedAsync(cancellationToken),
_mediaCacheService.SeedAsync(cancellationToken)
);
await _documentCacheService.SeedAsync(cancellationToken);
await _mediaCacheService.SeedAsync(cancellationToken);
}
}

View File

@@ -72,15 +72,18 @@ internal sealed class DocumentCacheService : IDocumentCacheService
public async Task<IPublishedContent?> GetByKeyAsync(Guid key, bool? preview = null)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
bool calculatedPreview = preview ?? GetPreview();
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
GetCacheKey(key, calculatedPreview), // Unique key to the cache entry
async cancel => await _databaseCacheRepository.GetContentSourceAsync(key, calculatedPreview));
async cancel =>
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, calculatedPreview);
scope.Complete();
return contentCacheNode;
});
scope.Complete();
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, calculatedPreview).CreateModel(_publishedModelFactory);
}
@@ -99,11 +102,16 @@ internal sealed class DocumentCacheService : IDocumentCacheService
bool calculatedPreview = preview ?? GetPreview();
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
GetCacheKey(keyAttempt.Result, calculatedPreview), // Unique key to the cache entry
async cancel => await _databaseCacheRepository.GetContentSourceAsync(id, calculatedPreview));
scope.Complete();
async cancel =>
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? contentCacheNode = await _databaseCacheRepository.GetContentSourceAsync(id, calculatedPreview);
scope.Complete();
return contentCacheNode;
});
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, calculatedPreview).CreateModel(_publishedModelFactory);;
}
@@ -120,8 +128,6 @@ internal sealed class DocumentCacheService : IDocumentCacheService
public async Task SeedAsync(CancellationToken cancellationToken)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
foreach (Guid key in SeedKeys)
{
if(cancellationToken.IsCancellationRequested)
@@ -131,31 +137,32 @@ internal sealed class DocumentCacheService : IDocumentCacheService
var cacheKey = GetCacheKey(key, false);
// We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
cacheKey,
async cancel =>
{
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
// We don't want to seed drafts
if (cacheNode is null || cacheNode.IsDraft)
// We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed.
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
cacheKey,
async cancel =>
{
return null;
}
using ICoreScope scope = _scopeProvider.CreateCoreScope();
return cacheNode;
},
GetSeedEntryOptions());
ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false);
// If the value is null, it's likely because
if (cachedValue is null)
scope.Complete();
// We don't want to seed drafts
if (cacheNode is null || cacheNode.IsDraft)
{
return null;
}
return cacheNode;
},
GetSeedEntryOptions());
// If the value is null, it's likely because
if (cachedValue is null)
{
await _hybridCache.RemoveAsync(cacheKey);
}
}
scope.Complete();
}
private HybridCacheEntryOptions GetSeedEntryOptions() => new()
@@ -257,6 +264,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService
using ICoreScope scope = _scopeProvider.CreateCoreScope();
_databaseCacheRepository.Rebuild(contentTypeIds.ToList());
IEnumerable<ContentCacheNode> contentByContentTypeKey = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeIds.Select(x => _idKeyMap.GetKeyForId(x, UmbracoObjectTypes.DocumentType).Result), ContentCacheDataSerializerEntityType.Document);
scope.Complete();
foreach (ContentCacheNode content in contentByContentTypeKey)
{
@@ -268,6 +276,6 @@ internal sealed class DocumentCacheService : IDocumentCacheService
}
}
scope.Complete();
}
}

View File

@@ -75,13 +75,16 @@ internal class MediaCacheService : IMediaCacheService
return null;
}
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
$"{key}", // Unique key to the cache entry
async cancel => await _databaseCacheRepository.GetMediaSourceAsync(idAttempt.Result));
async cancel =>
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(idAttempt.Result);
scope.Complete();
return mediaCacheNode;
});
scope.Complete();
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
}
@@ -93,11 +96,16 @@ internal class MediaCacheService : IMediaCacheService
return null;
}
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync(
$"{keyAttempt.Result}", // Unique key to the cache entry
async cancel => await _databaseCacheRepository.GetMediaSourceAsync(id));
scope.Complete();
async cancel =>
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(id);
scope.Complete();
return mediaCacheNode;
});
return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode).CreateModel(_publishedModelFactory);
}
@@ -144,7 +152,6 @@ internal class MediaCacheService : IMediaCacheService
public async Task SeedAsync(CancellationToken cancellationToken)
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
foreach (Guid key in SeedKeys)
{
@@ -157,7 +164,13 @@ internal class MediaCacheService : IMediaCacheService
ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync<ContentCacheNode?>(
cacheKey,
async cancel => await _databaseCacheRepository.GetMediaSourceAsync(key),
async cancel =>
{
using ICoreScope scope = _scopeProvider.CreateCoreScope();
ContentCacheNode? mediaCacheNode = await _databaseCacheRepository.GetMediaSourceAsync(key);
scope.Complete();
return mediaCacheNode;
},
GetSeedEntryOptions());
if (cachedValue is null)
@@ -165,8 +178,6 @@ internal class MediaCacheService : IMediaCacheService
await _hybridCache.RemoveAsync(cacheKey);
}
}
scope.Complete();
}
public void Rebuild(IReadOnlyCollection<int> contentTypeIds)

View File

@@ -194,8 +194,8 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory);
builder.Services.AddHostedService<RecurringBackgroundJobHostedServiceRunner>();
builder.Services.AddHostedService<QueuedHostedService>();
builder.Services.AddHostedService<NavigationInitializationHostedService>();
builder.Services.AddHostedService<PublishStatusInitializationHostedService>();
builder.AddNotificationAsyncHandler<PostRuntimePremigrationsUpgradeNotification, NavigationInitializationNotificationHandler>();
builder.AddNotificationAsyncHandler<PostRuntimePremigrationsUpgradeNotification, PublishStatusInitializationNotificationHandler>();
return builder;
}

View File

@@ -172,7 +172,7 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
.AddCoreMappingProfiles();
}
services.RemoveAll(x=>x.ImplementationType == typeof(DocumentUrlServiceInitializer));
services.RemoveAll(x=>x.ImplementationType == typeof(DocumentUrlServiceInitializerNotificationHandler));
services.AddSignalR();
services.AddMvc();

View File

@@ -35,7 +35,7 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
protected ContentType ContentType { get; private set; }
[SetUp]
public void Setup() => CreateTestData();
public virtual void Setup() => CreateTestData();
public virtual void CreateTestData()
{

View File

@@ -1,23 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Handlers;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Infrastructure.DependencyInjection;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
using Umbraco.Cms.Infrastructure.HostedServices;
using Umbraco.Cms.Infrastructure.Search;
using Umbraco.Cms.Tests.Common.Attributes;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
@@ -33,12 +23,15 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent
builder.Services.AddUnique<IServerMessenger, ScopedRepositoryTests.LocalServerMessenger>();
builder.AddNotificationHandler<ContentTreeChangeNotification, ContentTreeChangeDistributedCacheNotificationHandler>();
builder.Services.AddHostedService<DocumentUrlServiceInitializer>();
builder.Services.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, DocumentUrlServiceInitializerNotificationHandler>();
}
public override void Setup()
{
DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
base.Setup();
}
//
// [Test]
// [LongRunning]

View File

@@ -29,8 +29,14 @@ public class DocumentUrlServiceTest_HideTopLevel_False : UmbracoIntegrationTestW
builder.Services.AddUnique<IServerMessenger, ScopedRepositoryTests.LocalServerMessenger>();
builder.AddNotificationHandler<ContentTreeChangeNotification, ContentTreeChangeDistributedCacheNotificationHandler>();
builder.Services.AddHostedService<DocumentUrlServiceInitializer>();
}
public override void Setup()
{
DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
base.Setup();
}
[Test]
[TestCase("/textpage/", "en-US", true, ExpectedResult = TextpageKey)]
[TestCase("/textpage/text-page-1", "en-US", true, ExpectedResult = SubPageKey)]

View File

@@ -5,13 +5,11 @@ using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
using Umbraco.Cms.Infrastructure.HostedServices;
@@ -19,8 +17,6 @@ using Umbraco.Cms.Infrastructure.Search;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping;
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
using Umbraco.Cms.Web.Common.Security;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
@@ -37,7 +33,9 @@ public class BackOfficeExamineSearcherTests : ExamineBaseTest
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = Services;
Mock.Get(TestHelper.GetHttpContextAccessor()).Setup(x => x.HttpContext).Returns(httpContext);
}
DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
}
[TearDown]
public void TearDown()
@@ -48,6 +46,7 @@ public class BackOfficeExamineSearcherTests : ExamineBaseTest
Services.DisposeIfDisposable();
}
private IDocumentUrlService DocumentUrlService => GetRequiredService<IDocumentUrlService>();
private IBackOfficeExamineSearcher BackOfficeExamineSearcher => GetRequiredService<IBackOfficeExamineSearcher>();
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();

View File

@@ -44,9 +44,8 @@ public abstract class ExamineBaseTest : UmbracoIntegrationTest
builder
.AddNotificationHandler<ContentTreeChangeNotification,
ContentTreeChangeDistributedCacheNotificationHandler>();
builder.Services.AddHostedService<DocumentUrlServiceInitializer>();
}
/// <summary>
/// Used to create and manage a testable index
/// </summary>

View File

@@ -37,6 +37,8 @@ public class ExamineExternalIndexTests : ExamineBaseTest
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = Services;
Mock.Get(TestHelper.GetHttpContextAccessor()).Setup(x => x.HttpContext).Returns(httpContext);
DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
}
[TearDown]
@@ -52,6 +54,7 @@ public class ExamineExternalIndexTests : ExamineBaseTest
private IExamineExternalIndexSearcherTest ExamineExternalIndexSearcher =>
GetRequiredService<IExamineExternalIndexSearcherTest>();
private IDocumentUrlService DocumentUrlService => GetRequiredService<IDocumentUrlService>();
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
private ContentService ContentService => (ContentService)GetRequiredService<IContentService>();

View File

@@ -3,6 +3,7 @@ using Examine;
using Lucene.Net.Util;
using NUnit.Framework;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Tests.Common.Attributes;
using Umbraco.Cms.Tests.Common.Builders;
@@ -18,6 +19,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
public class IndexTest : ExamineBaseTest
{
private IDocumentUrlService DocumentUrlService => GetRequiredService<IDocumentUrlService>();
[SetUp]
public void Setup()
{
DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
}
[Test]
[LongRunning]
public void GivenValidationParentNode_WhenContentIndexedUnderDifferentParent_DocumentIsNotIndexed()

View File

@@ -18,6 +18,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)]
public class SearchTests : ExamineBaseTest
{
private IDocumentUrlService DocumentUrlService => GetRequiredService<IDocumentUrlService>();
[SetUp]
public void Setup()
{
DocumentUrlService.InitAsync(false, CancellationToken.None).GetAwaiter().GetResult();
}
[Test]
[LongRunning]
public void Test_Sort_Order_Sorting()

View File

@@ -52,14 +52,14 @@ public class MigrationTests
public ISqlContext SqlContext { get; set; }
#if DEBUG_SCOPES
public ScopeInfo GetScopeInfo(IScope scope)
{
throw new NotImplementedException();
}
public IEnumerable<ScopeInfo> ScopeInfos => throw new NotImplementedException();
#endif
public IScope AmbientScope { get; }
#if DEBUG_SCOPES
public IEnumerable<ScopeInfo> ScopeInfos => throw new NotImplementedException();
public ScopeInfo GetScopeInfo(IScope scope) => throw new NotImplementedException();
#endif
}
private class TestPlan : MigrationPlan