Skip to content

Remove building an intermediate service provider at startup #1431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 22, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions src/JsonApiDotNetCore/Configuration/InjectablesAssemblyScanner.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Scans assemblies for injectables (types that implement <see cref="IResourceService{TResource,TId}" />,
/// <see cref="IResourceRepository{TResource,TId}" /> or <see cref="IResourceDefinition{TResource,TId}" />) and registers them in the IoC container.
/// </summary>
internal sealed class InjectablesAssemblyScanner
{
internal static readonly HashSet<Type> 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<Type> RepositoryUnboundInterfaces =
[
typeof(IResourceRepository<,>),
typeof(IResourceWriteRepository<,>),
typeof(IResourceReadRepository<,>)
];

internal static readonly HashSet<Type> 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<ResourceDescriptor> descriptors = _assemblyCache.GetResourceDescriptors();
IReadOnlyCollection<Assembly> assemblies = _assemblyCache.GetAssemblies();

foreach (Assembly assembly in assemblies)
{
AddDbContextResolvers(assembly);
AddInjectables(descriptors, assembly);
}
}

private void AddDbContextResolvers(Assembly assembly)
{
IEnumerable<Type> 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<ResourceDescriptor> 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);
}
}
}
76 changes: 40 additions & 36 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
@@ -23,17 +23,15 @@
namespace JsonApiDotNetCore.Configuration;

/// <summary>
/// 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.
/// </summary>
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<MvcOptions>? ConfigureMvcOptions { get; set; }

@@ -44,12 +42,6 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv

_services = services;
_mvcBuilder = mvcBuilder;
_intermediateProvider = services.BuildServiceProvider();

var loggerFactory = _intermediateProvider.GetRequiredService<ILoggerFactory>();

_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory);
}

/// <summary>
@@ -61,35 +53,51 @@ public void ConfigureJsonApiOptions(Action<JsonApiOptions>? configureOptions)
}

/// <summary>
/// Executes the action provided by the user to configure <see cref="ServiceDiscoveryFacade" />.
/// Executes the action provided by the user to configure auto-discovery.
/// </summary>
public void ConfigureAutoDiscovery(Action<ServiceDiscoveryFacade>? configureAutoDiscovery)
{
configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade);
if (configureAutoDiscovery != null)
{
var facade = new ServiceDiscoveryFacade(_assemblyCache);
configureAutoDiscovery.Invoke(facade);
}
}

/// <summary>
/// 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.
/// </summary>
public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<ResourceGraphBuilder>? 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<ILoggerFactory>();
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;
});
}

/// <summary>
@@ -114,15 +122,16 @@ public void ConfigureMvc()
}

/// <summary>
/// Discovers DI registrable services in the assemblies marked for discovery.
/// Registers injectables in the IoC container found in assemblies marked for auto-discovery.
/// </summary>
public void DiscoverInjectables()
{
_serviceDiscoveryFacade.DiscoverInjectables();
var scanner = new InjectablesAssemblyScanner(_assemblyCache, _services);
scanner.DiscoverInjectables();
}

/// <summary>
/// Registers the remaining internals.
/// Registers the remaining internals in the IoC container.
/// </summary>
public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
{
@@ -182,15 +191,15 @@ private void AddMiddlewareLayer()

private void AddResourceLayer()
{
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>));
RegisterImplementationForInterfaces(InjectablesAssemblyScanner.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>));

_services.TryAddScoped<IResourceDefinitionAccessor, ResourceDefinitionAccessor>();
_services.TryAddScoped<IResourceFactory, ResourceFactory>();
}

private void AddRepositoryLayer()
{
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>));
RegisterImplementationForInterfaces(InjectablesAssemblyScanner.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>));

_services.TryAddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();

@@ -204,7 +213,7 @@ private void AddRepositoryLayer()

private void AddServiceLayer()
{
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ServiceUnboundInterfaces, typeof(JsonApiResourceService<,>));
RegisterImplementationForInterfaces(InjectablesAssemblyScanner.ServiceUnboundInterfaces, typeof(JsonApiResourceService<,>));
}

private void RegisterImplementationForInterfaces(HashSet<Type> unboundInterfaces, Type unboundImplementationType)
@@ -291,9 +300,4 @@ private void AddOperationsLayer()
_services.TryAddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
_services.TryAddScoped<ILocalIdTracker, LocalIdTracker>();
}

public void Dispose()
{
_intermediateProvider.Dispose();
}
}
29 changes: 29 additions & 0 deletions src/JsonApiDotNetCore/Configuration/ResourcesAssemblyScanner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using JsonApiDotNetCore.Resources;

namespace JsonApiDotNetCore.Configuration;

/// <summary>
/// Scans assemblies for types that implement <see cref="IIdentifiable{TId}" /> and adds them to the resource graph.
/// </summary>
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action<
Action<ServiceDiscoveryFacade>? configureAutoDiscovery, Action<ResourceGraphBuilder>? configureResources, IMvcCoreBuilder? mvcBuilder,
ICollection<Type> 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<TService>(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<TRepository>(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<TResourceDefinition>(this
{
ArgumentGuard.NotNull(services);

RegisterTypeForUnboundInterfaces(services, typeof(TResourceDefinition), ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces);
RegisterTypeForUnboundInterfaces(services, typeof(TResourceDefinition), InjectablesAssemblyScanner.ResourceDefinitionUnboundInterfaces);

return services;
}
146 changes: 7 additions & 139 deletions src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs
Original file line number Diff line number Diff line change
@@ -1,169 +1,37 @@
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;

/// <summary>
/// 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.
/// </summary>
[PublicAPI]
public sealed class ServiceDiscoveryFacade
{
internal static readonly HashSet<Type> 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<Type> RepositoryUnboundInterfaces =
[
typeof(IResourceRepository<,>),
typeof(IResourceWriteRepository<,>),
typeof(IResourceReadRepository<,>)
];

internal static readonly HashSet<Type> ResourceDefinitionUnboundInterfaces = [typeof(IResourceDefinition<,>)];

private readonly ILogger<ServiceDiscoveryFacade> _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<ServiceDiscoveryFacade>();
_services = services;
_resourceGraphBuilder = resourceGraphBuilder;
_assemblyCache = assemblyCache;
}

/// <summary>
/// Mark the calling assembly for scanning of resources and injectables.
/// Includes the calling assembly for auto-discovery of resources and related injectables.
/// </summary>
public ServiceDiscoveryFacade AddCurrentAssembly()
{
return AddAssembly(Assembly.GetCallingAssembly());
}

/// <summary>
/// Mark the specified assembly for scanning of resources and injectables.
/// Includes the specified assembly for auto-discovery of resources and related injectables.
/// </summary>
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<ResourceDescriptor> descriptors = _assemblyCache.GetResourceDescriptors();
IReadOnlyCollection<Assembly> assemblies = _assemblyCache.GetAssemblies();

foreach (Assembly assembly in assemblies)
{
AddDbContextResolvers(assembly);
AddInjectables(descriptors, assembly);
}
}

private void AddInjectables(IReadOnlyCollection<ResourceDescriptor> resourceDescriptors, Assembly assembly)
{
foreach (ResourceDescriptor resourceDescriptor in resourceDescriptors)
{
AddServices(assembly, resourceDescriptor);
AddRepositories(assembly, resourceDescriptor);
AddResourceDefinitions(assembly, resourceDescriptor);
}
}

private void AddDbContextResolvers(Assembly assembly)
{
IEnumerable<Type> 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);
}
}
}