Skip to content

Make it easier to register custom services in the IoC container #1352

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 2 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
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
8 changes: 5 additions & 3 deletions docs/usage/common-pitfalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ Neither sounds very compelling. If stored procedures is what you need, you're be
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.
Despite JsonApiDotNetCore trying its best to deal with it, the experience won't be as good as leaving it out.

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

> [!NOTE]
> In older versions of JsonApiDotNetCore, registering your own services in the IoC container *afterwards* increased the chances that your replacements would take effect.

#### Never use the Entity Framework Core In-Memory Database Provider
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.
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:
Expand Down
6 changes: 3 additions & 3 deletions src/Examples/MultiDbContextExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
SetDbContextDebugOptions(options);
});

builder.Services.AddResourceRepository<DbContextARepository<ResourceA>>();
builder.Services.AddResourceRepository<DbContextBRepository<ResourceB>>();

builder.Services.AddJsonApi(options =>
{
options.Namespace = "api";
Expand All @@ -39,9 +42,6 @@
typeof(DbContextB)
});

builder.Services.AddResourceRepository<DbContextARepository<ResourceA>>();
builder.Services.AddResourceRepository<DbContextBRepository<ResourceB>>();

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
Expand Down
4 changes: 2 additions & 2 deletions src/Examples/NoEntityFrameworkExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

// Add services to the container.

builder.Services.AddScoped<IInverseNavigationResolver, InMemoryInverseNavigationResolver>();

builder.Services.AddJsonApi(options =>
{
options.Namespace = "api";
Expand All @@ -18,8 +20,6 @@
#endif
}, discovery => discovery.AddCurrentAssembly());

builder.Services.AddScoped<IInverseNavigationResolver, InMemoryInverseNavigationResolver>();

WebApplication app = builder.Build();

// Configure the HTTP request pipeline.
Expand Down
132 changes: 66 additions & 66 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void ConfigureResourceGraph(ICollection<Type> dbContextTypes, Action<Reso

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

_services.AddSingleton(resourceGraph);
_services.TryAddSingleton(resourceGraph);
}

/// <summary>
Expand All @@ -109,7 +109,7 @@ public void ConfigureMvc()
if (_options.ValidateModelState)
{
_mvcBuilder.AddDataAnnotations();
_services.AddSingleton<IModelMetadataProvider, JsonApiModelMetadataProvider>();
_services.Replace(new ServiceDescriptor(typeof(IModelMetadataProvider), typeof(JsonApiModelMetadataProvider), ServiceLifetime.Singleton));
}
}

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

if (dbContextTypes.Any())
{
_services.AddScoped(typeof(DbContextResolver<>));
_services.TryAddScoped(typeof(DbContextResolver<>));

foreach (Type dbContextType in dbContextTypes)
{
Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType);
_services.AddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
_services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
}

_services.AddScoped<IOperationsTransactionFactory, EntityFrameworkCoreTransactionFactory>();
_services.TryAddScoped<IOperationsTransactionFactory, EntityFrameworkCoreTransactionFactory>();
}
else
{
_services.AddScoped<IOperationsTransactionFactory, MissingTransactionFactory>();
_services.TryAddScoped<IOperationsTransactionFactory, MissingTransactionFactory>();
}

AddResourceLayer();
Expand All @@ -153,46 +153,46 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
AddQueryStringLayer();
AddOperationsLayer();

_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
_services.AddScoped<IPaginationContext, PaginationContext>();
_services.AddScoped<IEvaluatedIncludeCache, EvaluatedIncludeCache>();
_services.AddScoped<ISparseFieldSetCache, SparseFieldSetCache>();
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();
_services.AddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
_services.TryAddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
_services.TryAddScoped<IPaginationContext, PaginationContext>();
_services.TryAddScoped<IEvaluatedIncludeCache, EvaluatedIncludeCache>();
_services.TryAddScoped<ISparseFieldSetCache, SparseFieldSetCache>();
_services.TryAddScoped<IQueryLayerComposer, QueryLayerComposer>();
_services.TryAddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
}

