Skip to content

Commit 2637d0d

Browse files
authored
Removes building an intermediate service provider at startup, which is 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.
1 parent 2a30f19 commit 2637d0d

File tree

5 files changed

+209
-179
lines changed

5 files changed

+209
-179
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System.Reflection;
2+
using JsonApiDotNetCore.Repositories;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCore.Services;
5+
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
9+
namespace JsonApiDotNetCore.Configuration;
10+
11+
/// <summary>
12+
/// Scans assemblies for injectables (types that implement <see cref="IResourceService{TResource,TId}" />,
13+
/// <see cref="IResourceRepository{TResource,TId}" /> or <see cref="IResourceDefinition{TResource,TId}" />) and registers them in the IoC container.
14+
/// </summary>
15+
internal sealed class InjectablesAssemblyScanner
16+
{
17+
internal static readonly HashSet<Type> ServiceUnboundInterfaces =
18+
[
19+
typeof(IResourceService<,>),
20+
typeof(IResourceCommandService<,>),
21+
typeof(IResourceQueryService<,>),
22+
typeof(IGetAllService<,>),
23+
typeof(IGetByIdService<,>),
24+
typeof(IGetSecondaryService<,>),
25+
typeof(IGetRelationshipService<,>),
26+
typeof(ICreateService<,>),
27+
typeof(IAddToRelationshipService<,>),
28+
typeof(IUpdateService<,>),
29+
typeof(ISetRelationshipService<,>),
30+
typeof(IDeleteService<,>),
31+
typeof(IRemoveFromRelationshipService<,>)
32+
];
33+
34+
internal static readonly HashSet<Type> RepositoryUnboundInterfaces =
35+
[
36+
typeof(IResourceRepository<,>),
37+
typeof(IResourceWriteRepository<,>),
38+
typeof(IResourceReadRepository<,>)
39+
];
40+
41+
internal static readonly HashSet<Type> ResourceDefinitionUnboundInterfaces = [typeof(IResourceDefinition<,>)];
42+
43+
private readonly ResourceDescriptorAssemblyCache _assemblyCache;
44+
private readonly IServiceCollection _services;
45+
private readonly TypeLocator _typeLocator = new();
46+
47+
public InjectablesAssemblyScanner(ResourceDescriptorAssemblyCache assemblyCache, IServiceCollection services)
48+
{
49+
ArgumentGuard.NotNull(assemblyCache);
50+
ArgumentGuard.NotNull(services);
51+
52+
_assemblyCache = assemblyCache;
53+
_services = services;
54+
}
55+
56+
public void DiscoverInjectables()
57+
{
58+
IReadOnlyCollection<ResourceDescriptor> descriptors = _assemblyCache.GetResourceDescriptors();
59+
IReadOnlyCollection<Assembly> assemblies = _assemblyCache.GetAssemblies();
60+
61+
foreach (Assembly assembly in assemblies)
62+
{
63+
AddDbContextResolvers(assembly);
64+
AddInjectables(descriptors, assembly);
65+
}
66+
}
67+
68+
private void AddDbContextResolvers(Assembly assembly)
69+
{
70+
IEnumerable<Type> dbContextTypes = _typeLocator.GetDerivedTypes(assembly, typeof(DbContext));
71+
72+
foreach (Type dbContextType in dbContextTypes)
73+
{
74+
Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType);
75+
_services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
76+
}
77+
}
78+
79+
private void AddInjectables(IEnumerable<ResourceDescriptor> resourceDescriptors, Assembly assembly)
80+
{
81+
foreach (ResourceDescriptor resourceDescriptor in resourceDescriptors)
82+
{
83+
AddServices(assembly, resourceDescriptor);
84+
AddRepositories(assembly, resourceDescriptor);
85+
AddResourceDefinitions(assembly, resourceDescriptor);
86+
}
87+
}
88+
89+
private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor)
90+
{
91+
foreach (Type serviceUnboundInterface in ServiceUnboundInterfaces)
92+
{
93+
RegisterImplementations(assembly, serviceUnboundInterface, resourceDescriptor);
94+
}
95+
}
96+
97+
private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor)
98+
{
99+
foreach (Type repositoryUnboundInterface in RepositoryUnboundInterfaces)
100+
{
101+
RegisterImplementations(assembly, repositoryUnboundInterface, resourceDescriptor);
102+
}
103+
}
104+
105+
private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resourceDescriptor)
106+
{
107+
foreach (Type resourceDefinitionUnboundInterface in ResourceDefinitionUnboundInterfaces)
108+
{
109+
RegisterImplementations(assembly, resourceDefinitionUnboundInterface, resourceDescriptor);
110+
}
111+
}
112+
113+
private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor)
114+
{
115+
Type[] typeArguments =
116+
[
117+
resourceDescriptor.ResourceClrType,
118+
resourceDescriptor.IdClrType
119+
];
120+
121+
(Type implementationType, Type serviceInterface)? result = _typeLocator.GetContainerRegistrationFromAssembly(assembly, interfaceType, typeArguments);
122+
123+
if (result != null)
124+
{
125+
(Type implementationType, Type serviceInterface) = result.Value;
126+
_services.TryAddScoped(serviceInterface, implementationType);
127+
}
128+
}
129+
}

