Skip to content

Commit 050a33d

Browse files
authored
Merge pull request #1352 from json-api-dotnet/try-add-services
Make it easier to register custom services in the IoC container
2 parents 8082765 + f179362 commit 050a33d

File tree

60 files changed

+244
-208
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+244
-208
lines changed

docs/usage/common-pitfalls.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ Neither sounds very compelling. If stored procedures is what you need, you're be
8787
Although recommended by Microsoft for hard-written controllers, the opinionated behavior of [`[ApiController]`](https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#apicontroller-attribute) violates the JSON:API specification.
8888
Despite JsonApiDotNetCore trying its best to deal with it, the experience won't be as good as leaving it out.
8989

90-
#### Replace injectable services *after* calling `AddJsonApi()`
91-
Registering your own services in the IoC container afterwards increases the chances that your replacements will take effect.
92-
Also, register with `services.AddResourceDefinition/AddResourceService/AddResourceRepository()` instead of `services.AddScoped()`.
90+
#### Register/override injectable services
91+
Register your JSON:API resource services, resource definitions and repositories with `services.AddResourceService/AddResourceDefinition/AddResourceRepository()` instead of `services.AddScoped()`.
9392
When using [Auto-discovery](~/usage/resource-graph.md#auto-discovery), you don't need to register these at all.
9493

94+
> [!NOTE]
95+
> In older versions of JsonApiDotNetCore, registering your own services in the IoC container *afterwards* increased the chances that your replacements would take effect.
96+
9597
#### Never use the Entity Framework Core In-Memory Database Provider
9698
When using this provider, many invalid mappings go unnoticed, leading to strange errors or wrong behavior. A real SQL engine fails to create the schema when mappings are invalid.
9799
If you're in need of a quick setup, use [SQLite](https://www.sqlite.org/). After adding its [NuGet package](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite), it's as simple as:

src/Examples/MultiDbContextExample/Program.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
SetDbContextDebugOptions(options);
2323
});
2424

25+
builder.Services.AddResourceRepository<DbContextARepository<ResourceA>>();
26+
builder.Services.AddResourceRepository<DbContextBRepository<ResourceB>>();
27+
2528
builder.Services.AddJsonApi(options =>
2629
{
2730
options.Namespace = "api";
@@ -39,9 +42,6 @@
3942
typeof(DbContextB)
4043
});
4144

42-
builder.Services.AddResourceRepository<DbContextARepository<ResourceA>>();
43-
builder.Services.AddResourceRepository<DbContextBRepository<ResourceB>>();
44-
4545
WebApplication app = builder.Build();
4646

4747
// Configure the HTTP request pipeline.

src/Examples/NoEntityFrameworkExample/Program.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
// Add services to the container.
77

8+
builder.Services.AddScoped<IInverseNavigationResolver, InMemoryInverseNavigationResolver>();
9+
810
builder.Services.AddJsonApi(options =>
911
{
1012
options.Namespace = "api";
@@ -18,8 +20,6 @@
1820
#endif
1921
}, discovery => discovery.AddCurrentAssembly());
2022

21-
builder.Services.AddScoped<IInverseNavigationResolver, InMemoryInverseNavigationResolver>();
22-
2323
WebApplication app = builder.Build();
2424

2525
// Configure the HTTP request pipeline.

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+66-66
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso
8989

9090
_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
9191

92-
_services.AddSingleton(resourceGraph);
92+
_services.TryAddSingleton(resourceGraph);
9393
}
9494

9595
/// <summary>
@@ -109,7 +109,7 @@ public void ConfigureMvc()
109109
if (_options.ValidateModelState)
110110
{
111111
_mvcBuilder.AddDataAnnotations();
112-
_services.AddSingleton<IModelMetadataProvider, JsonApiModelMetadataProvider>();
112+
_services.Replace(new ServiceDescriptor(typeof(IModelMetadataProvider), typeof(JsonApiModelMetadataProvider), ServiceLifetime.Singleton));
113113
}
114114
}
115115