private void AddMiddlewareLayer()
{
_services.AddSingleton<IJsonApiOptions>(_options);
_services.AddSingleton<IJsonApiApplicationBuilder>(this);
_services.AddSingleton<IExceptionHandler, ExceptionHandler>();
_services.AddScoped<IAsyncJsonApiExceptionFilter, AsyncJsonApiExceptionFilter>();
_services.AddScoped<IAsyncQueryStringActionFilter, AsyncQueryStringActionFilter>();
_services.AddScoped<IAsyncConvertEmptyActionResultFilter, AsyncConvertEmptyActionResultFilter>();
_services.AddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
_services.AddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
_services.AddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetRequiredService<IJsonApiRoutingConvention>());
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
_services.AddScoped<IJsonApiRequest, JsonApiRequest>();
_services.AddScoped<IJsonApiWriter, JsonApiWriter>();
_services.AddScoped<IJsonApiReader, JsonApiReader>();
_services.AddScoped<ITargetedFields, TargetedFields>();
_services.TryAddSingleton<IJsonApiOptions>(_options);
_services.TryAddSingleton<IJsonApiApplicationBuilder>(this);
_services.TryAddSingleton<IExceptionHandler, ExceptionHandler>();
_services.TryAddScoped<IAsyncJsonApiExceptionFilter, AsyncJsonApiExceptionFilter>();
_services.TryAddScoped<IAsyncQueryStringActionFilter, AsyncQueryStringActionFilter>();
_services.TryAddScoped<IAsyncConvertEmptyActionResultFilter, AsyncConvertEmptyActionResultFilter>();
_services.TryAddSingleton<IJsonApiInputFormatter, JsonApiInputFormatter>();
_services.TryAddSingleton<IJsonApiOutputFormatter, JsonApiOutputFormatter>();
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
_services.TryAddSingleton<IControllerResourceMapping>(provider => provider.GetRequiredService<IJsonApiRoutingConvention>());
_services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
_services.TryAddScoped<IJsonApiRequest, JsonApiRequest>();
_services.TryAddScoped<IJsonApiWriter, JsonApiWriter>();
_services.TryAddScoped<IJsonApiReader, JsonApiReader>();
_services.TryAddScoped<ITargetedFields, TargetedFields>();
}

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

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

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

_services.AddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();
_services.TryAddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();

_services.TryAddTransient<IQueryableBuilder, QueryableBuilder>();
_services.TryAddTransient<IIncludeClauseBuilder, IncludeClauseBuilder>();
Expand Down Expand Up @@ -225,12 +225,12 @@ private void AddQueryStringLayer()
_services.TryAddTransient<ISparseFieldSetParser, SparseFieldSetParser>();
_services.TryAddTransient<IPaginationParser, PaginationParser>();

_services.AddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
_services.AddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
_services.AddScoped<ISortQueryStringParameterReader, SortQueryStringParameterReader>();
_services.AddScoped<ISparseFieldSetQueryStringParameterReader, SparseFieldSetQueryStringParameterReader>();
_services.AddScoped<IPaginationQueryStringParameterReader, PaginationQueryStringParameterReader>();
_services.AddScoped<IResourceDefinitionQueryableParameterReader, ResourceDefinitionQueryableParameterReader>();
_services.TryAddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
_services.TryAddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
_services.TryAddScoped<ISortQueryStringParameterReader, SortQueryStringParameterReader>();
_services.TryAddScoped<ISparseFieldSetQueryStringParameterReader, SparseFieldSetQueryStringParameterReader>();
_services.TryAddScoped<IPaginationQueryStringParameterReader, PaginationQueryStringParameterReader>();
_services.TryAddScoped<IResourceDefinitionQueryableParameterReader, ResourceDefinitionQueryableParameterReader>();

RegisterDependentService<IQueryStringParameterReader, IIncludeQueryStringParameterReader>();
RegisterDependentService<IQueryStringParameterReader, IFilterQueryStringParameterReader>();
Expand All @@ -246,50 +246,50 @@ private void AddQueryStringLayer()
RegisterDependentService<IQueryConstraintProvider, IPaginationQueryStringParameterReader>();
RegisterDependentService<IQueryConstraintProvider, IResourceDefinitionQueryableParameterReader>();

