diff --git a/src/JsonApiDotNetCore/Configuration/InjectablesAssemblyScanner.cs b/src/JsonApiDotNetCore/Configuration/InjectablesAssemblyScanner.cs new file mode 100644 index 0000000000..9985533776 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/InjectablesAssemblyScanner.cs @@ -0,0 +1,129 @@ +using System.Reflection; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace JsonApiDotNetCore.Configuration; + +/// +/// Scans assemblies for injectables (types that implement , +/// or ) and registers them in the IoC container. +/// +internal sealed class InjectablesAssemblyScanner +{ + internal static readonly HashSet ServiceUnboundInterfaces = + [ + typeof(IResourceService<,>), + typeof(IResourceCommandService<,>), + typeof(IResourceQueryService<,>), + typeof(IGetAllService<,>), + typeof(IGetByIdService<,>), + typeof(IGetSecondaryService<,>), + typeof(IGetRelationshipService<,>), + typeof(ICreateService<,>), + typeof(IAddToRelationshipService<,>), + typeof(IUpdateService<,>), + typeof(ISetRelationshipService<,>), + typeof(IDeleteService<,>), + typeof(IRemoveFromRelationshipService<,>) + ]; + + internal static readonly HashSet RepositoryUnboundInterfaces = + [ + typeof(IResourceRepository<,>), + typeof(IResourceWriteRepository<,>), + typeof(IResourceReadRepository<,>) + ]; + + internal static readonly HashSet ResourceDefinitionUnboundInterfaces = [typeof(IResourceDefinition<,>)]; + + private readonly ResourceDescriptorAssemblyCache _assemblyCache; + private readonly IServiceCollection _services; + private readonly TypeLocator _typeLocator = new(); + + public InjectablesAssemblyScanner(ResourceDescriptorAssemblyCache assemblyCache, IServiceCollection services) + { + ArgumentGuard.NotNull(assemblyCache); + ArgumentGuard.NotNull(services); + + _assemblyCache = assemblyCache; + _services = services; + } + + public void DiscoverInjectables() + { + IReadOnlyCollection descriptors = _assemblyCache.GetResourceDescriptors(); + IReadOnlyCollection assemblies = _assemblyCache.GetAssemblies(); + + foreach (Assembly assembly in assemblies) + { + AddDbContextResolvers(assembly); + AddInjectables(descriptors, assembly); + } + } + + private void AddDbContextResolvers(Assembly assembly) + { + IEnumerable dbContextTypes = _typeLocator.GetDerivedTypes(assembly, typeof(DbContext)); + + foreach (Type dbContextType in dbContextTypes) + { + Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType); + } + } + + private void AddInjectables(IEnumerable resourceDescriptors, Assembly assembly) + { + foreach (ResourceDescriptor resourceDescriptor in resourceDescriptors) + { + AddServices(assembly, resourceDescriptor); + AddRepositories(assembly, resourceDescriptor); + AddResourceDefinitions(assembly, resourceDescriptor); + } + } + + private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) + { + foreach (Type serviceUnboundInterface in ServiceUnboundInterfaces) + { + RegisterImplementations(assembly, serviceUnboundInterface, resourceDescriptor); + } + } + + private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) + { + foreach (Type repositoryUnboundInterface in RepositoryUnboundInterfaces) + { + RegisterImplementations(assembly, repositoryUnboundInterface, resourceDescriptor); + } + } + + private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resourceDescriptor) + { + foreach (Type resourceDefinitionUnboundInterface in ResourceDefinitionUnboundInterfaces) + { + RegisterImplementations(assembly, resourceDefinitionUnboundInterface, resourceDescriptor); + } + } + + private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) + { + Type[] typeArguments = + [ + resourceDescriptor.ResourceClrType, + resourceDescriptor.IdClrType + ]; + + (Type implementationType, Type serviceInterface)? result = _typeLocator.GetContainerRegistrationFromAssembly(assembly, interfaceType, typeArguments); + + if (result != null) + { + (Type implementationType, Type serviceInterface) = result.Value; + _services.TryAddScoped(serviceInterface, implementationType); + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 17ca6677c3..2004178ccd 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -23,17 +23,15 @@ namespace JsonApiDotNetCore.Configuration; /// -/// A utility class that builds a JsonApi application. It registers all required services and allows the user to override parts of the startup +/// A utility class that builds a JSON:API application. It registers all required services and allows the user to override parts of the startup /// configuration. /// -internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, IDisposable +internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder { - private readonly JsonApiOptions _options = new(); private readonly IServiceCollection _services; private readonly IMvcCoreBuilder _mvcBuilder; - private readonly ResourceGraphBuilder _resourceGraphBuilder; - private readonly ServiceDiscoveryFacade _serviceDiscoveryFacade; - private readonly ServiceProvider _intermediateProvider; + private readonly JsonApiOptions _options = new(); + private readonly ResourceDescriptorAssemblyCache _assemblyCache = new(); public Action? ConfigureMvcOptions { get; set; } @@ -44,12 +42,6 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv _services = services; _mvcBuilder = mvcBuilder; - _intermediateProvider = services.BuildServiceProvider(); - - var loggerFactory = _intermediateProvider.GetRequiredService(); - - _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); - _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory); } /// @@ -61,35 +53,51 @@ public void ConfigureJsonApiOptions(Action? configureOptions) } /// - /// Executes the action provided by the user to configure . + /// Executes the action provided by the user to configure auto-discovery. /// public void ConfigureAutoDiscovery(Action? configureAutoDiscovery) { - configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade); + if (configureAutoDiscovery != null) + { + var facade = new ServiceDiscoveryFacade(_assemblyCache); + configureAutoDiscovery.Invoke(facade); + } } /// - /// Configures and builds the resource graph with resources from the provided sources and adds it to the DI container. + /// Configures and builds the resource graph with resources from the provided sources and adds them to the IoC container. /// public void ConfigureResourceGraph(ICollection dbContextTypes, Action? configureResourceGraph) { ArgumentGuard.NotNull(dbContextTypes); - _serviceDiscoveryFacade.DiscoverResources(); - - foreach (Type dbContextType in dbContextTypes) + _services.TryAddSingleton(serviceProvider => { - var dbContext = (DbContext)_intermediateProvider.GetRequiredService(dbContextType); - _resourceGraphBuilder.Add(dbContext); - } + var loggerFactory = serviceProvider.GetRequiredService(); + var resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); - configureResourceGraph?.Invoke(_resourceGraphBuilder); + var scanner = new ResourcesAssemblyScanner(_assemblyCache, resourceGraphBuilder); + scanner.DiscoverResources(); - IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); + if (dbContextTypes.Count > 0) + { + using IServiceScope scope = serviceProvider.CreateScope(); - _options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); + foreach (Type dbContextType in dbContextTypes) + { + var dbContext = (DbContext)scope.ServiceProvider.GetRequiredService(dbContextType); + resourceGraphBuilder.Add(dbContext); + } + } + + configureResourceGraph?.Invoke(resourceGraphBuilder); + + IResourceGraph resourceGraph = resourceGraphBuilder.Build(); + + _options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); - _services.TryAddSingleton(resourceGraph); + return resourceGraph; + }); } /// @@ -114,15 +122,16 @@ public void ConfigureMvc() } /// - /// Discovers DI registrable services in the assemblies marked for discovery. + /// Registers injectables in the IoC container found in assemblies marked for auto-discovery. /// public void DiscoverInjectables() { - _serviceDiscoveryFacade.DiscoverInjectables(); + var scanner = new InjectablesAssemblyScanner(_assemblyCache, _services); + scanner.DiscoverInjectables(); } /// - /// Registers the remaining internals. + /// Registers the remaining internals in the IoC container. /// public void ConfigureServiceContainer(ICollection dbContextTypes) { @@ -182,7 +191,7 @@ private void AddMiddlewareLayer() private void AddResourceLayer() { - RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>)); + RegisterImplementationForInterfaces(InjectablesAssemblyScanner.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>)); _services.TryAddScoped(); _services.TryAddScoped(); @@ -190,7 +199,7 @@ private void AddResourceLayer() private void AddRepositoryLayer() { - RegisterImplementationForInterfaces(ServiceDiscoveryFacade.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>)); + RegisterImplementationForInterfaces(InjectablesAssemblyScanner.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>)); _services.TryAddScoped(); @@ -204,7 +213,7 @@ private void AddRepositoryLayer() private void AddServiceLayer() { - RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ServiceUnboundInterfaces, typeof(JsonApiResourceService<,>)); + RegisterImplementationForInterfaces(InjectablesAssemblyScanner.ServiceUnboundInterfaces, typeof(JsonApiResourceService<,>)); } private void RegisterImplementationForInterfaces(HashSet unboundInterfaces, Type unboundImplementationType) @@ -291,9 +300,4 @@ private void AddOperationsLayer() _services.TryAddScoped(); _services.TryAddScoped(); } - - public void Dispose() - { - _intermediateProvider.Dispose(); - } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourcesAssemblyScanner.cs b/src/JsonApiDotNetCore/Configuration/ResourcesAssemblyScanner.cs new file mode 100644 index 0000000000..cdfdc4446c --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/ResourcesAssemblyScanner.cs @@ -0,0 +1,29 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.Configuration; + +/// +/// Scans assemblies for types that implement and adds them to the resource graph. +/// +internal sealed class ResourcesAssemblyScanner +{ + private readonly ResourceDescriptorAssemblyCache _assemblyCache; + private readonly ResourceGraphBuilder _resourceGraphBuilder; + + public ResourcesAssemblyScanner(ResourceDescriptorAssemblyCache assemblyCache, ResourceGraphBuilder resourceGraphBuilder) + { + ArgumentGuard.NotNull(assemblyCache); + ArgumentGuard.NotNull(resourceGraphBuilder); + + _assemblyCache = assemblyCache; + _resourceGraphBuilder = resourceGraphBuilder; + } + + public void DiscoverResources() + { + foreach (ResourceDescriptor resourceDescriptor in _assemblyCache.GetResourceDescriptors()) + { + _resourceGraphBuilder.Add(resourceDescriptor.ResourceClrType, resourceDescriptor.IdClrType); + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index b25c208086..5ad4474d95 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -43,7 +43,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< Action? configureAutoDiscovery, Action? configureResources, IMvcCoreBuilder? mvcBuilder, ICollection dbContextTypes) { - using var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); + var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); applicationBuilder.ConfigureJsonApiOptions(configureOptions); applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); @@ -61,7 +61,7 @@ public static IServiceCollection AddResourceService(this IServiceColle { ArgumentGuard.NotNull(services); - RegisterTypeForUnboundInterfaces(services, typeof(TService), ServiceDiscoveryFacade.ServiceUnboundInterfaces); + RegisterTypeForUnboundInterfaces(services, typeof(TService), InjectablesAssemblyScanner.ServiceUnboundInterfaces); return services; } @@ -74,7 +74,7 @@ public static IServiceCollection AddResourceRepository(this IServic { ArgumentGuard.NotNull(services); - RegisterTypeForUnboundInterfaces(services, typeof(TRepository), ServiceDiscoveryFacade.RepositoryUnboundInterfaces); + RegisterTypeForUnboundInterfaces(services, typeof(TRepository), InjectablesAssemblyScanner.RepositoryUnboundInterfaces); return services; } @@ -87,7 +87,7 @@ public static IServiceCollection AddResourceDefinition(this { ArgumentGuard.NotNull(services); - RegisterTypeForUnboundInterfaces(services, typeof(TResourceDefinition), ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces); + RegisterTypeForUnboundInterfaces(services, typeof(TResourceDefinition), InjectablesAssemblyScanner.ResourceDefinitionUnboundInterfaces); return services; } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 01f9ecc7cb..3139930852 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -1,66 +1,23 @@ using System.Reflection; -using JetBrains.Annotations; -using JsonApiDotNetCore.Repositories; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Services; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Configuration; /// -/// Scans for types like resources, services, repositories and resource definitions in an assembly and registers them to the IoC container. +/// Provides auto-discovery by scanning assemblies for resources and related injectables. /// -[PublicAPI] public sealed class ServiceDiscoveryFacade { - internal static readonly HashSet ServiceUnboundInterfaces = - [ - typeof(IResourceService<,>), - typeof(IResourceCommandService<,>), - typeof(IResourceQueryService<,>), - typeof(IGetAllService<,>), - typeof(IGetByIdService<,>), - typeof(IGetSecondaryService<,>), - typeof(IGetRelationshipService<,>), - typeof(ICreateService<,>), - typeof(IAddToRelationshipService<,>), - typeof(IUpdateService<,>), - typeof(ISetRelationshipService<,>), - typeof(IDeleteService<,>), - typeof(IRemoveFromRelationshipService<,>) - ]; + private readonly ResourceDescriptorAssemblyCache _assemblyCache; - internal static readonly HashSet RepositoryUnboundInterfaces = - [ - typeof(IResourceRepository<,>), - typeof(IResourceWriteRepository<,>), - typeof(IResourceReadRepository<,>) - ]; - - internal static readonly HashSet ResourceDefinitionUnboundInterfaces = [typeof(IResourceDefinition<,>)]; - - private readonly ILogger _logger; - private readonly IServiceCollection _services; - private readonly ResourceGraphBuilder _resourceGraphBuilder; - private readonly ResourceDescriptorAssemblyCache _assemblyCache = new(); - private readonly TypeLocator _typeLocator = new(); - - public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, ILoggerFactory loggerFactory) + internal ServiceDiscoveryFacade(ResourceDescriptorAssemblyCache assemblyCache) { - ArgumentGuard.NotNull(services); - ArgumentGuard.NotNull(resourceGraphBuilder); - ArgumentGuard.NotNull(loggerFactory); + ArgumentGuard.NotNull(assemblyCache); - _logger = loggerFactory.CreateLogger(); - _services = services; - _resourceGraphBuilder = resourceGraphBuilder; + _assemblyCache = assemblyCache; } /// - /// Mark the calling assembly for scanning of resources and injectables. + /// Includes the calling assembly for auto-discovery of resources and related injectables. /// public ServiceDiscoveryFacade AddCurrentAssembly() { @@ -68,102 +25,13 @@ public ServiceDiscoveryFacade AddCurrentAssembly() } /// - /// Mark the specified assembly for scanning of resources and injectables. + /// Includes the specified assembly for auto-discovery of resources and related injectables. /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { ArgumentGuard.NotNull(assembly); _assemblyCache.RegisterAssembly(assembly); - _logger.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); - return this; } - - internal void DiscoverResources() - { - foreach (ResourceDescriptor resourceDescriptor in _assemblyCache.GetResourceDescriptors()) - { - AddResource(resourceDescriptor); - } - } - - internal void DiscoverInjectables() - { - IReadOnlyCollection descriptors = _assemblyCache.GetResourceDescriptors(); - IReadOnlyCollection assemblies = _assemblyCache.GetAssemblies(); - - foreach (Assembly assembly in assemblies) - { - AddDbContextResolvers(assembly); - AddInjectables(descriptors, assembly); - } - } - - private void AddInjectables(IReadOnlyCollection resourceDescriptors, Assembly assembly) - { - foreach (ResourceDescriptor resourceDescriptor in resourceDescriptors) - { - AddServices(assembly, resourceDescriptor); - AddRepositories(assembly, resourceDescriptor); - AddResourceDefinitions(assembly, resourceDescriptor); - } - } - - private void AddDbContextResolvers(Assembly assembly) - { - IEnumerable dbContextTypes = _typeLocator.GetDerivedTypes(assembly, typeof(DbContext)); - - foreach (Type dbContextType in dbContextTypes) - { - Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType); - } - } - - private void AddResource(ResourceDescriptor resourceDescriptor) - { - _resourceGraphBuilder.Add(resourceDescriptor.ResourceClrType, resourceDescriptor.IdClrType); - } - - private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) - { - foreach (Type serviceUnboundInterface in ServiceUnboundInterfaces) - { - RegisterImplementations(assembly, serviceUnboundInterface, resourceDescriptor); - } - } - - private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) - { - foreach (Type repositoryUnboundInterface in RepositoryUnboundInterfaces) - { - RegisterImplementations(assembly, repositoryUnboundInterface, resourceDescriptor); - } - } - - private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resourceDescriptor) - { - foreach (Type resourceDefinitionUnboundInterface in ResourceDefinitionUnboundInterfaces) - { - RegisterImplementations(assembly, resourceDefinitionUnboundInterface, resourceDescriptor); - } - } - - private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) - { - Type[] typeArguments = - [ - resourceDescriptor.ResourceClrType, - resourceDescriptor.IdClrType - ]; - - (Type implementationType, Type serviceInterface)? result = _typeLocator.GetContainerRegistrationFromAssembly(assembly, interfaceType, typeArguments); - - if (result != null) - { - (Type implementationType, Type serviceInterface) = result.Value; - _services.TryAddScoped(serviceInterface, implementationType); - } - } }