Diff for: src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+40-36
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@
2323
namespace JsonApiDotNetCore.Configuration;
2424

2525
/// <summary>
26-
/// A utility class that builds a JsonApi application. It registers all required services and allows the user to override parts of the startup
26+
/// A utility class that builds a JSON:API application. It registers all required services and allows the user to override parts of the startup
2727
/// configuration.
2828
/// </summary>
29-
internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, IDisposable
29+
internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder
3030
{
31-
private readonly JsonApiOptions _options = new();
3231
private readonly IServiceCollection _services;
3332
private readonly IMvcCoreBuilder _mvcBuilder;
34-
private readonly ResourceGraphBuilder _resourceGraphBuilder;
35-
private readonly ServiceDiscoveryFacade _serviceDiscoveryFacade;
36-
private readonly ServiceProvider _intermediateProvider;
33+
private readonly JsonApiOptions _options = new();
34+
private readonly ResourceDescriptorAssemblyCache _assemblyCache = new();
3735

3836
public Action<MvcOptions>? ConfigureMvcOptions { get; set; }
3937

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

4543
_services = services;
4644
_mvcBuilder = mvcBuilder;
47-
_intermediateProvider = services.BuildServiceProvider();
48-
49-
var loggerFactory = _intermediateProvider.GetRequiredService<ILoggerFactory>();
50-
51-
_resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
52-
_serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory);
5345
}
5446

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

6355
/// <summary>
64-
/// Executes the action provided by the user to configure <see cref="ServiceDiscoveryFacade" />.
56+
/// Executes the action provided by the user to configure auto-discovery.
6557
/// </summary>
6658
public void ConfigureAutoDiscovery(Action<ServiceDiscoveryFacade>? configureAutoDiscovery)
6759
{
68-
configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade);
60+
if (configureAutoDiscovery != null)
61+
{
62+
var facade = new ServiceDiscoveryFacade(_assemblyCache);
63+
configureAutoDiscovery.Invoke(facade);
64+
}
6965
}
7066

7167
/// <summary>
72-
/// Configures and builds the resource graph with resources from the provided sources and adds it to the DI container.
68+
/// Configures and builds the resource graph with resources from the provided sources and adds them to the IoC container.
7369
/// </summary>
7470
public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<ResourceGraphBuilder>? configureResourceGraph)
7571
{
7672
ArgumentGuard.NotNull(dbContextTypes);
7773

78-
_serviceDiscoveryFacade.DiscoverResources();
79-
80-
foreach (Type dbContextType in dbContextTypes)
74+
_services.TryAddSingleton(serviceProvider =>
8175
{
82-
var dbContext = (DbContext)_intermediateProvider.GetRequiredService(dbContextType);
83-
_resourceGraphBuilder.Add(dbContext);
84-
}
76+
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
77+
var resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory);
8578

86-
configureResourceGraph?.Invoke(_resourceGraphBuilder);
79+
var scanner = new ResourcesAssemblyScanner(_assemblyCache, resourceGraphBuilder);
80+
scanner.DiscoverResources();
8781

88-
IResourceGraph resourceGraph = _resourceGraphBuilder.Build();
82+
if (dbContextTypes.Count > 0)
83+
{
84+
using IServiceScope scope = serviceProvider.CreateScope();
8985

90-
_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
86+
foreach (Type dbContextType in dbContextTypes)
87+
{
88+
var dbContext = (DbContext)scope.ServiceProvider.GetRequiredService(dbContextType);
89+
resourceGraphBuilder.Add(dbContext);
90+
}
91+
}
92+
93+
configureResourceGraph?.Invoke(resourceGraphBuilder);
94+
95+
IResourceGraph resourceGraph = resourceGraphBuilder.Build();
96+
97+
_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
9198

92-
_services.TryAddSingleton(resourceGraph);
99+
return resourceGraph;
100+
});
93101
}
94102

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

116124
/// <summary>
117-
/// Discovers DI registrable services in the assemblies marked for discovery.
125+
/// Registers injectables in the IoC container found in assemblies marked for auto-discovery.
118126
/// </summary>
119127
public void DiscoverInjectables()
120128
{
121-
_serviceDiscoveryFacade.DiscoverInjectables();
129+
var scanner = new InjectablesAssemblyScanner(_assemblyCache, _services);
130+
scanner.DiscoverInjectables();
122131
}
123132

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