_services.AddScoped<IQueryStringReader, QueryStringReader>();
_services.AddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
_services.TryAddScoped<IQueryStringReader, QueryStringReader>();
_services.TryAddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
}

private void RegisterDependentService<TCollectionElement, TElementToAdd>()
where TCollectionElement : class
where TElementToAdd : TCollectionElement
{
_services.AddScoped<TCollectionElement>(serviceProvider => serviceProvider.GetRequiredService<TElementToAdd>());
_services.AddScoped<TCollectionElement>(provider => provider.GetRequiredService<TElementToAdd>());
}

private void AddSerializationLayer()
{
_services.AddScoped<IResourceIdentifierObjectAdapter, ResourceIdentifierObjectAdapter>();
_services.AddScoped<IRelationshipDataAdapter, RelationshipDataAdapter>();
_services.AddScoped<IResourceObjectAdapter, ResourceObjectAdapter>();
_services.AddScoped<IResourceDataAdapter, ResourceDataAdapter>();
_services.AddScoped<IAtomicReferenceAdapter, AtomicReferenceAdapter>();
_services.AddScoped<IResourceDataInOperationsRequestAdapter, ResourceDataInOperationsRequestAdapter>();
_services.AddScoped<IAtomicOperationObjectAdapter, AtomicOperationObjectAdapter>();
_services.AddScoped<IDocumentInResourceOrRelationshipRequestAdapter, DocumentInResourceOrRelationshipRequestAdapter>();
_services.AddScoped<IDocumentInOperationsRequestAdapter, DocumentInOperationsRequestAdapter>();
_services.AddScoped<IDocumentAdapter, DocumentAdapter>();

_services.AddScoped<ILinkBuilder, LinkBuilder>();
_services.AddScoped<IResponseMeta, EmptyResponseMeta>();
_services.AddScoped<IMetaBuilder, MetaBuilder>();
_services.AddSingleton<IFingerprintGenerator, FingerprintGenerator>();
_services.AddSingleton<IETagGenerator, ETagGenerator>();
_services.AddScoped<IResponseModelAdapter, ResponseModelAdapter>();
_services.TryAddScoped<IResourceIdentifierObjectAdapter, ResourceIdentifierObjectAdapter>();
_services.TryAddScoped<IRelationshipDataAdapter, RelationshipDataAdapter>();
_services.TryAddScoped<IResourceObjectAdapter, ResourceObjectAdapter>();
_services.TryAddScoped<IResourceDataAdapter, ResourceDataAdapter>();
_services.TryAddScoped<IAtomicReferenceAdapter, AtomicReferenceAdapter>();
_services.TryAddScoped<IResourceDataInOperationsRequestAdapter, ResourceDataInOperationsRequestAdapter>();
_services.TryAddScoped<IAtomicOperationObjectAdapter, AtomicOperationObjectAdapter>();
_services.TryAddScoped<IDocumentInResourceOrRelationshipRequestAdapter, DocumentInResourceOrRelationshipRequestAdapter>();
_services.TryAddScoped<IDocumentInOperationsRequestAdapter, DocumentInOperationsRequestAdapter>();
_services.TryAddScoped<IDocumentAdapter, DocumentAdapter>();

_services.TryAddScoped<ILinkBuilder, LinkBuilder>();
_services.TryAddScoped<IResponseMeta, EmptyResponseMeta>();
_services.TryAddScoped<IMetaBuilder, MetaBuilder>();
_services.TryAddSingleton<IFingerprintGenerator, FingerprintGenerator>();
_services.TryAddSingleton<IETagGenerator, ETagGenerator>();
_services.TryAddScoped<IResponseModelAdapter, ResponseModelAdapter>();
}