@@ -130,19 +130,19 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
130130

131131
if (dbContextTypes.Any())
132132
{
133-
_services.AddScoped(typeof(DbContextResolver<>));
133+
_services.TryAddScoped(typeof(DbContextResolver<>));
134134

135135
foreach (Type dbContextType in dbContextTypes)
136136
{
137137
Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType);
138-
_services.AddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
138+
_services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
139139
}
140140

141-
_services.AddScoped<IOperationsTransactionFactory, EntityFrameworkCoreTransactionFactory>();
141+
_services.TryAddScoped<IOperationsTransactionFactory, EntityFrameworkCoreTransactionFactory>();
142142
}
143143
else
144144
{
145-
_services.AddScoped<IOperationsTransactionFactory, MissingTransactionFactory>();
145+
_services.TryAddScoped<IOperationsTransactionFactory, MissingTransactionFactory>();
146146
}
147147

148148
AddResourceLayer();
@@ -153,46 +153,46 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
153153
AddQueryStringLayer();
154154
AddOperationsLayer();
155155

156-
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
157-
_services.AddScoped<IPaginationContext, PaginationContext>();
158-
_services.AddScoped<IEvaluatedIncludeCache, EvaluatedIncludeCache>();
159-
_services.AddScoped<ISparseFieldSetCache, SparseFieldSetCache>();
160-
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();
161-
_services.AddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
156+
_services.TryAddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
157+
_services.TryAddScoped<IPaginationContext, PaginationContext>();
158+
_services.TryAddScoped<IEvaluatedIncludeCache, EvaluatedIncludeCache>();
159+
_services.TryAddScoped<ISparseFieldSetCache, SparseFieldSetCache>();
160+
_services.TryAddScoped<IQueryLayerComposer, QueryLayerComposer>();
161+
_services.TryAddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
162162
}
163163

164164
private void AddMiddlewareLayer()
165165
{
166-
_services.AddSingleton<IJsonApiOptions>(_options);
167-
_services.AddSingleton<IJsonApiApplicationBuilder>(this);
168-
_services.AddSingleton<IExceptionHandler, ExceptionHandler>();
169-
_services.AddScoped<IAsyncJsonApiExceptionFilter, AsyncJsonApiExceptionFilter>();
170-
_services.AddScoped<IAsyncQueryStringActionFilter, AsyncQueryStringActionFilter>();
171-
_services.AddScoped<IAsyncConvertEmptyActionResultFilter, AsyncConvertEmptyActionResultFilter>();
172-
_services.AddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
173-
_services.AddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
174-
_services.AddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
175-
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetRequiredService<IJsonApiRoutingConvention>());
176-
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
177-
_services.AddScoped<IJsonApiRequest, JsonApiRequest>();
178-
_services.AddScoped<IJsonApiWriter, JsonApiWriter>();
179-
_services.AddScoped<IJsonApiReader, JsonApiReader>();
180-
_services.AddScoped<ITargetedFields, TargetedFields>();
166+
_services.TryAddSingleton<IJsonApiOptions>(_options);
167+
_services.TryAddSingleton<IJsonApiApplicationBuilder>(this);
168+
_services.TryAddSingleton<IExceptionHandler, ExceptionHandler>();
169+
_services.TryAddScoped<IAsyncJsonApiExceptionFilter, AsyncJsonApiExceptionFilter>();
170+
_services.TryAddScoped<IAsyncQueryStringActionFilter, AsyncQueryStringActionFilter>();
171+
_services.TryAddScoped<IAsyncConvertEmptyActionResultFilter, AsyncConvertEmptyActionResultFilter>();
172+
_services.TryAddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
173+
_services.TryAddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
174+
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
175+
_services.TryAddSingleton<IControllerResourceMapping>(provider => provider.GetRequiredService<IJsonApiRoutingConvention>());
176+
_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
177+
_services.TryAddScoped<IJsonApiRequest, JsonApiRequest>();
178+
_services.TryAddScoped<IJsonApiWriter, JsonApiWriter>();
179+
_services.TryAddScoped<IJsonApiReader, JsonApiReader>();
180+
_services.TryAddScoped<ITargetedFields, TargetedFields>();
181181
}
182182