183192
private void AddResourceLayer()
184193
{
185-
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>));
194+
RegisterImplementationForInterfaces(InjectablesAssemblyScanner.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>));
186195

187196
_services.TryAddScoped<IResourceDefinitionAccessor, ResourceDefinitionAccessor>();
188197
_services.TryAddScoped<IResourceFactory, ResourceFactory>();
189198
}
190199

191200
private void AddRepositoryLayer()
192201
{
193-
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>));
202+
RegisterImplementationForInterfaces(InjectablesAssemblyScanner.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>));
194203

195204
_services.TryAddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();
196205

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

205214
private void AddServiceLayer()
206215
{
207-
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ServiceUnboundInterfaces, typeof(JsonApiResourceService<,>));
216+
RegisterImplementationForInterfaces(InjectablesAssemblyScanner.ServiceUnboundInterfaces, typeof(JsonApiResourceService<,>));
208217
}
209218

210219
private void RegisterImplementationForInterfaces(HashSet<Type> unboundInterfaces, Type unboundImplementationType)
@@ -291,9 +300,4 @@ private void AddOperationsLayer()
291300
_services.TryAddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
292301
_services.TryAddScoped<ILocalIdTracker, LocalIdTracker>();
293302
}
294-
295-
public void Dispose()
296-
{
297-
_intermediateProvider.Dispose();
298-
}
299303
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using JsonApiDotNetCore.Resources;
2+
3+
namespace JsonApiDotNetCore.Configuration;
4+
5+
/// <summary>
6+
/// Scans assemblies for types that implement <see cref="IIdentifiable{TId}" /> and adds them to the resource graph.
7+
/// </summary>
8+
internal sealed class ResourcesAssemblyScanner
9+
{
10+
private readonly ResourceDescriptorAssemblyCache _assemblyCache;
11+
private readonly ResourceGraphBuilder _resourceGraphBuilder;
12+
13+
public ResourcesAssemblyScanner(ResourceDescriptorAssemblyCache assemblyCache, ResourceGraphBuilder resourceGraphBuilder)
14+
{
15+
ArgumentGuard.NotNull(assemblyCache);
16+
ArgumentGuard.NotNull(resourceGraphBuilder);
17+
18+
_assemblyCache = assemblyCache;
19+
_resourceGraphBuilder = resourceGraphBuilder;
20+
}
21+
22+
public void DiscoverResources()
23+
{
24+
foreach (ResourceDescriptor resourceDescriptor in _assemblyCache.GetResourceDescriptors())
25+
{
26+
_resourceGraphBuilder.Add(resourceDescriptor.ResourceClrType, resourceDescriptor.IdClrType);
27+
}
28+
}
29+
}

Diff for: src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action<
4343
Action<ServiceDiscoveryFacade>? configureAutoDiscovery, Action<ResourceGraphBuilder>? configureResources, IMvcCoreBuilder? mvcBuilder,
4444
ICollection<Type> dbContextTypes)
4545
{
46-
using var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore());
46+
var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore());
4747

4848
applicationBuilder.ConfigureJsonApiOptions(configureOptions);
4949
applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery);
@@ -61,7 +61,7 @@ public static IServiceCollection AddResourceService<TService>(this IServiceColle
6161
{
6262
ArgumentGuard.NotNull(services);
6363

64-
RegisterTypeForUnboundInterfaces(services, typeof(TService), ServiceDiscoveryFacade.ServiceUnboundInterfaces);
64+
RegisterTypeForUnboundInterfaces(services, typeof(TService), InjectablesAssemblyScanner.ServiceUnboundInterfaces);
6565

6666
return services;
6767
}
@@ -74,7 +74,7 @@ public static IServiceCollection AddResourceRepository<TRepository>(this IServic
7474
{
7575
ArgumentGuard.NotNull(services);
7676

77-
RegisterTypeForUnboundInterfaces(services, typeof(TRepository), ServiceDiscoveryFacade.RepositoryUnboundInterfaces);
77+
RegisterTypeForUnboundInterfaces(services, typeof(TRepository), InjectablesAssemblyScanner.RepositoryUnboundInterfaces);
7878

7979
return services;
8080
}
@@ -87,7 +87,7 @@ public static IServiceCollection AddResourceDefinition<TResourceDefinition>(this
8787
{
8888
ArgumentGuard.NotNull(services);
8989

90-
RegisterTypeForUnboundInterfaces(services, typeof(TResourceDefinition), ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces);
90+
RegisterTypeForUnboundInterfaces(services, typeof(TResourceDefinition), InjectablesAssemblyScanner.ResourceDefinitionUnboundInterfaces);
9191

9292
return services;
9393
}

0 commit comments

Comments
 (0)