private void AddOperationsLayer()
{
_services.AddScoped(typeof(ICreateProcessor<,>), typeof(CreateProcessor<,>));
_services.AddScoped(typeof(IUpdateProcessor<,>), typeof(UpdateProcessor<,>));
_services.AddScoped(typeof(IDeleteProcessor<,>), typeof(DeleteProcessor<,>));
_services.AddScoped(typeof(IAddToRelationshipProcessor<,>), typeof(AddToRelationshipProcessor<,>));
_services.AddScoped(typeof(ISetRelationshipProcessor<,>), typeof(SetRelationshipProcessor<,>));
_services.AddScoped(typeof(IRemoveFromRelationshipProcessor<,>), typeof(RemoveFromRelationshipProcessor<,>));

_services.AddScoped<IOperationsProcessor, OperationsProcessor>();
_services.AddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
_services.AddScoped<ILocalIdTracker, LocalIdTracker>();
_services.TryAddScoped(typeof(ICreateProcessor<,>), typeof(CreateProcessor<,>));
_services.TryAddScoped(typeof(IUpdateProcessor<,>), typeof(UpdateProcessor<,>));
_services.TryAddScoped(typeof(IDeleteProcessor<,>), typeof(DeleteProcessor<,>));
_services.TryAddScoped(typeof(IAddToRelationshipProcessor<,>), typeof(AddToRelationshipProcessor<,>));
_services.TryAddScoped(typeof(ISetRelationshipProcessor<,>), typeof(SetRelationshipProcessor<,>));
_services.TryAddScoped(typeof(IRemoveFromRelationshipProcessor<,>), typeof(RemoveFromRelationshipProcessor<,>));

_services.TryAddScoped<IOperationsProcessor, OperationsProcessor>();
_services.TryAddScoped<IOperationProcessorAccessor, OperationProcessorAccessor>();
_services.TryAddScoped<ILocalIdTracker, LocalIdTracker>();
}

public void Dispose()
Expand Down
5 changes: 3 additions & 2 deletions src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using JsonApiDotNetCore.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCore.Configuration;
Expand Down Expand Up @@ -119,7 +120,7 @@ private void AddDbContextResolvers(Assembly assembly)
foreach (Type dbContextType in dbContextTypes)
{
Type dbContextResolverClosedType = typeof(DbContextResolver<>).MakeGenericType(dbContextType);
_services.AddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
_services.TryAddScoped(typeof(IDbContextResolver), dbContextResolverClosedType);
}
}

Expand Down Expand Up @@ -163,7 +164,7 @@ private void RegisterImplementations(Assembly assembly, Type interfaceType, Reso
if (result != null)
{
(Type implementationType, Type serviceInterface) = result.Value;
_services.AddScoped(serviceInterface, implementationType);
_services.TryAddScoped(serviceInterface, implementationType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public ArchiveTests(IntegrationTestContext<TestableStartup<TelevisionDbContext>,
testContext.UseController<TelevisionBroadcastsController>();
testContext.UseController<BroadcastCommentsController>();

testContext.ConfigureServicesAfterStartup(services =>
testContext.ConfigureServices(services =>
{
services.AddResourceDefinition<TelevisionBroadcastDefinition>();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public AtomicCreateResourceTests(IntegrationTestContext<TestableStartup<Operatio
testContext.UseController<MusicTracksController>();
testContext.UseController<PlaylistsController>();

testContext.ConfigureServicesBeforeStartup(services =>
testContext.ConfigureServices(services =>
{
services.AddSingleton<ISystemClock, FrozenSystemClock>();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public AtomicCreateResourceWithClientGeneratedIdTests(IntegrationTestContext<Tes
// These routes need to be registered in ASP.NET for rendering links to resource/relationship endpoints.
testContext.UseController<TextLanguagesController>();

testContext.ConfigureServicesAfterStartup(services =>
testContext.ConfigureServices(services =>
{
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public AtomicAbsoluteLinksTests(IntegrationTestContext<TestableStartup<Operation
testContext.UseController<TextLanguagesController>();
testContext.UseController<RecordCompaniesController>();

testContext.ConfigureServicesAfterStartup(services =>
testContext.ConfigureServices(services =>
{
services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public AtomicRelativeLinksWithNamespaceTests(
testContext.UseController<TextLanguagesController>();
testContext.UseController<RecordCompaniesController>();

testContext.ConfigureServicesAfterStartup(services =>
testContext.ConfigureServices(services =>
{
services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>));
});
Expand Down
Loading