183183
private void AddResourceLayer()
184184
{
185185
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.ResourceDefinitionUnboundInterfaces, typeof(JsonApiResourceDefinition<,>));
186186

187-
_services.AddScoped<IResourceDefinitionAccessor, ResourceDefinitionAccessor>();
188-
_services.AddScoped<IResourceFactory, ResourceFactory>();
187+
_services.TryAddScoped<IResourceDefinitionAccessor, ResourceDefinitionAccessor>();
188+
_services.TryAddScoped<IResourceFactory, ResourceFactory>();
189189
}
190190

191191
private void AddRepositoryLayer()
192192
{
193193
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>));
194194

195-
_services.AddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();
195+
_services.TryAddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();
196196

197197
_services.TryAddTransient<IQueryableBuilder, QueryableBuilder>();
198198
_services.TryAddTransient<IIncludeClauseBuilder, IncludeClauseBuilder>();
@@ -225,12 +225,12 @@ private void AddQueryStringLayer()
225225
_services.TryAddTransient<ISparseFieldSetParser, SparseFieldSetParser>();
226226
_services.TryAddTransient<IPaginationParser, PaginationParser>();
227227

228-
_services.AddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
229-
_services.AddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
230-
_services.AddScoped<ISortQueryStringParameterReader, SortQueryStringParameterReader>();
231-
_services.AddScoped<ISparseFieldSetQueryStringParameterReader, SparseFieldSetQueryStringParameterReader>();
232-
_services.AddScoped<IPaginationQueryStringParameterReader, PaginationQueryStringParameterReader>();
233-
_services.AddScoped<IResourceDefinitionQueryableParameterReader, ResourceDefinitionQueryableParameterReader>();
228+
_services.TryAddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
229+
_services.TryAddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
230+
_services.TryAddScoped<ISortQueryStringParameterReader, SortQueryStringParameterReader>();
231+
_services.TryAddScoped<ISparseFieldSetQueryStringParameterReader, SparseFieldSetQueryStringParameterReader>();
232+
_services.TryAddScoped<IPaginationQueryStringParameterReader, PaginationQueryStringParameterReader>();
233+
_services.TryAddScoped<IResourceDefinitionQueryableParameterReader, ResourceDefinitionQueryableParameterReader>();
234234

235235
RegisterDependentService<IQueryStringParameterReader, IIncludeQueryStringParameterReader>();
236236
RegisterDependentService<IQueryStringParameterReader, IFilterQueryStringParameterReader>();
@@ -246,50 +246,50 @@ private void AddQueryStringLayer()
246246
RegisterDependentService<IQueryConstraintProvider, IPaginationQueryStringParameterReader>();
247247
RegisterDependentService<IQueryConstraintProvider, IResourceDefinitionQueryableParameterReader>();
248248

249-
_services.AddScoped<IQueryStringReader, QueryStringReader>();
250-
_services.AddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
249+
_services.TryAddScoped<IQueryStringReader, QueryStringReader>();
250+
_services.TryAddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
251251
}
252252

253253
private void RegisterDependentService<TCollectionElement, TElementToAdd>()
254254
where TCollectionElement : class
255255
where TElementToAdd : TCollectionElement
256256
{
257-
_services.AddScoped<TCollectionElement>(serviceProvider => serviceProvider.GetRequiredService<TElementToAdd>());
257+
_services.AddScoped<TCollectionElement>(provider => provider.GetRequiredService<TElementToAdd>());
258258
}
259259

