From 576355ce1c8b88941171ea2c5d0f00d5e3f99e19 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 22 Sep 2020 15:09:12 +0200 Subject: [PATCH 1/2] Fixed support for using multiple DbContexts --- .gitignore | 5 +- JsonApiDotnetCore.sln | 30 ++++++++ docs/usage/extensibility/repositories.md | 67 ++++++++--------- .../Controllers/ResourceAsController.cs | 17 +++++ .../Controllers/ResourceBsController.cs | 17 +++++ .../MultiDbContextExample/Data/DbContextA.cs | 15 ++++ .../Data/DbContextAResolver.cs | 20 +++++ .../MultiDbContextExample/Data/DbContextB.cs | 15 ++++ .../Data/DbContextBResolver.cs | 20 +++++ .../MultiDbContextExample/Models/ResourceA.cs | 11 +++ .../MultiDbContextExample/Models/ResourceB.cs | 11 +++ .../MultiDbContextExample.csproj | 13 ++++ src/Examples/MultiDbContextExample/Program.cs | 19 +++++ .../Properties/launchSettings.json | 30 ++++++++ .../Repositories/DbContextARepository.cs | 23 ++++++ .../Repositories/DbContextBRepository.cs | 23 ++++++ src/Examples/MultiDbContextExample/Startup.cs | 74 +++++++++++++++++++ .../MultiDbContextExample/appsettings.json | 10 +++ .../Configuration/IdentifiableTypeCache.cs | 35 --------- .../Configuration/InverseRelationships.cs | 41 +++++----- .../JsonApiApplicationBuilder.cs | 23 +++--- .../Configuration/ResourceDescriptor.cs | 2 - .../ResourceDescriptorAssemblyCache.cs | 50 +++++++++++++ .../Configuration/ResourceGraphBuilder.cs | 2 +- .../ServiceCollectionExtensions.cs | 59 ++++++++------- .../Configuration/ServiceDiscoveryFacade.cs | 45 +++-------- .../Configuration/TypeLocator.cs | 44 ++++++----- .../Annotations/IsRequiredAttribute.cs | 1 - .../Services/JsonApiResourceService.cs | 2 +- .../ServiceDiscoveryFacadeTests.cs | 1 - .../Acceptance/Spec/DocumentTests/Meta.cs | 1 - .../SparseFieldSets/SparseFieldSetTests.cs | 8 +- .../MultiDbContextTests.csproj | 23 ++++++ test/MultiDbContextTests/ResourceTests.cs | 73 ++++++++++++++++++ test/MultiDbContextTests/xunit.runner.json | 5 ++ .../Graph/IdentifiableTypeCacheTests.cs | 39 ---------- .../ResourceDescriptorAssemblyCacheTests.cs | 47 ++++++++++++ test/UnitTests/Graph/TypeLocator_Tests.cs | 22 +++--- .../ResourceHooks/ResourceHooksTestsSetup.cs | 4 +- 39 files changed, 697 insertions(+), 250 deletions(-) create mode 100644 src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs create mode 100644 src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs create mode 100644 src/Examples/MultiDbContextExample/Data/DbContextA.cs create mode 100644 src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs create mode 100644 src/Examples/MultiDbContextExample/Data/DbContextB.cs create mode 100644 src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs create mode 100644 src/Examples/MultiDbContextExample/Models/ResourceA.cs create mode 100644 src/Examples/MultiDbContextExample/Models/ResourceB.cs create mode 100644 src/Examples/MultiDbContextExample/MultiDbContextExample.csproj create mode 100644 src/Examples/MultiDbContextExample/Program.cs create mode 100644 src/Examples/MultiDbContextExample/Properties/launchSettings.json create mode 100644 src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs create mode 100644 src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs create mode 100644 src/Examples/MultiDbContextExample/Startup.cs create mode 100644 src/Examples/MultiDbContextExample/appsettings.json delete mode 100644 src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs create mode 100644 src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs create mode 100644 test/MultiDbContextTests/MultiDbContextTests.csproj create mode 100644 test/MultiDbContextTests/ResourceTests.cs create mode 100644 test/MultiDbContextTests/xunit.runner.json delete mode 100644 test/UnitTests/Graph/IdentifiableTypeCacheTests.cs create mode 100644 test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs diff --git a/.gitignore b/.gitignore index 1b99ea253e..2bd200a72d 100644 --- a/.gitignore +++ b/.gitignore @@ -363,4 +363,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Sqlite example databases +*.db diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index 769230390d..f0dc4e0982 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -43,6 +43,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStarted", "src\Examp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "test\IntegrationTests\IntegrationTests.csproj", "{CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextExample", "src\Examples\MultiDbContextExample\MultiDbContextExample.csproj", "{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextTests", "test\MultiDbContextTests\MultiDbContextTests.csproj", "{EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -185,6 +189,30 @@ Global {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x64.Build.0 = Release|Any CPU {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x86.ActiveCfg = Release|Any CPU {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9}.Release|x86.Build.0 = Release|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|x64.ActiveCfg = Debug|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|x64.Build.0 = Debug|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|x86.ActiveCfg = Debug|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|x86.Build.0 = Debug|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Release|Any CPU.Build.0 = Release|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Release|x64.ActiveCfg = Release|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Release|x64.Build.0 = Release|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Release|x86.ActiveCfg = Release|Any CPU + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Release|x86.Build.0 = Release|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Debug|x64.Build.0 = Debug|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Debug|x86.Build.0 = Debug|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Release|Any CPU.Build.0 = Release|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Release|x64.ActiveCfg = Release|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Release|x64.Build.0 = Release|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Release|x86.ActiveCfg = Release|Any CPU + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -201,6 +229,8 @@ Global {8BCFF95F-4850-427C-AEDB-B5B4F62B2C7B} = {026FBC6C-AF76-4568-9B87-EC73457899FD} {21D27239-138D-4604-8E49-DCBE41BCE4C8} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {CEB08B86-6BF1-4227-B20F-45AE9C1CC6D9} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {6CAFDDBE-00AB-4784-801B-AB419C3C3A26} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md index 6cae2ac05b..6975b239ad 100644 --- a/docs/usage/extensibility/repositories.md +++ b/docs/usage/extensibility/repositories.md @@ -45,62 +45,53 @@ public class ArticleRepository : EntityFrameworkCoreRepository
## Multiple DbContexts -If you need to use multiple Entity Framework Core DbContexts, first add each DbContext to the ResourceGraphBuilder. - -Then, create an implementation of IDbContextResolver for each context. - -Register each of the new IDbContextResolver implementations in Startup.cs. - -You can then create a general repository for each context and inject it per resource type. This example shows a single DbContextARepository for all entities that are members of DbContextA. - -Then inject the repository for the correct entity, in this case Foo is a member of DbContextA. +If you need to use multiple Entity Framework Core DbContexts, first create an implementation of `IDbContextResolver` for each context. ```c# -// Startup.cs -services.AddJsonApi(resources: builder => +public sealed class DbContextAResolver : IDbContextResolver { - // Add both contexts using the builder - builder.AddDbContext(); - builder.AddDbContext(); -}); + private readonly DbContextA _dbContextA; -public class DbContextAResolver : IDbContextResolver -{ - private readonly DbContextA _context; - - public DbContextAResolver(DbContextA context) + public DbContextAResolver(DbContextA dbContextA) { - _context = context; + _dbContextA = dbContextA; } public DbContext GetContext() { - return _context; + return _dbContextA; } } +``` +Next, create a repository for each context and inject its resolver per resource type. This example shows a single `DbContextARepository` for all entities that are members of `DbContextA`. -// Startup.cs -services.AddScoped(); -services.AddScoped(); - - +```c# public class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextARepository( - ITargetedFields targetedFields, - DbContextAResolver contextResolver, - IResourceGraph resourceGraph, - IGenericServiceFactory genericServiceFactory, - IResourceFactory resourceFactory, - IEnumerable constraintProviders, + public DbContextARepository(ITargetedFields targetedFields, DbContextAResolver contextResolver, + // ^^^^^^^^^^^^^^^^^^ + IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory) - { } + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, + constraintProviders, loggerFactory) + { + } } +``` + +Then register the added types and use the non-generic overload of `AddJsonApi` to add their resources to the graph. + +```c# +// Startup.ConfigureServices +services.AddScoped(); +services.AddScoped(); + +services.AddScoped, DbContextARepository>(); +services.AddScoped, DbContextBRepository>(); -// Startup.cs -services.AddScoped, DbContextARepository>(); +services.AddJsonApi(dbContextTypes: new[] {typeof(DbContextA), typeof(DbContextB)}); ``` diff --git a/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs b/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs new file mode 100644 index 0000000000..b406411c41 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; +using MultiDbContextExample.Models; + +namespace MultiDbContextExample.Controllers +{ + public sealed class ResourceAsController : JsonApiController + { + public ResourceAsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs b/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs new file mode 100644 index 0000000000..7268bb7969 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; +using MultiDbContextExample.Models; + +namespace MultiDbContextExample.Controllers +{ + public sealed class ResourceBsController : JsonApiController + { + public ResourceBsController(IJsonApiOptions options, ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/src/Examples/MultiDbContextExample/Data/DbContextA.cs b/src/Examples/MultiDbContextExample/Data/DbContextA.cs new file mode 100644 index 0000000000..72fb233d5b --- /dev/null +++ b/src/Examples/MultiDbContextExample/Data/DbContextA.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using MultiDbContextExample.Models; + +namespace MultiDbContextExample.Data +{ + public sealed class DbContextA : DbContext + { + public DbSet ResourceAs { get; set; } + + public DbContextA(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs b/src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs new file mode 100644 index 0000000000..432ce4ea57 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs @@ -0,0 +1,20 @@ +using JsonApiDotNetCore.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace MultiDbContextExample.Data +{ + public sealed class DbContextAResolver : IDbContextResolver + { + private readonly DbContextA _dbContextA; + + public DbContextAResolver(DbContextA dbContextA) + { + _dbContextA = dbContextA; + } + + public DbContext GetContext() + { + return _dbContextA; + } + } +} diff --git a/src/Examples/MultiDbContextExample/Data/DbContextB.cs b/src/Examples/MultiDbContextExample/Data/DbContextB.cs new file mode 100644 index 0000000000..4b6c5e7690 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Data/DbContextB.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using MultiDbContextExample.Models; + +namespace MultiDbContextExample.Data +{ + public sealed class DbContextB : DbContext + { + public DbSet ResourceBs { get; set; } + + public DbContextB(DbContextOptions options) + : base(options) + { + } + } +} diff --git a/src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs b/src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs new file mode 100644 index 0000000000..4d31add33d --- /dev/null +++ b/src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs @@ -0,0 +1,20 @@ +using JsonApiDotNetCore.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace MultiDbContextExample.Data +{ + public sealed class DbContextBResolver : IDbContextResolver + { + private readonly DbContextB _dbContextB; + + public DbContextBResolver(DbContextB dbContextB) + { + _dbContextB = dbContextB; + } + + public DbContext GetContext() + { + return _dbContextB; + } + } +} diff --git a/src/Examples/MultiDbContextExample/Models/ResourceA.cs b/src/Examples/MultiDbContextExample/Models/ResourceA.cs new file mode 100644 index 0000000000..104611cffc --- /dev/null +++ b/src/Examples/MultiDbContextExample/Models/ResourceA.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace MultiDbContextExample.Models +{ + public sealed class ResourceA : Identifiable + { + [Attr] + public string NameA { get; set; } + } +} diff --git a/src/Examples/MultiDbContextExample/Models/ResourceB.cs b/src/Examples/MultiDbContextExample/Models/ResourceB.cs new file mode 100644 index 0000000000..1fb41b6f96 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Models/ResourceB.cs @@ -0,0 +1,11 @@ +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace MultiDbContextExample.Models +{ + public sealed class ResourceB : Identifiable + { + [Attr] + public string NameB { get; set; } + } +} diff --git a/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj b/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj new file mode 100644 index 0000000000..c4db919042 --- /dev/null +++ b/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj @@ -0,0 +1,13 @@ + + + netcoreapp3.1 + + + + + + + + + + diff --git a/src/Examples/MultiDbContextExample/Program.cs b/src/Examples/MultiDbContextExample/Program.cs new file mode 100644 index 0000000000..c6e698750c --- /dev/null +++ b/src/Examples/MultiDbContextExample/Program.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace MultiDbContextExample +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); + } + } +} diff --git a/src/Examples/MultiDbContextExample/Properties/launchSettings.json b/src/Examples/MultiDbContextExample/Properties/launchSettings.json new file mode 100644 index 0000000000..1f33cc0f31 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14150", + "sslPort": 44350 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "/resourceBs", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "/resourceBs", + "applicationUrl": "https://localhost:44350;http://localhost:14150", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs new file mode 100644 index 0000000000..be64881ce9 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using Microsoft.Extensions.Logging; +using MultiDbContextExample.Data; + +namespace MultiDbContextExample.Repositories +{ + public class DbContextARepository : EntityFrameworkCoreRepository + where TResource : class, IIdentifiable + { + public DbContextARepository(ITargetedFields targetedFields, DbContextAResolver contextResolver, + IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, + IResourceFactory resourceFactory, IEnumerable constraintProviders, + ILoggerFactory loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, + constraintProviders, loggerFactory) + { + } + } +} diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs new file mode 100644 index 0000000000..e7dc6066d0 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Repositories; +using JsonApiDotNetCore.Resources; +using Microsoft.Extensions.Logging; +using MultiDbContextExample.Data; + +namespace MultiDbContextExample.Repositories +{ + public class DbContextBRepository : EntityFrameworkCoreRepository + where TResource : class, IIdentifiable + { + public DbContextBRepository(ITargetedFields targetedFields, DbContextBResolver contextResolver, + IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, + IResourceFactory resourceFactory, IEnumerable constraintProviders, + ILoggerFactory loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, + constraintProviders, loggerFactory) + { + } + } +} diff --git a/src/Examples/MultiDbContextExample/Startup.cs b/src/Examples/MultiDbContextExample/Startup.cs new file mode 100644 index 0000000000..c06c609c69 --- /dev/null +++ b/src/Examples/MultiDbContextExample/Startup.cs @@ -0,0 +1,74 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Repositories; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MultiDbContextExample.Data; +using MultiDbContextExample.Models; +using MultiDbContextExample.Repositories; + +namespace MultiDbContextExample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(options => options.UseSqlite("Data Source=A.db")); + services.AddDbContext(options => options.UseSqlite("Data Source=B.db")); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped, DbContextARepository>(); + services.AddScoped, DbContextBRepository>(); + + services.AddJsonApi(dbContextTypes: new[] {typeof(DbContextA), typeof(DbContextB)}); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DbContextA dbContextA, + DbContextB dbContextB) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + EnsureSampleDataA(dbContextA); + EnsureSampleDataB(dbContextB); + + app.UseRouting(); + app.UseJsonApi(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + + private static void EnsureSampleDataA(DbContextA dbContextA) + { + dbContextA.Database.EnsureDeleted(); + dbContextA.Database.EnsureCreated(); + + dbContextA.ResourceAs.Add(new ResourceA + { + NameA = "SampleA" + }); + + dbContextA.SaveChanges(); + } + + private static void EnsureSampleDataB(DbContextB dbContextB) + { + dbContextB.Database.EnsureDeleted(); + dbContextB.Database.EnsureCreated(); + + dbContextB.ResourceBs.Add(new ResourceB + { + NameB = "SampleB" + }); + + dbContextB.SaveChanges(); + } + } +} diff --git a/src/Examples/MultiDbContextExample/appsettings.json b/src/Examples/MultiDbContextExample/appsettings.json new file mode 100644 index 0000000000..4442d5117f --- /dev/null +++ b/src/Examples/MultiDbContextExample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs b/src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs deleted file mode 100644 index f0c5b309e8..0000000000 --- a/src/JsonApiDotNetCore/Configuration/IdentifiableTypeCache.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Configuration -{ - /// - /// Used to cache and locate types, to facilitate resource auto-discovery. - /// - internal sealed class IdentifiableTypeCache - { - private readonly ConcurrentDictionary> _typeCache = new ConcurrentDictionary>(); - - /// - /// Gets all implementations of in the assembly. - /// - public IReadOnlyCollection GetIdentifiableTypes(Assembly assembly) - { - return _typeCache.GetOrAdd(assembly, asm => FindIdentifiableTypes(asm).ToArray()); - } - - private static IEnumerable FindIdentifiableTypes(Assembly assembly) - { - foreach (var type in assembly.GetTypes()) - { - if (TypeLocator.TryGetResourceDescriptor(type, out var descriptor)) - { - yield return descriptor; - } - } - } - } -} diff --git a/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs b/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs index 5c217373dd..f8164b490e 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; @@ -9,37 +10,43 @@ namespace JsonApiDotNetCore.Configuration /// public class InverseRelationships : IInverseRelationships { - private readonly IResourceContextProvider _provider; - private readonly IDbContextResolver _resolver; + private readonly IResourceContextProvider _resourceContextProvider; + private readonly IEnumerable _dbContextResolvers; - public InverseRelationships(IResourceContextProvider provider, IDbContextResolver resolver = null) + public InverseRelationships(IResourceContextProvider resourceContextProvider, + IEnumerable dbContextResolvers) { - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - _resolver = resolver; + _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider)); + _dbContextResolvers = dbContextResolvers ?? throw new ArgumentNullException(nameof(dbContextResolvers)); } /// public void Resolve() { - if (IsEntityFrameworkCoreEnabled()) + foreach (var dbContextResolver in _dbContextResolvers) { - DbContext context = _resolver.GetContext(); + DbContext dbContext = dbContextResolver.GetContext(); + Resolve(dbContext); + } + } - foreach (ResourceContext ce in _provider.GetResourceContexts()) + private void Resolve(DbContext dbContext) + { + foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts()) + { + IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType); + if (entityType != null) { - IEntityType meta = context.Model.FindEntityType(ce.ResourceType); - if (meta == null) continue; - foreach (var attr in ce.Relationships) + foreach (var relationship in resourceContext.Relationships) { - if (attr is HasManyThroughAttribute) continue; - INavigation inverseNavigation = meta.FindNavigation(attr.Property.Name)?.FindInverse(); - attr.InverseNavigation = inverseNavigation?.Name; + if (!(relationship is HasManyThroughAttribute)) + { + INavigation inverseNavigation = entityType.FindNavigation(relationship.Property.Name)?.FindInverse(); + relationship.InverseNavigation = inverseNavigation?.Name; + } } } } } - - // If EF Core is not being used, we're expecting the resolver to not be registered. - private bool IsEntityFrameworkCoreEnabled() => _resolver != null; } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 9f3db2ce18..72eafd378e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Hooks.Internal; using JsonApiDotNetCore.Hooks.Internal.Discovery; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -68,15 +69,16 @@ public void ConfigureAutoDiscovery(Action configureAutoD /// /// Configures and builds the resource graph with resources from the provided sources and adds it to the DI container. /// - public void AddResourceGraph(Type dbContextType, Action configureResourceGraph) + public void AddResourceGraph(ICollection dbContextTypes, Action configureResourceGraph) { _serviceDiscoveryFacade.DiscoverResources(); - - if (dbContextType != null) + + foreach (var dbContextType in dbContextTypes) { - AddResourcesFromDbContext((DbContext)_intermediateProvider.GetService(dbContextType), _resourceGraphBuilder); + var dbContext = (DbContext)_intermediateProvider.GetRequiredService(dbContextType); + AddResourcesFromDbContext(dbContext, _resourceGraphBuilder); } - + configureResourceGraph?.Invoke(_resourceGraphBuilder); var resourceGraph = _resourceGraphBuilder.Build(); @@ -114,17 +116,12 @@ public void DiscoverInjectables() /// /// Registers the remaining internals. /// - public void ConfigureServiceContainer(Type dbContextType) + public void ConfigureServiceContainer(ICollection dbContextTypes) { - if (dbContextType != null) + foreach (var dbContextType in dbContextTypes) { var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.TryAddScoped(typeof(IDbContextResolver), contextResolverType); - } - else - { - _services.AddScoped(); - _services.AddSingleton(new DbContextOptionsBuilder().Options); + _services.AddScoped(typeof(IDbContextResolver), contextResolverType); } AddResourceLayer(); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs index a038930b65..420b9015d6 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptor.cs @@ -7,8 +7,6 @@ internal class ResourceDescriptor public Type ResourceType { get; } public Type IdType { get; } - internal static readonly ResourceDescriptor Empty = new ResourceDescriptor(null, null); - public ResourceDescriptor(Type resourceType, Type idType) { ResourceType = resourceType; diff --git a/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs new file mode 100644 index 0000000000..56f7450214 --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace JsonApiDotNetCore.Configuration +{ + /// + /// Used to scan assemblies for types and cache them, to facilitate resource auto-discovery. + /// + internal sealed class ResourceDescriptorAssemblyCache + { + private readonly Dictionary> _resourceDescriptorsPerAssembly = new Dictionary>(); + + public void RegisterAssembly(Assembly assembly) + { + if (!_resourceDescriptorsPerAssembly.ContainsKey(assembly)) + { + _resourceDescriptorsPerAssembly[assembly] = null; + } + } + + public IEnumerable<(Assembly assembly, IReadOnlyCollection resourceDescriptors)> GetResourceDescriptorsPerAssembly() + { + EnsureAssembliesScanned(); + + return _resourceDescriptorsPerAssembly.Select(pair => (pair.Key, pair.Value)); + } + + private void EnsureAssembliesScanned() + { + foreach (var assemblyToScan in _resourceDescriptorsPerAssembly.Where(pair => pair.Value == null) + .Select(pair => pair.Key).ToArray()) + { + _resourceDescriptorsPerAssembly[assemblyToScan] = ScanForResourceDescriptors(assemblyToScan).ToArray(); + } + } + + private static IEnumerable ScanForResourceDescriptors(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + var resourceDescriptor = TypeLocator.TryGetResourceDescriptor(type); + if (resourceDescriptor != null) + { + yield return resourceDescriptor; + } + } + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 8aaaa35d83..ff4dccb82e 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -87,7 +87,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu if (TypeHelper.IsOrImplementsInterface(resourceType, typeof(IIdentifiable))) { publicName ??= FormatResourceName(resourceType); - idType ??= TypeLocator.GetIdType(resourceType); + idType ??= TypeLocator.TryGetIdType(resourceType); var resourceContext = CreateResourceContext(publicName, resourceType, idType); _resources.Add(resourceContext); } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 55e1b52028..b39df70424 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Client.Internal; +using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -19,11 +20,13 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options = null, Action discovery = null, Action resources = null, - IMvcCoreBuilder mvcBuilder = null) + IMvcCoreBuilder mvcBuilder = null, + ICollection dbContextTypes = null) { if (services == null) throw new ArgumentNullException(nameof(services)); - SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, null); + SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, + dbContextTypes ?? Array.Empty()); return services; } @@ -40,23 +43,23 @@ public static IServiceCollection AddJsonApi(this IServiceCollection { if (services == null) throw new ArgumentNullException(nameof(services)); - SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); + SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, new[] {typeof(TDbContext)}); return services; } private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, Action configureAutoDiscovery, - Action configureResourceGraph, IMvcCoreBuilder mvcBuilder, Type dbContextType) + Action configureResourceGraph, IMvcCoreBuilder mvcBuilder, ICollection dbContextTypes) { using var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); applicationBuilder.ConfigureJsonApiOptions(configureOptions); applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); - applicationBuilder.AddResourceGraph(dbContextType, configureResourceGraph); + applicationBuilder.AddResourceGraph(dbContextTypes, configureResourceGraph); applicationBuilder.ConfigureMvc(); applicationBuilder.DiscoverInjectables(); - applicationBuilder.ConfigureServiceContainer(dbContextType); + applicationBuilder.ConfigureServiceContainer(dbContextTypes); } /// @@ -78,7 +81,8 @@ public static IServiceCollection AddClientSerialization(this IServiceCollection } /// - /// Adds all required registrations for the service to the container. + /// Adds IoC container registrations for the various JsonApiDotNetCore resource service interfaces, + /// such as , and various others. /// /// public static IServiceCollection AddResourceService(this IServiceCollection services) @@ -86,29 +90,29 @@ public static IServiceCollection AddResourceService(this IServiceColle if (services == null) throw new ArgumentNullException(nameof(services)); var typeImplementsAnExpectedInterface = false; - var serviceImplementationType = typeof(TService); + var resourceDescriptor = TryGetResourceTypeFromServiceImplementation(serviceImplementationType); - // it is _possible_ that a single concrete type could be used for multiple resources... - var resourceDescriptors = GetResourceTypesFromServiceImplementation(serviceImplementationType); - - foreach (var resourceDescriptor in resourceDescriptors) + if (resourceDescriptor != null) { foreach (var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces) { - // A shorthand interface is one where the ID type is omitted + // A shorthand interface is one where the ID type is omitted. // e.g. IResourceService is the shorthand for IResourceService var isShorthandInterface = openGenericType.GetTypeInfo().GenericTypeParameters.Length == 1; if (isShorthandInterface && resourceDescriptor.IdType != typeof(int)) - continue; // we can't create a shorthand for ID types other than int + { + // We can't create a shorthand for ID types other than int. + continue; + } - var concreteGenericType = isShorthandInterface + var constructedType = isShorthandInterface ? openGenericType.MakeGenericType(resourceDescriptor.ResourceType) : openGenericType.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType); - if (concreteGenericType.IsAssignableFrom(serviceImplementationType)) + if (constructedType.IsAssignableFrom(serviceImplementationType)) { - services.AddScoped(concreteGenericType, serviceImplementationType); + services.AddScoped(constructedType, serviceImplementationType); typeImplementsAnExpectedInterface = true; } } @@ -120,22 +124,25 @@ public static IServiceCollection AddResourceService(this IServiceColle return services; } - private static HashSet GetResourceTypesFromServiceImplementation(Type type) + private static ResourceDescriptor TryGetResourceTypeFromServiceImplementation(Type serviceType) { - var resourceDescriptors = new HashSet(); - var interfaces = type.GetInterfaces(); - foreach (var i in interfaces) + foreach (var @interface in serviceType.GetInterfaces()) { - if (i.IsGenericType) + var firstGenericArgument = @interface.IsGenericType + ? @interface.GenericTypeArguments.First() + : null; + + if (firstGenericArgument != null) { - var firstGenericArgument = i.GenericTypeArguments.FirstOrDefault(); - if (TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor)) + var resourceDescriptor = TypeLocator.TryGetResourceDescriptor(firstGenericArgument); + if (resourceDescriptor != null) { - resourceDescriptors.Add(resourceDescriptor); + return resourceDescriptor; } } } - return resourceDescriptors; + + return null; } } } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index c01111f3b4..b28fe466ea 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -58,8 +58,7 @@ public class ServiceDiscoveryFacade private readonly IServiceCollection _services; private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly IJsonApiOptions _options; - private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); - private readonly Dictionary> _resourceDescriptorsPerAssemblyCache = new Dictionary>(); + private readonly ResourceDescriptorAssemblyCache _assemblyCache = new ResourceDescriptorAssemblyCache(); public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, IJsonApiOptions options, ILoggerFactory loggerFactory) { @@ -89,42 +88,38 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) throw new ArgumentNullException(nameof(assembly)); } - _resourceDescriptorsPerAssemblyCache.Add(assembly, null); + _assemblyCache.RegisterAssembly(assembly); _logger.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); return this; } - + internal void DiscoverResources() { - foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) + foreach (var (_, resourceDescriptors) in _assemblyCache.GetResourceDescriptorsPerAssembly()) { - var resourceDescriptors = GetResourceDescriptorsFromCache(discoveredResourceDescriptors, assembly); - - foreach (var descriptor in resourceDescriptors) + foreach (var resourceDescriptor in resourceDescriptors) { - AddResource(descriptor); + AddResource(resourceDescriptor); } } } internal void DiscoverInjectables() { - foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) + foreach (var (assembly, resourceDescriptors) in _assemblyCache.GetResourceDescriptorsPerAssembly()) { AddDbContextResolvers(assembly); - var resourceDescriptors = GetResourceDescriptorsFromCache(discoveredResourceDescriptors, assembly); - - foreach (var descriptor in resourceDescriptors) + foreach (var resourceDescriptor in resourceDescriptors) { - AddServices(assembly, descriptor); - AddRepositories(assembly, descriptor); - AddResourceDefinitions(assembly, descriptor); + AddServices(assembly, resourceDescriptor); + AddRepositories(assembly, resourceDescriptor); + AddResourceDefinitions(assembly, resourceDescriptor); if (_options.EnableResourceHooks) { - AddResourceHookDefinitions(assembly, descriptor); + AddResourceHookDefinitions(assembly, resourceDescriptor); } } } @@ -197,21 +192,5 @@ private void RegisterImplementations(Assembly assembly, Type interfaceType, Reso _services.AddScoped(registrationInterface, implementation); } } - - private IList GetResourceDescriptorsFromCache(IList discoveredResourceDescriptors, Assembly assembly) - { - IList resourceDescriptors; - if (discoveredResourceDescriptors == null) - { - resourceDescriptors = (IList)_typeCache.GetIdentifiableTypes(assembly); - _resourceDescriptorsPerAssemblyCache[assembly] = resourceDescriptors; - } - else - { - resourceDescriptors = discoveredResourceDescriptors; - } - - return resourceDescriptors; - } } } diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index 4661f19dd0..6de8b18c14 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -12,63 +12,61 @@ namespace JsonApiDotNetCore.Configuration internal static class TypeLocator { /// - /// Determine whether or not this is a json:api resource by checking if it implements . + /// Attempts to lookup the ID type of the specified resource type. Returns null if it does not implement . /// - public static Type GetIdType(Type resourceType) + public static Type TryGetIdType(Type resourceType) { var identifiableInterface = resourceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIdentifiable<>)); return identifiableInterface?.GetGenericArguments()[0]; } /// - /// Attempts to get a descriptor for the resource type. + /// Attempts to get a descriptor for the specified resource type. /// - /// - /// true if the type is a valid json:api type (must implement ); false, otherwise. - /// - internal static bool TryGetResourceDescriptor(Type type, out ResourceDescriptor descriptor) + public static ResourceDescriptor TryGetResourceDescriptor(Type type) { if (TypeHelper.IsOrImplementsInterface(type, typeof(IIdentifiable))) { - descriptor = new ResourceDescriptor(type, GetIdType(type)); - return true; + var idType = TryGetIdType(type); + if (idType != null) + { + return new ResourceDescriptor(type, idType); + } } - descriptor = ResourceDescriptor.Empty; - return false; + + return null; } + /// /// Gets all implementations of the generic interface. /// /// The assembly to search. - /// The open generic type, e.g. `typeof(IResourceService<>)`. + /// The open generic type, e.g. `typeof(IResourceService<>)`. /// Parameters to the generic type. /// /// ), typeof(Article), typeof(Guid)); /// ]]> /// - public static (Type implementation, Type registrationInterface) GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterfaceType, params Type[] genericInterfaceArguments) + public static (Type implementation, Type registrationInterface) GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterface, params Type[] genericInterfaceArguments) { if (assembly == null) throw new ArgumentNullException(nameof(assembly)); - if (openGenericInterfaceType == null) throw new ArgumentNullException(nameof(openGenericInterfaceType)); + if (openGenericInterface == null) throw new ArgumentNullException(nameof(openGenericInterface)); if (genericInterfaceArguments == null) throw new ArgumentNullException(nameof(genericInterfaceArguments)); if (genericInterfaceArguments.Length == 0) throw new ArgumentException("No arguments supplied for the generic interface.", nameof(genericInterfaceArguments)); - if (!openGenericInterfaceType.IsGenericType) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterfaceType)); + if (!openGenericInterface.IsGenericType) throw new ArgumentException("Requested type is not a generic type.", nameof(openGenericInterface)); foreach (var type in assembly.GetTypes()) { var interfaces = type.GetInterfaces(); - foreach (var interfaceType in interfaces) + foreach (var @interface in interfaces) { - if (interfaceType.IsGenericType) + if (@interface.IsGenericType) { - var genericTypeDefinition = interfaceType.GetGenericTypeDefinition(); - if (interfaceType.GetGenericArguments().First() == genericInterfaceArguments.First() &&genericTypeDefinition == openGenericInterfaceType.GetGenericTypeDefinition()) + var genericTypeDefinition = @interface.GetGenericTypeDefinition(); + if (@interface.GetGenericArguments().First() == genericInterfaceArguments.First() &&genericTypeDefinition == openGenericInterface.GetGenericTypeDefinition()) { - return ( - type, - genericTypeDefinition.MakeGenericType(genericInterfaceArguments) - ); + return (type, genericTypeDefinition.MakeGenericType(genericInterfaceArguments)); } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs index 8a2b0dc37b..0d031aa7d2 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using JsonApiDotNetCore.Configuration; diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index e72377f877..f38b199b5e 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -353,7 +353,7 @@ public class JsonApiResourceService : JsonApiResourceService { public JsonApiResourceService( - IResourceRepository repository, + IResourceRepository repository, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, IJsonApiOptions options, diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index c370354a60..5f4c324cd7 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; using Microsoft.EntityFrameworkCore; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs index 422af54634..351dd35d47 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/Meta.cs @@ -8,7 +8,6 @@ using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Definitions; using JsonApiDotNetCoreExample.Models; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs index 97a7d1b117..d98c85f20f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/SparseFieldSets/SparseFieldSetTests.cs @@ -33,10 +33,10 @@ public SparseFieldSetTests(IntegrationTestContext testCon { services.AddSingleton(); - services.AddScoped, ResultCapturingRepository>(); - services.AddScoped, ResultCapturingRepository
>(); - services.AddScoped, ResultCapturingRepository>(); - services.AddScoped, ResultCapturingRepository>(); + services.AddScoped, ResultCapturingRepository>(); + services.AddScoped, ResultCapturingRepository
>(); + services.AddScoped, ResultCapturingRepository>(); + services.AddScoped, ResultCapturingRepository>(); services.AddScoped, JsonApiResourceService
>(); }); diff --git a/test/MultiDbContextTests/MultiDbContextTests.csproj b/test/MultiDbContextTests/MultiDbContextTests.csproj new file mode 100644 index 0000000000..c319c91e7a --- /dev/null +++ b/test/MultiDbContextTests/MultiDbContextTests.csproj @@ -0,0 +1,23 @@ + + + $(NetCoreAppVersion) + + + + + PreserveNewest + + + + + + + + + + + + + + + diff --git a/test/MultiDbContextTests/ResourceTests.cs b/test/MultiDbContextTests/ResourceTests.cs new file mode 100644 index 0000000000..403f51c2e7 --- /dev/null +++ b/test/MultiDbContextTests/ResourceTests.cs @@ -0,0 +1,73 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.AspNetCore.Mvc.Testing; +using MultiDbContextExample; +using Newtonsoft.Json; +using Xunit; + +namespace MultiDbContextTests +{ + public sealed class ResourceTests : IClassFixture> + { + private readonly WebApplicationFactory _factory; + + public ResourceTests(WebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task Can_Get_ResourceAs() + { + // Arrange + var client = _factory.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/resourceAs"); + + // Act + var response = await client.SendAsync(request); + + // Assert + AssertStatusCode(HttpStatusCode.OK, response); + + string responseBody = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseBody); + + document.ManyData.Should().HaveCount(1); + document.ManyData[0].Attributes["nameA"].Should().Be("SampleA"); + } + + [Fact] + public async Task Can_Get_ResourceBs() + { + // Arrange + var client = _factory.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/resourceBs"); + + // Act + var response = await client.SendAsync(request); + + // Assert + AssertStatusCode(HttpStatusCode.OK, response); + + string responseBody = await response.Content.ReadAsStringAsync(); + var document = JsonConvert.DeserializeObject(responseBody); + + document.ManyData.Should().HaveCount(1); + document.ManyData[0].Attributes["nameB"].Should().Be("SampleB"); + } + + private static void AssertStatusCode(HttpStatusCode expected, HttpResponseMessage response) + { + if (expected != response.StatusCode) + { + var responseBody = response.Content.ReadAsStringAsync().Result; + Assert.True(false, $"Got {response.StatusCode} status code instead of {expected}. Payload: {responseBody}"); + } + } + } +} diff --git a/test/MultiDbContextTests/xunit.runner.json b/test/MultiDbContextTests/xunit.runner.json new file mode 100644 index 0000000000..8f5f10571b --- /dev/null +++ b/test/MultiDbContextTests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": false, + "maxParallelThreads": 1 +} diff --git a/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs b/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs deleted file mode 100644 index 352b3e4f4e..0000000000 --- a/test/UnitTests/Graph/IdentifiableTypeCacheTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using UnitTests.Internal; -using Xunit; - -namespace UnitTests.Graph -{ - public class IdentifiableTypeCacheTests - { - [Fact] - public void GetIdentifiableTypes_Locates_Identifiable_Resource() - { - // Arrange - var resourceType = typeof(Model); - var typeCache = new IdentifiableTypeCache(); - - // Act - var results = typeCache.GetIdentifiableTypes(resourceType.Assembly); - - // Assert - Assert.Contains(results, r => r.ResourceType == resourceType); - } - - [Fact] - public void GetIdentifiableTypes_Only_Contains_IIdentifiable_Types() - { - // Arrange - var resourceType = typeof(Model); - var typeCache = new IdentifiableTypeCache(); - - // Act - var resourceDescriptors = typeCache.GetIdentifiableTypes(resourceType.Assembly); - - // Assert - foreach(var resourceDescriptor in resourceDescriptors) - Assert.True(typeof(IIdentifiable).IsAssignableFrom(resourceDescriptor.ResourceType)); - } - } -} diff --git a/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs b/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs new file mode 100644 index 0000000000..d533400a3e --- /dev/null +++ b/test/UnitTests/Graph/ResourceDescriptorAssemblyCacheTests.cs @@ -0,0 +1,47 @@ +using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using UnitTests.Internal; +using Xunit; + +namespace UnitTests.Graph +{ + public class ResourceDescriptorAssemblyCacheTests + { + [Fact] + public void GetResourceDescriptorsPerAssembly_Locates_Identifiable_Resource() + { + // Arrange + var resourceType = typeof(Model); + + var assemblyCache = new ResourceDescriptorAssemblyCache(); + assemblyCache.RegisterAssembly(resourceType.Assembly); + + // Act + var results = assemblyCache.GetResourceDescriptorsPerAssembly(); + + // Assert + Assert.Contains(results, result => result.resourceDescriptors != null && + result.resourceDescriptors.Any(descriptor => descriptor.ResourceType == resourceType)); + } + + [Fact] + public void GetResourceDescriptorsPerAssembly_Only_Contains_IIdentifiable_Types() + { + // Arrange + var resourceType = typeof(Model); + + var assemblyCache = new ResourceDescriptorAssemblyCache(); + assemblyCache.RegisterAssembly(resourceType.Assembly); + + // Act + var results = assemblyCache.GetResourceDescriptorsPerAssembly(); + + // Assert + foreach (var resourceDescriptor in results.SelectMany(result => result.resourceDescriptors)) + { + Assert.True(typeof(IIdentifiable).IsAssignableFrom(resourceDescriptor.ResourceType)); + } + } + } +} diff --git a/test/UnitTests/Graph/TypeLocator_Tests.cs b/test/UnitTests/Graph/TypeLocator_Tests.cs index 03d17f8dcb..cc3a3803b3 100644 --- a/test/UnitTests/Graph/TypeLocator_Tests.cs +++ b/test/UnitTests/Graph/TypeLocator_Tests.cs @@ -58,13 +58,12 @@ public void GetIdType_Correctly_Identifies_JsonApiResource() { // Arrange var type = typeof(Model); - var expectedIdType = typeof(int); // Act - var idType = TypeLocator.GetIdType(type); + var idType = TypeLocator.TryGetIdType(type); // Assert - Assert.Equal(expectedIdType, idType); + Assert.Equal(typeof(int), idType); } [Fact] @@ -72,26 +71,25 @@ public void GetIdType_Correctly_Identifies_NonJsonApiResource() { // Arrange var type = typeof(DerivedType); - Type expectedIdType = null; // Act - var idType = TypeLocator.GetIdType(type); + var idType = TypeLocator.TryGetIdType(type); // Assert - Assert.Equal(expectedIdType, idType); + Assert.Null(idType); } [Fact] - public void TryGetResourceDescriptor_Returns_True_If_Type_Is_IIdentifiable() + public void TryGetResourceDescriptor_Returns_Type_If_Type_Is_IIdentifiable() { // Arrange var resourceType = typeof(Model); // Act - var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var descriptor); + var descriptor = TypeLocator.TryGetResourceDescriptor(resourceType); // Assert - Assert.True(isJsonApiResource); + Assert.NotNull(descriptor); Assert.Equal(resourceType, descriptor.ResourceType); Assert.Equal(typeof(int), descriptor.IdType); } @@ -103,18 +101,16 @@ public void TryGetResourceDescriptor_Returns_False_If_Type_Is_IIdentifiable() var resourceType = typeof(String); // Act - var isJsonApiResource = TypeLocator.TryGetResourceDescriptor(resourceType, out var _); + var descriptor = TypeLocator.TryGetResourceDescriptor(resourceType); // Assert - Assert.False(isJsonApiResource); + Assert.Null(descriptor); } } - public interface IGenericInterface { } public sealed class Implementation : IGenericInterface { } - public class BaseType { } public sealed class DerivedType : BaseType { } diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 86498d24a2..138c1f61df 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -384,7 +384,9 @@ private IDbContextResolver CreateTestDbResolver(AppDbContext dbContext) private void ResolveInverseRelationships(AppDbContext context) { - new InverseRelationships(_resourceGraph, new DbContextResolver(context)).Resolve(); + var dbContextResolvers = new[] {new DbContextResolver(context)}; + var inverseRelationships = new InverseRelationships(_resourceGraph, dbContextResolvers); + inverseRelationships.Resolve(); } private Mock> CreateResourceDefinition From f62b329762480cf861b6c7ecaf6b4b6daedf6ff8 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 29 Sep 2020 11:14:39 +0200 Subject: [PATCH 2/2] Removed the need for adding custom resolvers. --- docs/usage/extensibility/repositories.md | 34 +++++-------------- .../Data/DbContextAResolver.cs | 20 ----------- .../Data/DbContextBResolver.cs | 20 ----------- .../Repositories/DbContextARepository.cs | 2 +- .../Repositories/DbContextBRepository.cs | 2 +- src/Examples/MultiDbContextExample/Startup.cs | 3 -- .../JsonApiApplicationBuilder.cs | 12 +++++-- .../ServiceCollectionExtensions.cs | 6 +--- .../Repositories/DbContextResolver.cs | 2 ++ 9 files changed, 22 insertions(+), 79 deletions(-) delete mode 100644 src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs delete mode 100644 src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md index 6975b239ad..d4e9c27f09 100644 --- a/docs/usage/extensibility/repositories.md +++ b/docs/usage/extensibility/repositories.md @@ -45,33 +45,15 @@ public class ArticleRepository : EntityFrameworkCoreRepository
## Multiple DbContexts -If you need to use multiple Entity Framework Core DbContexts, first create an implementation of `IDbContextResolver` for each context. - -```c# -public sealed class DbContextAResolver : IDbContextResolver -{ - private readonly DbContextA _dbContextA; - - public DbContextAResolver(DbContextA dbContextA) - { - _dbContextA = dbContextA; - } - - public DbContext GetContext() - { - return _dbContextA; - } -} -``` - -Next, create a repository for each context and inject its resolver per resource type. This example shows a single `DbContextARepository` for all entities that are members of `DbContextA`. +If you need to use multiple Entity Framework Core DbContexts, first create a repository for each context and inject its typed resolver. +This example shows a single `DbContextARepository` for all entities that are members of `DbContextA`. ```c# public class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextARepository(ITargetedFields targetedFields, DbContextAResolver contextResolver, - // ^^^^^^^^^^^^^^^^^^ + public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) @@ -80,15 +62,15 @@ public class DbContextARepository : EntityFrameworkCoreRepository(); -services.AddScoped(); +// In Startup.ConfigureServices + +services.AddDbContext(options => options.UseSqlite("Data Source=A.db")); +services.AddDbContext(options => options.UseSqlite("Data Source=B.db")); services.AddScoped, DbContextARepository>(); services.AddScoped, DbContextBRepository>(); diff --git a/src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs b/src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs deleted file mode 100644 index 432ce4ea57..0000000000 --- a/src/Examples/MultiDbContextExample/Data/DbContextAResolver.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JsonApiDotNetCore.Repositories; -using Microsoft.EntityFrameworkCore; - -namespace MultiDbContextExample.Data -{ - public sealed class DbContextAResolver : IDbContextResolver - { - private readonly DbContextA _dbContextA; - - public DbContextAResolver(DbContextA dbContextA) - { - _dbContextA = dbContextA; - } - - public DbContext GetContext() - { - return _dbContextA; - } - } -} diff --git a/src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs b/src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs deleted file mode 100644 index 4d31add33d..0000000000 --- a/src/Examples/MultiDbContextExample/Data/DbContextBResolver.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JsonApiDotNetCore.Repositories; -using Microsoft.EntityFrameworkCore; - -namespace MultiDbContextExample.Data -{ - public sealed class DbContextBResolver : IDbContextResolver - { - private readonly DbContextB _dbContextB; - - public DbContextBResolver(DbContextB dbContextB) - { - _dbContextB = dbContextB; - } - - public DbContext GetContext() - { - return _dbContextB; - } - } -} diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs index be64881ce9..751c02703a 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs @@ -11,7 +11,7 @@ namespace MultiDbContextExample.Repositories public class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextARepository(ITargetedFields targetedFields, DbContextAResolver contextResolver, + public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs index e7dc6066d0..c0761187b1 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs @@ -11,7 +11,7 @@ namespace MultiDbContextExample.Repositories public class DbContextBRepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextBRepository(ITargetedFields targetedFields, DbContextBResolver contextResolver, + public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) diff --git a/src/Examples/MultiDbContextExample/Startup.cs b/src/Examples/MultiDbContextExample/Startup.cs index c06c609c69..35f274cf5f 100644 --- a/src/Examples/MultiDbContextExample/Startup.cs +++ b/src/Examples/MultiDbContextExample/Startup.cs @@ -19,9 +19,6 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(options => options.UseSqlite("Data Source=A.db")); services.AddDbContext(options => options.UseSqlite("Data Source=B.db")); - services.AddScoped(); - services.AddScoped(); - services.AddScoped, DbContextARepository>(); services.AddScoped, DbContextBRepository>(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 72eafd378e..4ffc42205a 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Hooks.Internal; using JsonApiDotNetCore.Hooks.Internal.Discovery; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -118,10 +119,15 @@ public void DiscoverInjectables() ///
public void ConfigureServiceContainer(ICollection dbContextTypes) { - foreach (var dbContextType in dbContextTypes) + if (dbContextTypes.Any()) { - var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.AddScoped(typeof(IDbContextResolver), contextResolverType); + _services.AddScoped(typeof(DbContextResolver<>)); + + foreach (var dbContextType in dbContextTypes) + { + var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.AddScoped(typeof(IDbContextResolver), contextResolverType); + } } AddResourceLayer(); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index b39df70424..0fb325d8f2 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -41,11 +41,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection IMvcCoreBuilder mvcBuilder = null) where TDbContext : DbContext { - if (services == null) throw new ArgumentNullException(nameof(services)); - - SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, new[] {typeof(TDbContext)}); - - return services; + return AddJsonApi(services, options, discovery, resources, mvcBuilder, new[] {typeof(TDbContext)}); } private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, diff --git a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs index f0f6ea166f..6a4984b7ab 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs @@ -15,5 +15,7 @@ public DbContextResolver(TDbContext context) } public DbContext GetContext() => _context; + + public TDbContext GetTypedContext() => _context; } }