Skip to content

Commit

Permalink
Removes building an intermediate service provider at startup, which i…
Browse files Browse the repository at this point in the history
…s considered an anti-pattern, leading to occasional compatibility issues. (#1431)

This unblocks use in [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) (more specifically, its usage of `AddNpgsqlDataSource`, which throws an `ObjectDisposedException`) and fixes #1082.
  • Loading branch information
bkoelman committed Dec 22, 2023
1 parent 2a30f19 commit 2637d0d
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 179 deletions.
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
Expand Up @@ -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; }

Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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)
{
Expand Down Expand Up @@ -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>();

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Up @@ -43,7 +43,7 @@ public static class ServiceCollectionExtensions
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);
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down

0 comments on commit 2637d0d

Please sign in to comment.