260260
private void AddSerializationLayer()
261261
{
262-
_services.AddScoped<IResourceIdentifierObjectAdapter, ResourceIdentifierObjectAdapter>();
263-
_services.AddScoped<IRelationshipDataAdapter, RelationshipDataAdapter>();
264-
_services.AddScoped<IResourceObjectAdapter, ResourceObjectAdapter>();
265-
_services.AddScoped<IResourceDataAdapter, ResourceDataAdapter>();
266-
_services.AddScoped<IAtomicReferenceAdapter, AtomicReferenceAdapter>();
267-
_services.AddScoped<IResourceDataInOperationsRequestAdapter, ResourceDataInOperationsRequestAdapter>();
268-
_services.AddScoped<IAtomicOperationObjectAdapter, AtomicOperationObjectAdapter>();
269-
_services.AddScoped<IDocumentInResourceOrRelationshipRequestAdapter, DocumentInResourceOrRelationshipRequestAdapter>();
270-
_services.AddScoped<IDocumentInOperationsRequestAdapter, DocumentInOperationsRequestAdapter>();
271-
_services.AddScoped<IDocumentAdapter, DocumentAdapter>();
272-
273-
_services.AddScoped<ILinkBuilder, LinkBuilder>();
274-
_services.AddScoped<IResponseMeta, EmptyResponseMeta>();
275-
_services.AddScoped<IMetaBuilder, MetaBuilder>();
276-
_services.AddSingleton<IFingerprintGenerator, FingerprintGenerator>();
277-
_services.AddSingleton<IETagGenerator, ETagGenerator>();
278-
_services.AddScoped<IResponseModelAdapter, ResponseModelAdapter>();
262+
_services.TryAddScoped<IResourceIdentifierObjectAdapter, ResourceIdentifierObjectAdapter>();
263+
_services.TryAddScoped<IRelationshipDataAdapter, RelationshipDataAdapter>();
264+
_services.TryAddScoped<IResourceObjectAdapter, ResourceObjectAdapter>();
265+
_services.TryAddScoped<IResourceDataAdapter, ResourceDataAdapter>();
266+
_services.TryAddScoped<IAtomicReferenceAdapter, AtomicReferenceAdapter>();
267+
_services.TryAddScoped<IResourceDataInOperationsRequestAdapter, ResourceDataInOperationsRequestAdapter>();
268+
_services.TryAddScoped<IAtomicOperationObjectAdapter, AtomicOperationObjectAdapter>();
269+
_services.TryAddScoped<IDocumentInResourceOrRelationshipRequestAdapter, DocumentInResourceOrRelationshipRequestAdapter>();
270+
_services.TryAddScoped<IDocumentInOperationsRequestAdapter, DocumentInOperationsRequestAdapter>();
271+
_services.TryAddScoped<IDocumentAdapter, DocumentAdapter>();
272+
273+
_services.TryAddScoped<ILinkBuilder, LinkBuilder>();
274+
_services.TryAddScoped<IResponseMeta, EmptyResponseMeta>();
275+
_services.TryAddScoped<IMetaBuilder, MetaBuilder>();
276+
_services.TryAddSingleton<IFingerprintGenerator, FingerprintGenerator>();
277+
_services.TryAddSingleton<IETagGenerator, ETagGenerator>();
278+
_services.TryAddScoped<IResponseModelAdapter, ResponseModelAdapter>();
279279
}
280280

