diff --git a/VirtoCommerce.Platform.sln b/VirtoCommerce.Platform.sln index 5fae0912b00..8fbbdd7225a 100644 --- a/VirtoCommerce.Platform.sln +++ b/VirtoCommerce.Platform.sln @@ -50,6 +50,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VirtoCommerce.Platform.Data EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtoCommerce.Platform.Data.SqlServer", "src\VirtoCommerce.Platform.Data.SqlServer\VirtoCommerce.Platform.Data.SqlServer.csproj", "{6B11F73A-4AF3-4EEF-99EE-A4B39134AF9C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExtendedServiceProvider", "src\ExtendedServiceProvider\ExtendedServiceProvider.csproj", "{DB5AF8C4-C55D-1E90-8064-F217DA5FC908}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -122,6 +124,10 @@ Global {6B11F73A-4AF3-4EEF-99EE-A4B39134AF9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B11F73A-4AF3-4EEF-99EE-A4B39134AF9C}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B11F73A-4AF3-4EEF-99EE-A4B39134AF9C}.Release|Any CPU.Build.0 = Release|Any CPU + {DB5AF8C4-C55D-1E90-8064-F217DA5FC908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB5AF8C4-C55D-1E90-8064-F217DA5FC908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB5AF8C4-C55D-1E90-8064-F217DA5FC908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB5AF8C4-C55D-1E90-8064-F217DA5FC908}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -143,6 +149,7 @@ Global {9E100C75-7490-4A14-B74A-0F83CCA0C2A5} = {90F6E3EE-475A-4844-A4C4-078613591F81} {4826F1CB-84E6-4932-9B61-B8AF467C3A4A} = {90F6E3EE-475A-4844-A4C4-078613591F81} {6B11F73A-4AF3-4EEF-99EE-A4B39134AF9C} = {90F6E3EE-475A-4844-A4C4-078613591F81} + {DB5AF8C4-C55D-1E90-8064-F217DA5FC908} = {90F6E3EE-475A-4844-A4C4-078613591F81} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {01A94F38-27D1-412F-A4F4-F00991D1157C} diff --git a/src/ExtendedServiceProvider/ExtendedServiceProvider.cs b/src/ExtendedServiceProvider/ExtendedServiceProvider.cs new file mode 100644 index 00000000000..0525ec99ec0 --- /dev/null +++ b/src/ExtendedServiceProvider/ExtendedServiceProvider.cs @@ -0,0 +1,93 @@ +using System.Collections; +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ExtendedServiceProvider +{ + public interface IExtendedServiceProvider : IKeyedServiceProvider, IServiceProviderIsKeyedService, ISupportRequiredService, IServiceScopeFactory; + + [DebuggerDisplay("ServiceDescriptors = {_services.Count}, Extended")] + internal class ExtendedServiceProvider : IExtendedServiceProvider, IEnumerable + { + private readonly IServiceCollection _services; + private readonly ILogger _logger; + private readonly IKeyedServiceProvider _serviceProvider; + private readonly IEnumerable _resolvers; + private readonly IEnumerable _hooks; + private static readonly Type[] _selfTypes = [typeof(IExtendedServiceProvider), typeof(IServiceProvider), typeof(IKeyedServiceProvider), typeof(IServiceProviderIsService), typeof(IServiceProviderIsKeyedService), typeof(ISupportRequiredService), typeof(IServiceScopeFactory)]; + + public ExtendedServiceProvider(IServiceCollection services, ServiceProviderOptions? options = null, IServiceProviderResolver? resolver = null) + { + ArgumentNullException.ThrowIfNull(services); + _services = services; + _serviceProvider = services.BuildServiceProvider(options ?? ExtendedServiceProviderFactory.DefaultServiceProviderOptions); + _serviceProvider = (_serviceProvider.CreateScope().ServiceProvider as IKeyedServiceProvider)!; + _logger = _serviceProvider.GetRequiredService>(); + var resolvers = _serviceProvider.GetServices(); + _resolvers = (resolver != null) ? resolvers.Concat([resolver]) : resolvers; + _hooks = _serviceProvider.GetServices(); + } + + public object? GetKeyedService(Type serviceType, object? serviceKey) + { + ArgumentNullException.ThrowIfNull(serviceType); + + _logger.LogDebug("GetKeyedService: serviceType = {serviceType}, serviceKey = {serviceKey}", serviceType, serviceKey); + + if (_selfTypes.Contains(serviceType) && serviceKey is null) + { + _logger.LogDebug("GetKeyedService: asking for {serviceType}, returning self", serviceType); + return this; + } + + var service = ResolveKeyedService(serviceType, serviceKey) ?? _serviceProvider.GetKeyedService(serviceType, serviceKey); + + if (service != null) + { + foreach (var hook in _hooks) + { + hook.ServiceResolved(serviceType, service); + } + } + + return service; + } + + private object? ResolveKeyedService(Type serviceType, object? serviceKey) + { + object? service = null; + + foreach (var resolver in _resolvers) + { + service = resolver.Resolve(_serviceProvider, serviceType, serviceKey); + if (service != null) + { + break; + } + } + + return service; + } + + public object GetRequiredKeyedService(Type serviceType, object? serviceKey) + { + var service = GetKeyedService(serviceType, serviceKey); + return service ?? throw new InvalidOperationException($"No service for type '{serviceType}' has been registered."); + } + + public object? GetService(Type serviceType) => GetKeyedService(serviceType, null); + + public object GetRequiredService(Type serviceType) => GetRequiredKeyedService(serviceType, null); + + public bool IsService(Type serviceType) => IsKeyedService(serviceType, null); + + public bool IsKeyedService(Type serviceType, object? serviceKey) => _services.Any(x => x.ServiceType == serviceType && x.ServiceKey == serviceKey); + + public IServiceScope CreateScope() => _serviceProvider.CreateScope(); + + public IEnumerator GetEnumerator() => _services.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _services.GetEnumerator(); + } +} diff --git a/src/ExtendedServiceProvider/ExtendedServiceProvider.csproj b/src/ExtendedServiceProvider/ExtendedServiceProvider.csproj new file mode 100644 index 00000000000..e38430edc31 --- /dev/null +++ b/src/ExtendedServiceProvider/ExtendedServiceProvider.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + ExtendedServiceProvider + Ricardo Peres + Ricardo Peres + https://github.com/rjperes/ExtendedServiceProvider + + + + + + + + + + + + + + + + diff --git a/src/ExtendedServiceProvider/ExtendedServiceProviderFactory.cs b/src/ExtendedServiceProvider/ExtendedServiceProviderFactory.cs new file mode 100644 index 00000000000..075102c75ca --- /dev/null +++ b/src/ExtendedServiceProvider/ExtendedServiceProviderFactory.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ExtendedServiceProvider +{ + internal class ExtendedServiceProviderFactory : IServiceProviderFactory + { + internal static readonly ServiceProviderOptions DefaultServiceProviderOptions = new() { ValidateOnBuild = true, ValidateScopes = true }; + + private IServiceProvider? _serviceProvider; + private readonly IServiceProviderResolver? _resolver; + private readonly ServiceProviderOptions? _options; + + public ExtendedServiceProviderFactory(ServiceProviderOptions? options = null, IServiceProviderResolver? resolver = null) + { + _options = options; + _resolver = resolver; + } + + public IServiceCollection CreateBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + return services; + } + + public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder) + { + ArgumentNullException.ThrowIfNull(containerBuilder); + _serviceProvider ??= new ExtendedServiceProvider(containerBuilder, _options, _resolver); + return _serviceProvider; + } + } +} diff --git a/src/ExtendedServiceProvider/ExtendedServiceProvidersFeatureFilter.cs b/src/ExtendedServiceProvider/ExtendedServiceProvidersFeatureFilter.cs new file mode 100644 index 00000000000..921f1fd0194 --- /dev/null +++ b/src/ExtendedServiceProvider/ExtendedServiceProvidersFeatureFilter.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; + +namespace ExtendedServiceProvider +{ + public class ExtendedServiceProvidersFeatureFilter : IStartupFilter, IServiceProvidersFeature + { + public ExtendedServiceProvidersFeatureFilter(IServiceProvider serviceProvider) + { + RequestServices = serviceProvider; + } + + public IServiceProvider RequestServices { get; set; } + + public Action Configure(Action next) + { + return app => + { + app.Use(async (context, nxt) => + { + context.Features.Set(this); + await nxt(context); + }); + next(app); + }; + } + } +} diff --git a/src/ExtendedServiceProvider/GenericLazy.cs b/src/ExtendedServiceProvider/GenericLazy.cs new file mode 100644 index 00000000000..af9c0dcec35 --- /dev/null +++ b/src/ExtendedServiceProvider/GenericLazy.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ExtendedServiceProvider +{ + public class GenericLazy : Lazy + where T : notnull + { + public GenericLazy() { } + public GenericLazy(bool isThreadSafe) : base(isThreadSafe) { } + public GenericLazy(Func valueFactory) : base(valueFactory) { } + public GenericLazy(LazyThreadSafetyMode mode) : base(mode) { } + public GenericLazy(T value) : base(value) { } + public GenericLazy(Func valueFactory, bool isThreadSafe) : base(valueFactory, isThreadSafe) { } + public GenericLazy(Func valueFactory, LazyThreadSafetyMode mode) : base(valueFactory, mode) { } + public GenericLazy(IServiceProvider serviceProvider) : this(() => serviceProvider.GetRequiredService()) { } + + public static implicit operator T(GenericLazy value) => value.Value; + } +} diff --git a/src/ExtendedServiceProvider/HostBuilderExtensions.cs b/src/ExtendedServiceProvider/HostBuilderExtensions.cs new file mode 100644 index 00000000000..fd38ce118ee --- /dev/null +++ b/src/ExtendedServiceProvider/HostBuilderExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace ExtendedServiceProvider +{ + public static class HostBuilderExtensions + { + public static IHostBuilder UseExtendedServiceProvider(this IHostBuilder builder, ServiceProviderOptions? options = null, IServiceProviderResolver? resolver = null) + { + ArgumentNullException.ThrowIfNull(builder); + var serviceProviderFactory = new ExtendedServiceProviderFactory(options ?? ExtendedServiceProviderFactory.DefaultServiceProviderOptions, resolver); + return builder + .UseServiceProviderFactory(serviceProviderFactory) + .ConfigureServices((ctx, services) => + { + services.AddSingleton(_ => + { + var serviceProvider = serviceProviderFactory.CreateServiceProvider(services); + return new ExtendedServiceProvidersFeatureFilter(serviceProvider); + }); + }); + } + } +} diff --git a/src/ExtendedServiceProvider/IServiceProviderHook.cs b/src/ExtendedServiceProvider/IServiceProviderHook.cs new file mode 100644 index 00000000000..6ab6395ff08 --- /dev/null +++ b/src/ExtendedServiceProvider/IServiceProviderHook.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ExtendedServiceProvider +{ + public interface IServiceProviderHook + { + void ServiceResolved(Type serviceType, object service); + } + + public sealed class LoggerServiceProviderHook : IServiceProviderHook + { + private readonly ILogger _logger; + + public LoggerServiceProviderHook(ILogger logger) + { + _logger = logger; + } + + public void ServiceResolved(Type serviceType, object service) + { + _logger.LogDebug("ServiceResolved: serviceType = {serviceType}, service = {service}", serviceType, service); + } + } + + public sealed class PropertyInitializerServiceProviderHook : IServiceProviderHook + { + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public PropertyInitializerServiceProviderHook(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _logger = serviceProvider.GetRequiredService>(); + } + + public void ServiceResolved(Type serviceType, object service) + { + foreach (var prop in service.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.GetProperty)) + { + var injectPropertyAttribute = prop.GetCustomAttribute(); + + if (injectPropertyAttribute != null && prop.GetValue(service) == null) + { + _logger.LogDebug("ServiceResolved: serviceType = {serviceType}, setting {propertyName}", serviceType, prop.Name); + var propertyService = _serviceProvider.GetRequiredKeyedService(prop.PropertyType, injectPropertyAttribute.ServiceKey); + prop.SetValue(service, propertyService); + } + } + } + } +} diff --git a/src/ExtendedServiceProvider/IServiceProviderResolver.cs b/src/ExtendedServiceProvider/IServiceProviderResolver.cs new file mode 100644 index 00000000000..7f6bb86220d --- /dev/null +++ b/src/ExtendedServiceProvider/IServiceProviderResolver.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ExtendedServiceProvider +{ + public interface IServiceProviderResolver + { + object? Resolve(IKeyedServiceProvider serviceProvider, Type serviceType, object? serviceKey); + } +} diff --git a/src/ExtendedServiceProvider/InjectAttribute.cs b/src/ExtendedServiceProvider/InjectAttribute.cs new file mode 100644 index 00000000000..b59c4391581 --- /dev/null +++ b/src/ExtendedServiceProvider/InjectAttribute.cs @@ -0,0 +1,24 @@ +namespace ExtendedServiceProvider +{ + [Serializable] + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class InjectAttribute : Attribute + { + public object? ServiceKey { get; init; } + + public override bool Equals(object? obj) => Match(obj); + + public override bool Match(object? obj) + { + if (obj is InjectAttribute attribute) + { + return object.Equals(ServiceKey, attribute.ServiceKey); + } + return false; + } + + public override int GetHashCode() => ServiceKey?.GetHashCode() ?? 0; + + public override bool IsDefaultAttribute() => ServiceKey == null; + } +} diff --git a/src/ExtendedServiceProvider/ServiceCollectionExtensions.cs b/src/ExtendedServiceProvider/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..373e71139f9 --- /dev/null +++ b/src/ExtendedServiceProvider/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ExtendedServiceProvider +{ + public static class ServiceCollectionExtensions + { + public static IServiceProvider BuildExtendedServiceProvider(this IServiceCollection services, ServiceProviderOptions? options = null, IServiceProviderResolver? resolver = null) + { + ArgumentNullException.ThrowIfNull(services); + return new ExtendedServiceProvider(services, options, resolver); + } + + public static IServiceCollection AddServiceProviderHook(this IServiceCollection services) where THook : class, IServiceProviderHook + { + ArgumentNullException.ThrowIfNull(services); + return services.AddSingleton(); + } + + public static IServiceCollection AddServiceProviderResolver(this IServiceCollection services) where TResolver : class, IServiceProviderResolver + { + ArgumentNullException.ThrowIfNull(services); + return services.AddSingleton(); + } + } +} diff --git a/src/ExtendedServiceProvider/ServiceProviderExtensions.cs b/src/ExtendedServiceProvider/ServiceProviderExtensions.cs new file mode 100644 index 00000000000..19bfc3b0b6d --- /dev/null +++ b/src/ExtendedServiceProvider/ServiceProviderExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace ExtendedServiceProvider +{ + public static class ServiceProviderExtensions + { + public static Lazy GetLazyService(this IServiceProvider serviceProvider) where T : class + { + return new GenericLazy(() => serviceProvider.GetService()!); + } + + public static Lazy GetLazyKeyedService(this IServiceProvider serviceProvider, object serviceKey) where T : class + { + return new GenericLazy(() => serviceProvider.GetKeyedService(serviceKey)!); + } + + public static Lazy GetLazyRequiredService(this IServiceProvider serviceProvider) where T : class + { + return new GenericLazy(() => serviceProvider.GetRequiredService()); + } + + public static Lazy GetLazyRequiredKeyedService(this IServiceProvider serviceProvider, object serviceKey) where T : class + { + return new GenericLazy(() => serviceProvider.GetRequiredKeyedService(serviceKey)); + } + } +} diff --git a/src/VirtoCommerce.Platform.Security/Authorization/PermissionAuthorizationHandlerBase.cs b/src/VirtoCommerce.Platform.Security/Authorization/PermissionAuthorizationHandlerBase.cs index 26289e778db..9492ba505b4 100644 --- a/src/VirtoCommerce.Platform.Security/Authorization/PermissionAuthorizationHandlerBase.cs +++ b/src/VirtoCommerce.Platform.Security/Authorization/PermissionAuthorizationHandlerBase.cs @@ -2,7 +2,9 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using ExtendedServiceProvider; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; using VirtoCommerce.Platform.Core; using VirtoCommerce.Platform.Core.Security; @@ -10,8 +12,13 @@ namespace VirtoCommerce.Platform.Security.Authorization { public abstract class PermissionAuthorizationHandlerBase : AuthorizationHandler where TRequirement : PermissionAuthorizationRequirement { + [Inject] + public ILogger> Logger { get; set; } + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement) { + Logger?.LogInformation("Property injection: {Method}", "PermissionAuthorizationHandlerBase.HandleRequirementAsync()"); + var limitedPermissionsClaim = context.User.FindFirstValue(PlatformConstants.Security.Claims.LimitedPermissionsClaimType); // LimitedPermissions claims that will be granted to the user by cookies when bearer token authentication is enabled. diff --git a/src/VirtoCommerce.Platform.Security/VirtoCommerce.Platform.Security.csproj b/src/VirtoCommerce.Platform.Security/VirtoCommerce.Platform.Security.csproj index 55e2b969c46..8cfc1795e39 100644 --- a/src/VirtoCommerce.Platform.Security/VirtoCommerce.Platform.Security.csproj +++ b/src/VirtoCommerce.Platform.Security/VirtoCommerce.Platform.Security.csproj @@ -28,6 +28,7 @@ + diff --git a/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs b/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs index 29137cd116f..730a7b5c25e 100644 --- a/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs +++ b/src/VirtoCommerce.Platform.Web/Controllers/Api/SecurityController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using ExtendedServiceProvider; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -79,6 +80,9 @@ public SecurityController( _externalSigninProviderConfigs = externalSigninProviderConfigs; } + [Inject] + public ILogger Logger { get; set; } + private UserManager UserManager => _signInManager.UserManager; private readonly string UserNotFound = "User not found."; @@ -333,6 +337,8 @@ public async Task> UpdateRole([FromBody] Role role) [Authorize(PlatformPermissions.SecurityQuery)] public async Task> SearchUsers([FromBody] UserSearchCriteria criteria) { + Logger?.LogInformation("Property injection: {Method}", "SecurityController.SearchUsers()"); + var result = await _userSearchService.SearchUsersAsync(criteria); result.Results = ReduceUsersDetails(result.Results); diff --git a/src/VirtoCommerce.Platform.Web/Program.cs b/src/VirtoCommerce.Platform.Web/Program.cs index 3aecc066bf3..1d480af4b50 100644 --- a/src/VirtoCommerce.Platform.Web/Program.cs +++ b/src/VirtoCommerce.Platform.Web/Program.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using ExtendedServiceProvider; using Hangfire; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -19,67 +20,70 @@ public static void Main(string[] args) } public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureLogging((_, logging) => - { - logging.ClearProviders(); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - webBuilder.ConfigureKestrel((_, options) => { options.Limits.MaxRequestBodySize = null; }); - - webBuilder.ConfigureAppConfiguration((context, configurationBuilder) => + Host.CreateDefaultBuilder(args) + .UseExtendedServiceProvider() + .ConfigureLogging((_, logging) => + { + logging.ClearProviders(); + }) + .ConfigureWebHostDefaults(webBuilder => { - var configuration = configurationBuilder.Build(); + webBuilder.UseStartup(); + webBuilder.ConfigureKestrel((_, options) => { options.Limits.MaxRequestBodySize = null; }); - // Load configuration from Azure App Configuration - // Azure App Configuration will be loaded last i.e. it will override any existing sections - // configuration loads all keys that have no label and keys that have label based on the environment (Development, Production etc.) - if (configuration.TryGetAzureAppConfigurationConnectionString(out var connectionString)) + webBuilder.ConfigureAppConfiguration((context, configurationBuilder) => { - configurationBuilder.AddAzureAppConfiguration(options => + var configuration = configurationBuilder.Build(); + + // Load configuration from Azure App Configuration + // Azure App Configuration will be loaded last i.e. it will override any existing sections + // configuration loads all keys that have no label and keys that have label based on the environment (Development, Production etc.) + if (configuration.TryGetAzureAppConfigurationConnectionString(out var connectionString)) { - options - .Connect(connectionString) - .Select(KeyFilter.Any) - .Select(KeyFilter.Any, context.HostingEnvironment.EnvironmentName) - .ConfigureRefresh(refreshOptions => + configurationBuilder.AddAzureAppConfiguration(options => { - // Reload all configuration values if the "Sentinel" key value is modified - refreshOptions.Register("Sentinel", refreshAll: true); + options + .Connect(connectionString) + .Select(KeyFilter.Any) + .Select(KeyFilter.Any, context.HostingEnvironment.EnvironmentName) + .ConfigureRefresh(refreshOptions => + { + // Reload all configuration values if the "Sentinel" key value is modified + refreshOptions.Register("Sentinel", refreshAll: true); + }); }); - }); - } - }); + } + }); - }) - .ConfigureServices((hostingContext, services) => - { - //Conditionally use the hangFire server for this app instance to have possibility to disable processing background jobs - if (hostingContext.Configuration.GetValue("VirtoCommerce:Hangfire:UseHangfireServer", true)) + }) + .ConfigureServices((hostingContext, services) => { - // Add & start hangfire server immediately. - // We do this there after all services initialize, to have dependencies in hangfire tasks correctly resolved. - // Hangfire uses the ASP.NET HostedServices to host job background processing tasks. - // According to the official documentation https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#ihostedservice-interface, - // in order to change running hosted services after the app's pipeline, we need to place AddHangfireServer here instead of Startup. - services.AddHangfireServer(options => + services.AddServiceProviderHook(); + + //Conditionally use the hangFire server for this app instance to have possibility to disable processing background jobs + if (hostingContext.Configuration.GetValue("VirtoCommerce:Hangfire:UseHangfireServer", true)) { - var queues = hostingContext.Configuration.GetSection("VirtoCommerce:Hangfire:Queues").Get>(); - if (!queues.IsNullOrEmpty()) + // Add & start hangfire server immediately. + // We do this there after all services initialize, to have dependencies in hangfire tasks correctly resolved. + // Hangfire uses the ASP.NET HostedServices to host job background processing tasks. + // According to the official documentation https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#ihostedservice-interface, + // in order to change running hosted services after the app's pipeline, we need to place AddHangfireServer here instead of Startup. + services.AddHangfireServer(options => { - queues.Add("default"); - options.Queues = queues.Select(x => x.ToLower()).Distinct().ToArray(); - } + var queues = hostingContext.Configuration.GetSection("VirtoCommerce:Hangfire:Queues").Get>(); + if (!queues.IsNullOrEmpty()) + { + queues.Add("default"); + options.Queues = queues.Select(x => x.ToLower()).Distinct().ToArray(); + } - var workerCount = hostingContext.Configuration.GetValue("VirtoCommerce:Hangfire:WorkerCount", null); - if (workerCount != null) - { - options.WorkerCount = workerCount.Value; - } - }); - } - }); + var workerCount = hostingContext.Configuration.GetValue("VirtoCommerce:Hangfire:WorkerCount", null); + if (workerCount != null) + { + options.WorkerCount = workerCount.Value; + } + }); + } + }); } }