281281
private void AddOperationsLayer()
282282
{
283-
_services.AddScoped(typeof(ICreateProcessor<,>), typeof(CreateProcessor<,>));
284-
_services.AddScoped(typeof(IUpdateProcessor<,>), typeof(UpdateProcessor<,>));
285-
_services.AddScoped(typeof(IDeleteProcessor<,>), typeof(DeleteProcessor<,>));
286-
_services.AddScoped(typeof(IAddToRelationshipProcessor<,>), typeof(AddToRelationshipProcessor<,>));
287-
_services.AddScoped(typeof(ISetRelationshipProcessor<,>), typeof(SetRelationshipProcessor<,>));
288-
_services.AddScoped(typeof(IRemoveFromRelationshipProcessor<,>), typeof(RemoveFromRelationshipProcessor<,>));
289-
290-
_services.AddScoped<IOperationsProcessor, OperationsProcessor>();
291-
_services.AddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
292-
_services.AddScoped<ILocalIdTracker, LocalIdTracker>();
283+
_services.TryAddScoped(typeof(ICreateProcessor<,>), typeof(CreateProcessor<,>));
284+
_services.TryAddScoped(typeof(IUpdateProcessor<,>), typeof(UpdateProcessor<,>));
285+
_services.TryAddScoped(typeof(IDeleteProcessor<,>), typeof(DeleteProcessor<,>));
286+
_services.TryAddScoped(typeof(IAddToRelationshipProcessor<,>), typeof(AddToRelationshipProcessor<,>));
287+
_services.TryAddScoped(typeof(ISetRelationshipProcessor<,>), typeof(SetRelationshipProcessor<,>));
288+
_services.TryAddScoped(typeof(IRemoveFromRelationshipProcessor<,>), typeof(RemoveFromRelationshipProcessor<,>));
289+
290+
_services.TryAddScoped<IOperationsProcessor, OperationsProcessor>();
291+
_services.TryAddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
292+
_services.TryAddScoped<ILocalIdTracker, LocalIdTracker>();
293293
}
294294

295295
public void Dispose()

src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using JsonApiDotNetCore.Services;
66
using Microsoft.EntityFrameworkCore;
77
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.DependencyInjection.Extensions;
89
using Microsoft.Extensions.Logging;
910

1011
namespace JsonApiDotNetCore.Configuration;
@@ -119,7 +120,7 @@ private void AddDbContextResolvers(Assembly assembly)
119120
foreach (Type dbContextType in dbContextTypes)
120121
{
121122
Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType);
122-
_services.AddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
123+
_services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
123124
}
124125
}
125126

@@ -163,7 +164,7 @@ private void RegisterImplementations(Assembly assembly, Type interfaceType, Reso
163164
if (result != null)
164165
{
165166
(Type implementationType, Type serviceInterface) = result.Value;
166-
_services.AddScoped(serviceInterface, implementationType);
167+
_services.TryAddScoped(serviceInterface, implementationType);
167168
}
168169
}
169170
}

test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public ArchiveTests(IntegrationTestContext<TestableStartup<TelevisionDbContext>,
2121
testContext.UseController<TelevisionBroadcastsController>();
2222
testContext.UseController<BroadcastCommentsController>();
2323

24-
testContext.ConfigureServicesAfterStartup(services =>
24+
testContext.ConfigureServices(services =>
2525
{
2626
services.AddResourceDefinition<TelevisionBroadcastDefinition>();
2727
});

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public AtomicCreateResourceTests(IntegrationTestContext<TestableStartup<Operatio
2727
testContext.UseController<MusicTracksController>();
2828
testContext.UseController<PlaylistsController>();
2929

30-
testContext.ConfigureServicesBeforeStartup(services =>
30+
testContext.ConfigureServices(services =>
3131
{
3232
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3333
});

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public AtomicCreateResourceWithClientGeneratedIdTests(IntegrationTestContext<Tes
2424
// These routes need to be registered in ASP.NET for rendering links to resource/relationship endpoints.
2525
testContext.UseController<TextLanguagesController>();
2626

27-
testContext.ConfigureServicesAfterStartup(services =>
27+
testContext.ConfigureServices(services =>
2828
{
2929
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();
3030

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public AtomicAbsoluteLinksTests(IntegrationTestContext<TestableStartup<Operation
2525
testContext.UseController<TextLanguagesController>();
2626
testContext.UseController<RecordCompaniesController>();
2727

28-
testContext.ConfigureServicesAfterStartup(services =>
28+
testContext.ConfigureServices(services =>
2929
{
3030
services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
3131
});

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public AtomicRelativeLinksWithNamespaceTests(
2626
testContext.UseController<TextLanguagesController>();
2727
testContext.UseController<RecordCompaniesController>();
2828

29-
testContext.ConfigureServicesAfterStartup(services =>
29+
testContext.ConfigureServices(services =>
3030
{
3131
services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
3232
});

0 commit comments

Comments
 (0)