From 0800bb20a005a1167a89a2ee6e29f880e6334ad4 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 10:35:04 +0200 Subject: [PATCH 01/51] feat: use resource graph in routing convention rather than formatter --- .../Startups/Startup.cs | 9 +- .../Builders/JsonApiApplicationBuilder.cs | 117 +++++++++++------- .../Extensions/ServiceCollectionExtensions.cs | 7 +- .../Graph/ServiceDiscoveryFacade.cs | 41 ++++-- .../Internal/JsonApiRoutingConvention.cs | 57 ++++----- .../Models/Annotation/ResourceAttribute.cs | 4 +- .../ClientGeneratedIdsApplicationFactory.cs | 2 +- 7 files changed, 142 insertions(+), 95 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 0b0d0eba07..f5fe24774a 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCoreExample.Data; using Microsoft.EntityFrameworkCore; using System; +using System.Linq; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.QueryStrings; @@ -38,7 +39,13 @@ public override void ConfigureServices(IServiceCollection services) options.UseNpgsql(_connectionString, innerOptions => innerOptions.SetPostgresVersion(new Version(9, 6))); }, ServiceLifetime.Transient); - services.AddJsonApi(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly()); + services.AddJsonApi(ConfigureJsonApiOptions, discovery => + { + discovery.AddCurrentAssembly(); + // discovery.AddAssembly(AppDomain.CurrentDomain + // .GetAssemblies() + // .First(a => a.FullName.Contains("JsonApiDotNetCoreExampleTests"))); + }); // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 6102e9b851..74eec5968d 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -36,6 +36,8 @@ internal sealed class JsonApiApplicationBuilder private readonly JsonApiOptions _options = new JsonApiOptions(); private IResourceGraphBuilder _resourceGraphBuilder; private Type _dbContextType; + private Action _configureResources; + private Action _configureAutoDiscovery; private readonly IServiceCollection _services; private IServiceDiscoveryFacade _serviceDiscoveryFacade; private readonly IMvcCoreBuilder _mvcBuilder; @@ -54,33 +56,42 @@ public void ConfigureJsonApiOptions(Action options) options?.Invoke(_options); } + public void RegisterResourceSources(Type dbContextType, Action autoDiscovery, + Action resources) + { + _dbContextType = dbContextType; + _configureAutoDiscovery = autoDiscovery; + _configureResources = resources; + } + /// /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: /// , , , /// and . /// - public void ConfigureMvc(Type dbContextType) + public void ConfigureMvc() { RegisterJsonApiStartupServices(); - + + using (var resourceConfigurationTemporaryProvider = _services.BuildServiceProvider()) + { + _resourceGraphBuilder = resourceConfigurationTemporaryProvider.GetRequiredService(); + _serviceDiscoveryFacade = resourceConfigurationTemporaryProvider.GetRequiredService(); + _services.AddSingleton(BuildResourceGraph(resourceConfigurationTemporaryProvider)); + } + IJsonApiExceptionFilterProvider exceptionFilterProvider; IJsonApiTypeMatchFilterProvider typeMatchFilterProvider; IJsonApiRoutingConvention routingConvention; - - using (var intermediateProvider = _services.BuildServiceProvider()) + + using (var middlewareConfigurationTemporaryProvider = _services.BuildServiceProvider()) { - _resourceGraphBuilder = intermediateProvider.GetRequiredService(); - _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); - _dbContextType = dbContextType; - - AddResourceTypesFromDbContext(intermediateProvider); - - exceptionFilterProvider = intermediateProvider.GetRequiredService(); - typeMatchFilterProvider = intermediateProvider.GetRequiredService(); - routingConvention = intermediateProvider.GetRequiredService(); + exceptionFilterProvider = middlewareConfigurationTemporaryProvider.GetRequiredService(); + typeMatchFilterProvider = middlewareConfigurationTemporaryProvider.GetRequiredService(); + routingConvention = middlewareConfigurationTemporaryProvider.GetRequiredService(); } - + _mvcBuilder.AddMvcOptions(options => { options.EnableEndpointRouting = true; @@ -100,42 +111,14 @@ public void ConfigureMvc(Type dbContextType) _services.AddSingleton(routingConvention); } - private void AddResourceTypesFromDbContext(ServiceProvider intermediateProvider) - { - if (_dbContextType != null) - { - var dbContext = (DbContext) intermediateProvider.GetRequiredService(_dbContextType); - - foreach (var entityType in dbContext.Model.GetEntityTypes()) - { - _resourceGraphBuilder.AddResource(entityType.ClrType); - } - } - } - - /// - /// Executes auto-discovery of JADNC services. - /// - public void AutoDiscover(Action autoDiscover) - { - autoDiscover?.Invoke(_serviceDiscoveryFacade); - } - - /// - /// Executes the action provided by the user to configure the resources using - /// - public void ConfigureResources(Action resources) - { - resources?.Invoke(_resourceGraphBuilder); - } - /// /// Registers the remaining internals. /// public void ConfigureServices() { - var resourceGraph = _resourceGraphBuilder.Build(); - + + ((ServiceDiscoveryFacade)_serviceDiscoveryFacade).DiscoverServices(); + if (_dbContextType != null) { var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(_dbContextType); @@ -179,10 +162,9 @@ public void ConfigureServices() _services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>)); _services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>)); - - _services.AddSingleton(resourceGraph); + _services.AddSingleton(); - _services.AddSingleton(resourceGraph); + _services.AddSingleton(sp => sp.GetRequiredService()); _services.AddSingleton(); _services.AddScoped(); @@ -270,5 +252,44 @@ private void RegisterJsonApiStartupServices() _services.TryAddScoped(); _services.TryAddScoped(); } + + private IResourceGraph BuildResourceGraph(ServiceProvider intermediateProvider) + { + AddResourceTypesFromDbContext(intermediateProvider); + AutoDiscoverResources(); + ConfigureResources(); + + return _resourceGraphBuilder.Build(); + } + + private void AddResourceTypesFromDbContext(ServiceProvider intermediateProvider) + { + if (_dbContextType != null) + { + var dbContext = (DbContext) intermediateProvider.GetRequiredService(_dbContextType); + + foreach (var entityType in dbContext.Model.GetEntityTypes()) + { + _resourceGraphBuilder.AddResource(entityType.ClrType); + } + } + } + + /// + /// Executes auto-discovery of JADNC services. + /// + private void AutoDiscoverResources() + { + _configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade); + ((ServiceDiscoveryFacade)_serviceDiscoveryFacade).DiscoverResources(); + } + + /// + /// Executes the action provided by the user to configure the resources using + /// + private void ConfigureResources() + { + _configureResources?.Invoke(_resourceGraphBuilder); + } } } diff --git a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs index 19863400ca..f37193b5a7 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs @@ -48,15 +48,14 @@ public static IServiceCollection AddJsonApi(this IServiceCollection } private static void SetupApplicationBuilder(IServiceCollection services, Action options, - Action discovery, + Action autoDiscovery, Action resources, IMvcCoreBuilder mvcBuilder, Type dbContextType) { var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); applicationBuilder.ConfigureJsonApiOptions(options); - applicationBuilder.ConfigureMvc(dbContextType); - applicationBuilder.AutoDiscover(discovery); - applicationBuilder.ConfigureResources(resources); + applicationBuilder.RegisterResourceSources(dbContextType, autoDiscovery, resources); + applicationBuilder.ConfigureMvc(); applicationBuilder.ConfigureServices(); } diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index cbe795cdce..8bf37eb48e 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -49,7 +49,7 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade private readonly IServiceCollection _services; private readonly IResourceGraphBuilder _resourceGraphBuilder; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); - + private readonly Dictionary> _discoverableAssemblies = new Dictionary>(); public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder resourceGraphBuilder) { _services = services; @@ -66,18 +66,37 @@ public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { - AddDbContextResolvers(assembly); + _discoverableAssemblies.Add(assembly, _typeCache.GetIdentifiableTypes(assembly)); + + return this; + } - var resourceDescriptors = _typeCache.GetIdentifiableTypes(assembly); - foreach (var resourceDescriptor in resourceDescriptors) + public void DiscoverResources() + { + foreach (var (assembly, resourceDescriptors) in _discoverableAssemblies) { - AddResource(assembly, resourceDescriptor); - AddServices(assembly, resourceDescriptor); - AddRepositories(assembly, resourceDescriptor); + foreach (var descriptor in resourceDescriptors) + { + AddResource(assembly, descriptor); + } } - return this; } + public void DiscoverServices() + { + foreach (var (assembly, resourceDescriptors) in _discoverableAssemblies) + { + AddDbContextResolvers(assembly); + + foreach (var descriptor in resourceDescriptors) + { + AddResourceDefinition(assembly, descriptor); + AddServices(assembly, descriptor); + AddRepositories(assembly, descriptor); + } + } + } + private void AddDbContextResolvers(Assembly assembly) { var dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); @@ -88,14 +107,14 @@ private void AddDbContextResolvers(Assembly assembly) } } + + private void AddResource(Assembly assembly, ResourceDescriptor resourceDescriptor) { - RegisterResourceDefinition(assembly, resourceDescriptor); - _resourceGraphBuilder.AddResource(resourceDescriptor.ResourceType, resourceDescriptor.IdType); } - private void RegisterResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) + private void AddResourceDefinition(Assembly assembly, ResourceDescriptor identifiable) { try { diff --git a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs index 72067044d2..fcaed4d9e2 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs @@ -5,7 +5,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -31,21 +31,22 @@ namespace JsonApiDotNetCore.Internal public class JsonApiRoutingConvention : IJsonApiRoutingConvention { private readonly IJsonApiOptions _options; - private readonly ResourceNameFormatter _formatter; + private readonly IResourceGraph _resourceGraph; private readonly HashSet _registeredTemplates = new HashSet(); - private readonly Dictionary _registeredResources = new Dictionary(); + private readonly Dictionary _registeredResources = new Dictionary(); - public JsonApiRoutingConvention(IJsonApiOptions options) + public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph) { _options = options; - _formatter = new ResourceNameFormatter(options); + _resourceGraph = resourceGraph; } /// public Type GetAssociatedResource(string controllerName) { - _registeredResources.TryGetValue(controllerName, out Type type); - return type; + _registeredResources.TryGetValue(controllerName, out var resourceContext); + // ReSharper disable once PossibleNullReferenceException + return resourceContext.ResourceType; } /// @@ -53,17 +54,24 @@ public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) { - var resourceType = GetResourceTypeFromController(controller.ControllerType); + var resourceType = ExtractResourceTypeFromController(controller.ControllerType); + var resourceContext = _resourceGraph.GetResourceContext(resourceType); - if (resourceType != null) - _registeredResources.Add(controller.ControllerName, resourceType); + if (resourceContext != null) + { + _registeredResources.Add(controller.ControllerName, resourceContext); + } if (RoutingConventionDisabled(controller) == false) + { continue; + } var template = TemplateFromResource(controller) ?? TemplateFromController(controller); if (template == null) + { throw new JsonApiSetupException($"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); + } controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; } @@ -84,15 +92,14 @@ private bool RoutingConventionDisabled(ControllerModel controller) /// private string TemplateFromResource(ControllerModel model) { - if (_registeredResources.TryGetValue(model.ControllerName, out Type resourceType)) + if (!_registeredResources.TryGetValue(model.ControllerName, out var resourceContext)) { - var template = $"{_options.Namespace}/{_formatter.FormatResourceName(resourceType)}"; - if (_registeredTemplates.Add(template)) - { - return template; - } + return null; } - return null; + + var template = $"{_options.Namespace}/{resourceContext.ResourceName}"; + + return _registeredTemplates.Add(template) ? template : null; } /// @@ -100,23 +107,17 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - string controllerName = _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); + var controllerName = _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); var template = $"{_options.Namespace}/{controllerName}"; - if (_registeredTemplates.Add(template)) - { - return template; - } - else - { - return null; - } + + return _registeredTemplates.Add(template) ? template : null; } /// - /// Determines the resource associated to a controller by inspecting generic arguments. + /// Determines the resource associated to a controller by inspecting generic arguments in its inheritance tree. /// - private Type GetResourceTypeFromController(Type type) + private Type ExtractResourceTypeFromController(Type type) { var aspNetControllerType = typeof(ControllerBase); var coreControllerType = typeof(CoreJsonApiController); diff --git a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs index 4178f27dad..d95f700f11 100644 --- a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs @@ -5,9 +5,9 @@ namespace JsonApiDotNetCore.Models.Annotation [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class ResourceAttribute : Attribute { - public ResourceAttribute(string resourceName) + public ResourceAttribute(string pluralizedResourceName) { - ResourceName = resourceName; + ResourceName = pluralizedResourceName; } public string ResourceName { get; } diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs index 4407e43642..157474e129 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs @@ -25,7 +25,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.IncludeTotalResourceCount = true; options.AllowClientGeneratedIds = true; }, - discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + discovery => discovery.AddCurrentAssembly().AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); }); } } From 0d97cba94846ea4179397411dc1acf07b15ab239 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 10:44:39 +0200 Subject: [PATCH 02/51] fix: discovery tests --- .../ServiceDiscoveryFacadeTests.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 6fae07413f..e6de3e42d0 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -54,15 +54,15 @@ public ServiceDiscoveryFacadeTests() _resourceGraphBuilder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); } - - private ServiceDiscoveryFacade Facade => new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); - + [Fact] public void AddAssembly_Adds_All_Resources_To_Graph() { // Arrange, act - Facade.AddAssembly(typeof(Person).Assembly); - + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + facade.AddAssembly(typeof(Person).Assembly); + facade.DiscoverResources(); + // Assert var resourceGraph = _resourceGraphBuilder.Build(); var personResource = resourceGraph.GetResourceContext(typeof(Person)); @@ -76,8 +76,10 @@ public void AddAssembly_Adds_All_Resources_To_Graph() public void AddCurrentAssembly_Adds_Resources_To_Graph() { // Arrange, act - Facade.AddCurrentAssembly(); - + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + facade.AddCurrentAssembly(); + facade.DiscoverResources(); + // Assert var resourceGraph = _resourceGraphBuilder.Build(); var testModelResource = resourceGraph.GetResourceContext(typeof(TestModel)); @@ -88,8 +90,10 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() public void AddCurrentAssembly_Adds_Services_To_Container() { // Arrange, act - Facade.AddCurrentAssembly(); - + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + facade.AddCurrentAssembly(); + facade.DiscoverServices(); + // Assert var services = _services.BuildServiceProvider(); var service = services.GetService>(); @@ -100,7 +104,9 @@ public void AddCurrentAssembly_Adds_Services_To_Container() public void AddCurrentAssembly_Adds_Repositories_To_Container() { // Arrange, act - Facade.AddCurrentAssembly(); + var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + facade.AddCurrentAssembly(); + facade.DiscoverServices(); // Assert var services = _services.BuildServiceProvider(); From 10efa97b140776fd13ab1901c194b885e41c790e Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 10:51:32 +0200 Subject: [PATCH 03/51] fix: null ref exception --- .../Internal/JsonApiRoutingConvention.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs index fcaed4d9e2..8b4347820f 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs @@ -44,9 +44,12 @@ public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resource /// public Type GetAssociatedResource(string controllerName) { - _registeredResources.TryGetValue(controllerName, out var resourceContext); - // ReSharper disable once PossibleNullReferenceException - return resourceContext.ResourceType; + if (_registeredResources.TryGetValue(controllerName, out var resourceContext)) + { + return resourceContext.ResourceType; + }; + + return null; } /// From 6d314aac796bf1dc92403619c9edc103f36fa7bf Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 10:53:33 +0200 Subject: [PATCH 04/51] fix: client generated id tests --- .../Factories/ClientGeneratedIdsApplicationFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs index 157474e129..088001fd74 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs @@ -25,7 +25,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.IncludeTotalResourceCount = true; options.AllowClientGeneratedIds = true; }, - discovery => discovery.AddCurrentAssembly().AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); }); } } From 22d8700af15af614363cf7ec59b9b13c8bc2ebe7 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 11:24:53 +0200 Subject: [PATCH 05/51] fix: self review 1 --- .../Startups/Startup.cs | 10 +--- src/JsonApiDotNetCore/AssemblyInfo.cs | 1 + .../Builders/JsonApiApplicationBuilder.cs | 22 ++++---- .../Graph/IServiceDiscoveryFacade.cs | 18 +++++- .../Graph/ServiceDiscoveryFacade.cs | 56 ++++++++++++------- .../ServiceDiscoveryFacadeTests.cs | 10 ++-- 6 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index f5fe24774a..ba83b79713 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -39,14 +39,8 @@ public override void ConfigureServices(IServiceCollection services) options.UseNpgsql(_connectionString, innerOptions => innerOptions.SetPostgresVersion(new Version(9, 6))); }, ServiceLifetime.Transient); - services.AddJsonApi(ConfigureJsonApiOptions, discovery => - { - discovery.AddCurrentAssembly(); - // discovery.AddAssembly(AppDomain.CurrentDomain - // .GetAssemblies() - // .First(a => a.FullName.Contains("JsonApiDotNetCoreExampleTests"))); - }); - + services.AddJsonApi(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly()); + // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); } diff --git a/src/JsonApiDotNetCore/AssemblyInfo.cs b/src/JsonApiDotNetCore/AssemblyInfo.cs index 6fa08b113d..dc72a2c8e4 100644 --- a/src/JsonApiDotNetCore/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("UnitTests")] +[assembly:InternalsVisibleTo("DiscoveryTests")] [assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] [assembly:InternalsVisibleTo("NoEntityFrameworkTests")] [assembly:InternalsVisibleTo("Benchmarks")] diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 74eec5968d..90798dde64 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -74,22 +74,22 @@ public void ConfigureMvc() { RegisterJsonApiStartupServices(); - using (var resourceConfigurationTemporaryProvider = _services.BuildServiceProvider()) + using (var resourceConfigurationIntermediateProvider = _services.BuildServiceProvider()) { - _resourceGraphBuilder = resourceConfigurationTemporaryProvider.GetRequiredService(); - _serviceDiscoveryFacade = resourceConfigurationTemporaryProvider.GetRequiredService(); - _services.AddSingleton(BuildResourceGraph(resourceConfigurationTemporaryProvider)); + _resourceGraphBuilder = resourceConfigurationIntermediateProvider.GetRequiredService(); + _serviceDiscoveryFacade = resourceConfigurationIntermediateProvider.GetRequiredService(); + _services.AddSingleton(BuildResourceGraph(resourceConfigurationIntermediateProvider)); } IJsonApiExceptionFilterProvider exceptionFilterProvider; IJsonApiTypeMatchFilterProvider typeMatchFilterProvider; IJsonApiRoutingConvention routingConvention; - using (var middlewareConfigurationTemporaryProvider = _services.BuildServiceProvider()) + using (var middlewareConfigurationIntermediateProvider = _services.BuildServiceProvider()) { - exceptionFilterProvider = middlewareConfigurationTemporaryProvider.GetRequiredService(); - typeMatchFilterProvider = middlewareConfigurationTemporaryProvider.GetRequiredService(); - routingConvention = middlewareConfigurationTemporaryProvider.GetRequiredService(); + exceptionFilterProvider = middlewareConfigurationIntermediateProvider.GetRequiredService(); + typeMatchFilterProvider = middlewareConfigurationIntermediateProvider.GetRequiredService(); + routingConvention = middlewareConfigurationIntermediateProvider.GetRequiredService(); } _mvcBuilder.AddMvcOptions(options => @@ -117,7 +117,7 @@ public void ConfigureMvc() public void ConfigureServices() { - ((ServiceDiscoveryFacade)_serviceDiscoveryFacade).DiscoverServices(); + _serviceDiscoveryFacade.DiscoverServices(); if (_dbContextType != null) { @@ -276,12 +276,12 @@ private void AddResourceTypesFromDbContext(ServiceProvider intermediateProvider) } /// - /// Executes auto-discovery of JADNC services. + /// Performs auto-discovery of JsonApiDotNetCore services. /// private void AutoDiscoverResources() { _configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade); - ((ServiceDiscoveryFacade)_serviceDiscoveryFacade).DiscoverResources(); + _serviceDiscoveryFacade.DiscoverResources(); } /// diff --git a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs index 53e4e80cc5..3eb998b86b 100644 --- a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs @@ -4,7 +4,23 @@ namespace JsonApiDotNetCore.Graph { public interface IServiceDiscoveryFacade { + /// + /// Registers the designated assembly for discovery of JsonApiDotNetCore services and resources. + /// ServiceDiscoveryFacade AddAssembly(Assembly assembly); + /// + /// Registers the current assembly for discovery of JsonApiDotNetCore services and resources. + /// ServiceDiscoveryFacade AddCurrentAssembly(); + + /// + /// Discovers JsonApiDotNetCore services in the registered assemblies and adds them to the DI container. + /// + internal void DiscoverServices(); + + /// + /// Discovers JsonApiDotNetCore resources in the registered assemblies and adds them to the resource graph. + /// + internal void DiscoverResources(); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 8bf37eb48e..d30fc4de4c 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -37,7 +37,7 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade typeof(IDeleteService<,>) }; - private static readonly HashSet RepositoryInterfaces = new HashSet { + private static readonly HashSet _repositoryInterfaces = new HashSet { typeof(IResourceRepository<>), typeof(IResourceRepository<,>), typeof(IResourceWriteRepository<>), @@ -50,44 +50,48 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade private readonly IResourceGraphBuilder _resourceGraphBuilder; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); private readonly Dictionary> _discoverableAssemblies = new Dictionary>(); + public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder resourceGraphBuilder) { _services = services; _resourceGraphBuilder = resourceGraphBuilder; } - /// - /// Adds resource, service and repository implementations to the container. - /// + /// public ServiceDiscoveryFacade AddCurrentAssembly() => AddAssembly(Assembly.GetCallingAssembly()); - /// - /// Adds resource, service and repository implementations defined in the specified assembly to the container. - /// + /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { - _discoverableAssemblies.Add(assembly, _typeCache.GetIdentifiableTypes(assembly)); + _discoverableAssemblies.Add(assembly, null); return this; } - public void DiscoverResources() + /// + void IServiceDiscoveryFacade.DiscoverResources() { - foreach (var (assembly, resourceDescriptors) in _discoverableAssemblies) + + foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies) { + var resourceDescriptors = GetOrSetResourceDescriptors(discoveredResourceDescriptors, assembly); + foreach (var descriptor in resourceDescriptors) { AddResource(assembly, descriptor); } } } - - public void DiscoverServices() + + /// + void IServiceDiscoveryFacade.DiscoverServices() { - foreach (var (assembly, resourceDescriptors) in _discoverableAssemblies) + foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies) { AddDbContextResolvers(assembly); + var resourceDescriptors = GetOrSetResourceDescriptors(discoveredResourceDescriptors, assembly); + foreach (var descriptor in resourceDescriptors) { AddResourceDefinition(assembly, descriptor); @@ -106,8 +110,6 @@ private void AddDbContextResolvers(Assembly assembly) _services.AddScoped(typeof(IDbContextResolver), resolverType); } } - - private void AddResource(Assembly assembly, ResourceDescriptor resourceDescriptor) { @@ -140,7 +142,7 @@ private void AddServices(Assembly assembly, ResourceDescriptor resourceDescripto private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach (var serviceInterface in RepositoryInterfaces) + foreach (var serviceInterface in _repositoryInterfaces) { RegisterServiceImplementations(assembly, serviceInterface, resourceDescriptor); } @@ -153,12 +155,28 @@ private void RegisterServiceImplementations(Assembly assembly, Type interfaceTyp return; } var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 ? new[] { resourceDescriptor.ResourceType, resourceDescriptor.IdType } : new[] { resourceDescriptor.ResourceType }; - var service = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + var (implementation, registrationInterface) = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); - if (service.implementation != null) + if (implementation != null) { - _services.AddScoped(service.registrationInterface, service.implementation); + _services.AddScoped(registrationInterface, implementation); } } + + private IEnumerable GetOrSetResourceDescriptors(IEnumerable discoveredResourceDescriptors, Assembly assembly) + { + IEnumerable resourceDescriptors; + if (discoveredResourceDescriptors == null) + { + resourceDescriptors = _typeCache.GetIdentifiableTypes(assembly); + _discoverableAssemblies[assembly] = resourceDescriptors; + } + else + { + resourceDescriptors = discoveredResourceDescriptors; + } + + return resourceDescriptors; + } } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index e6de3e42d0..82f2ffab3d 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -59,10 +59,10 @@ public ServiceDiscoveryFacadeTests() public void AddAssembly_Adds_All_Resources_To_Graph() { // Arrange, act - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); facade.AddAssembly(typeof(Person).Assembly); facade.DiscoverResources(); - + // Assert var resourceGraph = _resourceGraphBuilder.Build(); var personResource = resourceGraph.GetResourceContext(typeof(Person)); @@ -76,7 +76,7 @@ public void AddAssembly_Adds_All_Resources_To_Graph() public void AddCurrentAssembly_Adds_Resources_To_Graph() { // Arrange, act - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); facade.AddCurrentAssembly(); facade.DiscoverResources(); @@ -90,7 +90,7 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() public void AddCurrentAssembly_Adds_Services_To_Container() { // Arrange, act - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); facade.AddCurrentAssembly(); facade.DiscoverServices(); @@ -104,7 +104,7 @@ public void AddCurrentAssembly_Adds_Services_To_Container() public void AddCurrentAssembly_Adds_Repositories_To_Container() { // Arrange, act - var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); facade.AddCurrentAssembly(); facade.DiscoverServices(); From 0476228ad5ac805d7332d9ec10e0196c3f324886 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 11:29:25 +0200 Subject: [PATCH 06/51] chore: rename to pluralizedResourceName --- .../Builders/IResourceGraphBuilder.cs | 12 ++++++------ .../Builders/ResourceGraphBuilder.cs | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs index 143c05650d..d9635d780e 100644 --- a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -15,30 +15,30 @@ public interface IResourceGraphBuilder /// Add a json:api resource /// /// The resource model type - /// + /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable; /// /// Add a json:api resource /// /// The resource model type /// The resource model identifier type - /// + /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. /// - IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; + IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable; /// /// Add a Json:Api resource /// /// The resource model type /// The resource model identifier type - /// + /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. /// - IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedTypeName = null); + IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedResourceName = null); } } diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index f6781f93ed..5f638ce8f5 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -44,23 +44,23 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } /// - public IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable - => AddResource(pluralizedTypeName); + public IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable + => AddResource(pluralizedResourceName); /// - public IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable - => AddResource(typeof(TResource), typeof(TId), pluralizedTypeName); + public IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable + => AddResource(typeof(TResource), typeof(TId), pluralizedResourceName); /// - public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedTypeName = null) + public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedResourceName = null) { if (_resources.All(e => e.ResourceType != resourceType)) { if (resourceType.IsOrImplementsInterface(typeof(IIdentifiable))) { - pluralizedTypeName ??= FormatResourceName(resourceType); + pluralizedResourceName ??= FormatResourceName(resourceType); idType ??= TypeLocator.GetIdType(resourceType); - var resourceContext = CreateResourceContext(pluralizedTypeName, resourceType, idType); + var resourceContext = CreateResourceContext(pluralizedResourceName, resourceType, idType); _resources.Add(resourceContext); } else @@ -72,9 +72,9 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, return this; } - private ResourceContext CreateResourceContext(string pluralizedTypeName, Type resourceType, Type idType) => new ResourceContext + private ResourceContext CreateResourceContext(string pluralizedResourceName, Type resourceType, Type idType) => new ResourceContext { - ResourceName = pluralizedTypeName, + ResourceName = pluralizedResourceName, ResourceType = resourceType, IdentityType = idType, Attributes = GetAttributes(resourceType), From af01214a3e9317e80a64f1a95b8591d7a91d100e Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 11:37:23 +0200 Subject: [PATCH 07/51] fix: whitespace --- src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 90798dde64..18e186a8a2 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -116,7 +116,6 @@ public void ConfigureMvc() /// public void ConfigureServices() { - _serviceDiscoveryFacade.DiscoverServices(); if (_dbContextType != null) @@ -184,7 +183,9 @@ public void ConfigureServices() AddServerSerialization(); AddQueryStringParameterServices(); if (_options.EnableResourceHooks) + { AddResourceHooks(); + } _services.AddScoped(); } From c2484e8cd67bedca5899d0ad72e4aacf0e14af1c Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 11:52:27 +0200 Subject: [PATCH 08/51] fix: enumeration exception --- src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs | 4 ++-- test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index d30fc4de4c..1a1d85d1f5 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -72,7 +72,7 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) void IServiceDiscoveryFacade.DiscoverResources() { - foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies) + foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies.ToArray()) { var resourceDescriptors = GetOrSetResourceDescriptors(discoveredResourceDescriptors, assembly); @@ -86,7 +86,7 @@ void IServiceDiscoveryFacade.DiscoverResources() /// void IServiceDiscoveryFacade.DiscoverServices() { - foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies) + foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies.ToArray()) { AddDbContextResolvers(assembly); diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 82f2ffab3d..07fb0e8d4c 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -54,7 +54,7 @@ public ServiceDiscoveryFacadeTests() _resourceGraphBuilder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); } - + [Fact] public void AddAssembly_Adds_All_Resources_To_Graph() { From 9ac745644b1d53f470809a798cbae84ac0204e55 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 26 Aug 2020 11:57:03 +0200 Subject: [PATCH 09/51] fix: remove whitespace --- .../Factories/ClientGeneratedIdsApplicationFactory.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs index 088001fd74..61b2156122 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs @@ -24,8 +24,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.DefaultPageSize = new PageSize(5); options.IncludeTotalResourceCount = true; options.AllowClientGeneratedIds = true; - }, - discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + }, discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); }); } } From 787002a861958e4a9a05f272a71a887dcde68212 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 12:26:05 +0200 Subject: [PATCH 10/51] fix: review --- .../Startups/Startup.cs | 1 - .../Builders/JsonApiApplicationBuilder.cs | 177 ++++++++++-------- .../Extensions/ServiceCollectionExtensions.cs | 15 +- .../Graph/ServiceDiscoveryFacade.cs | 23 +-- .../ResourceHooksApplicationFactory.cs | 1 + 5 files changed, 117 insertions(+), 100 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index ba83b79713..3426b94a67 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCoreExample.Data; using Microsoft.EntityFrameworkCore; using System; -using System.Linq; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.QueryStrings; diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 18e186a8a2..8f09cbfe9b 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -34,15 +34,13 @@ namespace JsonApiDotNetCore.Builders internal sealed class JsonApiApplicationBuilder { private readonly JsonApiOptions _options = new JsonApiOptions(); - private IResourceGraphBuilder _resourceGraphBuilder; - private Type _dbContextType; - private Action _configureResources; - private Action _configureAutoDiscovery; private readonly IServiceCollection _services; private IServiceDiscoveryFacade _serviceDiscoveryFacade; + private IResourceGraphBuilder _resourceGraphBuilder; private readonly IMvcCoreBuilder _mvcBuilder; - public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) + public JsonApiApplicationBuilder(IServiceCollection services, + IMvcCoreBuilder mvcBuilder) { _services = services; _mvcBuilder = mvcBuilder; @@ -51,19 +49,11 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv /// /// Executes the action provided by the user to configure /// - public void ConfigureJsonApiOptions(Action options) - { - options?.Invoke(_options); - } - - public void RegisterResourceSources(Type dbContextType, Action autoDiscovery, - Action resources) + public void ConfigureJsonApiOptions(Action configureOptions) { - _dbContextType = dbContextType; - _configureAutoDiscovery = autoDiscovery; - _configureResources = resources; + configureOptions?.Invoke(_options); } - + /// /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: @@ -72,24 +62,16 @@ public void RegisterResourceSources(Type dbContextType, Action public void ConfigureMvc() { - RegisterJsonApiStartupServices(); - - using (var resourceConfigurationIntermediateProvider = _services.BuildServiceProvider()) - { - _resourceGraphBuilder = resourceConfigurationIntermediateProvider.GetRequiredService(); - _serviceDiscoveryFacade = resourceConfigurationIntermediateProvider.GetRequiredService(); - _services.AddSingleton(BuildResourceGraph(resourceConfigurationIntermediateProvider)); - } - IJsonApiExceptionFilterProvider exceptionFilterProvider; IJsonApiTypeMatchFilterProvider typeMatchFilterProvider; IJsonApiRoutingConvention routingConvention; - using (var middlewareConfigurationIntermediateProvider = _services.BuildServiceProvider()) + using (var intermediateProvider = _services.BuildServiceProvider()) { - exceptionFilterProvider = middlewareConfigurationIntermediateProvider.GetRequiredService(); - typeMatchFilterProvider = middlewareConfigurationIntermediateProvider.GetRequiredService(); - routingConvention = middlewareConfigurationIntermediateProvider.GetRequiredService(); + exceptionFilterProvider = intermediateProvider.GetRequiredService(); + typeMatchFilterProvider = intermediateProvider.GetRequiredService(); + routingConvention = intermediateProvider.GetRequiredService(); + _services.AddSingleton(routingConvention); } _mvcBuilder.AddMvcOptions(options => @@ -107,20 +89,41 @@ public void ConfigureMvc() { _mvcBuilder.AddDataAnnotations(); } - - _services.AddSingleton(routingConvention); } + /// + /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. + /// + public void AddResourceGraph(Type dbContextType, Action configureResources) + { + using var intermediateProvider = _services.BuildServiceProvider(); + AddResourcesFromDbContext(dbContextType, intermediateProvider, _resourceGraphBuilder); + AutoDiscoverResources(_serviceDiscoveryFacade); + UserConfigureResources(configureResources, _resourceGraphBuilder); + _services.AddSingleton(_resourceGraphBuilder.Build()); + } + + public void ConfigureAutoDiscovery(Action configureAutoDiscovery) + { + using var intermediateProvider = _services.BuildServiceProvider(); + _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); + _resourceGraphBuilder = intermediateProvider.GetRequiredService(); + RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); + } + + private void RegisterDiscoverableAssemblies(Action configureAutoDiscovery, IServiceDiscoveryFacade serviceDiscoveryFacade) + { + configureAutoDiscovery?.Invoke(serviceDiscoveryFacade); + } + /// /// Registers the remaining internals. /// - public void ConfigureServices() + public void ConfigureServices(Type dbContextType) { - _serviceDiscoveryFacade.DiscoverServices(); - - if (_dbContextType != null) + if (dbContextType != null) { - var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(_dbContextType); + var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); _services.AddScoped(typeof(IDbContextResolver), contextResolverType); } else @@ -129,12 +132,56 @@ public void ConfigureServices() _services.AddSingleton(new DbContextOptionsBuilder().Options); } + AddRepositoryLayer(); + AddServiceLayer(); + + _services.AddSingleton(); + _services.AddSingleton(sp => sp.GetRequiredService()); + _services.AddSingleton(); + + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + + AddServerSerialization(); + AddQueryStringParameterServices(); + if (_options.EnableResourceHooks) + { + AddResourceHooks(); + } + + _services.AddScoped(); + } + + /// + /// Discovers DI registrable services in the assemblies marked for discovery. + /// + public void DiscoverServices() + { + _serviceDiscoveryFacade.DiscoverServices(); + } + + private void AddRepositoryLayer() + { _services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>)); _services.AddScoped(typeof(IResourceRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); _services.AddScoped(typeof(IResourceReadRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); _services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + } + private void AddServiceLayer() + { _services.AddScoped(typeof(ICreateService<>), typeof(JsonApiResourceService<>)); _services.AddScoped(typeof(ICreateService<,>), typeof(JsonApiResourceService<,>)); @@ -161,33 +208,6 @@ public void ConfigureServices() _services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>)); _services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>)); - - _services.AddSingleton(); - _services.AddSingleton(sp => sp.GetRequiredService()); - _services.AddSingleton(); - - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - - AddServerSerialization(); - AddQueryStringParameterServices(); - if (_options.EnableResourceHooks) - { - AddResourceHooks(); - } - - _services.AddScoped(); } private void AddQueryStringParameterServices() @@ -244,7 +264,10 @@ private void AddServerSerialization() _services.AddScoped(); } - private void RegisterJsonApiStartupServices() + /// + /// Registers services that are required for the configuration of JsonApiDotNetCore during the start up. + /// + public void RegisterJsonApiStartupServices() { _services.AddSingleton(_options); _services.TryAddSingleton(); @@ -254,24 +277,15 @@ private void RegisterJsonApiStartupServices() _services.TryAddScoped(); } - private IResourceGraph BuildResourceGraph(ServiceProvider intermediateProvider) - { - AddResourceTypesFromDbContext(intermediateProvider); - AutoDiscoverResources(); - ConfigureResources(); - - return _resourceGraphBuilder.Build(); - } - - private void AddResourceTypesFromDbContext(ServiceProvider intermediateProvider) + private void AddResourcesFromDbContext(Type dbContextType, ServiceProvider intermediateProvider, IResourceGraphBuilder builder) { - if (_dbContextType != null) + if (dbContextType != null) { - var dbContext = (DbContext) intermediateProvider.GetRequiredService(_dbContextType); + var dbContext = (DbContext) intermediateProvider.GetRequiredService(dbContextType); foreach (var entityType in dbContext.Model.GetEntityTypes()) { - _resourceGraphBuilder.AddResource(entityType.ClrType); + builder.AddResource(entityType.ClrType); } } } @@ -279,18 +293,17 @@ private void AddResourceTypesFromDbContext(ServiceProvider intermediateProvider) /// /// Performs auto-discovery of JsonApiDotNetCore services. /// - private void AutoDiscoverResources() + private void AutoDiscoverResources(IServiceDiscoveryFacade serviceDiscoveryFacade) { - _configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade); - _serviceDiscoveryFacade.DiscoverResources(); + serviceDiscoveryFacade.DiscoverResources(); } /// /// Executes the action provided by the user to configure the resources using /// - private void ConfigureResources() + private void UserConfigureResources(Action configureResources, IResourceGraphBuilder resourceGraphBuilder) { - _configureResources?.Invoke(_resourceGraphBuilder); + configureResources?.Invoke(resourceGraphBuilder); } } } diff --git a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs index f37193b5a7..126e74897f 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs @@ -47,16 +47,19 @@ public static IServiceCollection AddJsonApi(this IServiceCollection return services; } - private static void SetupApplicationBuilder(IServiceCollection services, Action options, - Action autoDiscovery, - Action resources, IMvcCoreBuilder mvcBuilder, Type dbContextType) + private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, + Action configureAutoDiscovery, + Action configureResources, IMvcCoreBuilder mvcBuilder, Type dbContextType) { var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); - applicationBuilder.ConfigureJsonApiOptions(options); - applicationBuilder.RegisterResourceSources(dbContextType, autoDiscovery, resources); + applicationBuilder.ConfigureJsonApiOptions(configureOptions); + applicationBuilder.RegisterJsonApiStartupServices(); + applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); + applicationBuilder.AddResourceGraph(dbContextType, configureResources); applicationBuilder.ConfigureMvc(); - applicationBuilder.ConfigureServices(); + applicationBuilder.DiscoverServices(); + applicationBuilder.ConfigureServices(dbContextType); } private static void ResolveInverseRelationships(IServiceCollection services) diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 1a1d85d1f5..01ed41ae01 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -49,7 +49,7 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade private readonly IServiceCollection _services; private readonly IResourceGraphBuilder _resourceGraphBuilder; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); - private readonly Dictionary> _discoverableAssemblies = new Dictionary>(); + private readonly Dictionary> _resourceDescriptorsPerAssemblyCache = new Dictionary>(); public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder resourceGraphBuilder) { @@ -63,7 +63,7 @@ public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { - _discoverableAssemblies.Add(assembly, null); + _resourceDescriptorsPerAssemblyCache.Add(assembly, null); return this; } @@ -71,10 +71,9 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) /// void IServiceDiscoveryFacade.DiscoverResources() { - - foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies.ToArray()) + foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { - var resourceDescriptors = GetOrSetResourceDescriptors(discoveredResourceDescriptors, assembly); + var resourceDescriptors = GetResourceDescriptorsFromCache(discoveredResourceDescriptors, assembly); foreach (var descriptor in resourceDescriptors) { @@ -86,11 +85,11 @@ void IServiceDiscoveryFacade.DiscoverResources() /// void IServiceDiscoveryFacade.DiscoverServices() { - foreach (var (assembly, discoveredResourceDescriptors) in _discoverableAssemblies.ToArray()) + foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { AddDbContextResolvers(assembly); - var resourceDescriptors = GetOrSetResourceDescriptors(discoveredResourceDescriptors, assembly); + var resourceDescriptors = GetResourceDescriptorsFromCache(discoveredResourceDescriptors, assembly); foreach (var descriptor in resourceDescriptors) { @@ -124,7 +123,9 @@ private void AddResourceDefinition(Assembly assembly, ResourceDescriptor identif .SingleOrDefault(); if (resourceDefinition != null) + { _services.AddScoped(typeof(ResourceDefinition<>).MakeGenericType(identifiable.ResourceType), resourceDefinition); + } } catch (InvalidOperationException e) { @@ -163,13 +164,13 @@ private void RegisterServiceImplementations(Assembly assembly, Type interfaceTyp } } - private IEnumerable GetOrSetResourceDescriptors(IEnumerable discoveredResourceDescriptors, Assembly assembly) + private IList GetResourceDescriptorsFromCache(IList discoveredResourceDescriptors, Assembly assembly) { - IEnumerable resourceDescriptors; + IList resourceDescriptors; if (discoveredResourceDescriptors == null) { - resourceDescriptors = _typeCache.GetIdentifiableTypes(assembly); - _discoverableAssemblies[assembly] = resourceDescriptors; + resourceDescriptors = (IList)_typeCache.GetIdentifiableTypes(assembly); + _resourceDescriptorsPerAssemblyCache[assembly] = resourceDescriptors; } else { diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs index 5861a3f63b..7d9e8afbcf 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ResourceHooksApplicationFactory.cs @@ -1,5 +1,6 @@ using System.Reflection; using JsonApiDotNetCore; +using JsonApiDotNetCoreExample.Data; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; From dc0c75ac0cc49d3024121c78eeddc321feb9c3c2 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 12:28:09 +0200 Subject: [PATCH 11/51] fix: review --- .../Builders/IResourceGraphBuilder.cs | 12 ++++++------ .../Builders/ResourceGraphBuilder.cs | 18 +++++++++--------- .../Models/Annotation/ResourceAttribute.cs | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs index d9635d780e..29eaf6a7ce 100644 --- a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -15,30 +15,30 @@ public interface IResourceGraphBuilder /// Add a json:api resource /// /// The resource model type - /// + /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. /// - IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable; + IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable; /// /// Add a json:api resource /// /// The resource model type /// The resource model identifier type - /// + /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. /// - IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable; + IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable; /// /// Add a Json:Api resource /// /// The resource model type /// The resource model identifier type - /// + /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. /// - IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedResourceName = null); + IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string publicResourceName = null); } } diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index 5f638ce8f5..e7658c176a 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -44,23 +44,23 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } /// - public IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable - => AddResource(pluralizedResourceName); + public IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable + => AddResource(publicResourceName); /// - public IResourceGraphBuilder AddResource(string pluralizedResourceName = null) where TResource : class, IIdentifiable - => AddResource(typeof(TResource), typeof(TId), pluralizedResourceName); + public IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable + => AddResource(typeof(TResource), typeof(TId), publicResourceName); /// - public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string pluralizedResourceName = null) + public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string publicResourceName = null) { if (_resources.All(e => e.ResourceType != resourceType)) { if (resourceType.IsOrImplementsInterface(typeof(IIdentifiable))) { - pluralizedResourceName ??= FormatResourceName(resourceType); + publicResourceName ??= FormatResourceName(resourceType); idType ??= TypeLocator.GetIdType(resourceType); - var resourceContext = CreateResourceContext(pluralizedResourceName, resourceType, idType); + var resourceContext = CreateResourceContext(publicResourceName, resourceType, idType); _resources.Add(resourceContext); } else @@ -72,9 +72,9 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, return this; } - private ResourceContext CreateResourceContext(string pluralizedResourceName, Type resourceType, Type idType) => new ResourceContext + private ResourceContext CreateResourceContext(string publicResourceName, Type resourceType, Type idType) => new ResourceContext { - ResourceName = pluralizedResourceName, + ResourceName = publicResourceName, ResourceType = resourceType, IdentityType = idType, Attributes = GetAttributes(resourceType), diff --git a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs index d95f700f11..03632e8318 100644 --- a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs +++ b/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs @@ -5,9 +5,9 @@ namespace JsonApiDotNetCore.Models.Annotation [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class ResourceAttribute : Attribute { - public ResourceAttribute(string pluralizedResourceName) + public ResourceAttribute(string publicResourceName) { - ResourceName = pluralizedResourceName; + ResourceName = publicResourceName; } public string ResourceName { get; } From 05f0ad941f74d5938ab3d8b689b9e0643e55057b Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 12:39:00 +0200 Subject: [PATCH 12/51] test: resource definition discovery test --- .../Builders/JsonApiApplicationBuilder.cs | 2 +- .../ServiceDiscoveryFacadeTests.cs | 35 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 8f09cbfe9b..bbaf5d02e3 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -124,7 +124,7 @@ public void ConfigureServices(Type dbContextType) if (dbContextType != null) { var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.AddScoped(typeof(IDbContextResolver), contextResolverType); + _services.TryAddScoped(typeof(IDbContextResolver), contextResolverType); } else { diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 07fb0e8d4c..ce4b075e22 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -58,12 +58,12 @@ public ServiceDiscoveryFacadeTests() [Fact] public void AddAssembly_Adds_All_Resources_To_Graph() { - // Arrange, act + // Arrange IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); facade.AddAssembly(typeof(Person).Assembly); facade.DiscoverResources(); - // Assert + // Act var resourceGraph = _resourceGraphBuilder.Build(); var personResource = resourceGraph.GetResourceContext(typeof(Person)); var articleResource = resourceGraph.GetResourceContext(typeof(Article)); @@ -75,8 +75,10 @@ public void AddAssembly_Adds_All_Resources_To_Graph() [Fact] public void AddCurrentAssembly_Adds_Resources_To_Graph() { - // Arrange, act + // Arrange IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + + // Act facade.AddCurrentAssembly(); facade.DiscoverResources(); @@ -89,8 +91,10 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() [Fact] public void AddCurrentAssembly_Adds_Services_To_Container() { - // Arrange, act + // Arrange IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + + // Act facade.AddCurrentAssembly(); facade.DiscoverServices(); @@ -103,8 +107,10 @@ public void AddCurrentAssembly_Adds_Services_To_Container() [Fact] public void AddCurrentAssembly_Adds_Repositories_To_Container() { - // Arrange, act + // Arrange IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + + // Act facade.AddCurrentAssembly(); facade.DiscoverServices(); @@ -113,6 +119,20 @@ public void AddCurrentAssembly_Adds_Repositories_To_Container() Assert.IsType(services.GetService>()); } + [Fact] + public void AddCurrentAssembly_Adds_ResourceDefinitions_To_Container() + { + // Arrange + IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + + // Act + facade.AddCurrentAssembly(); + facade.DiscoverServices(); + + // Assert + var services = _services.BuildServiceProvider(); + Assert.IsType(services.GetService>()); + } public sealed class TestModel : Identifiable { } public class TestModelService : JsonApiResourceService @@ -147,5 +167,10 @@ public TestModelRepository( : base(targetedFields, _dbContextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory) { } } + + public class TestModelResourceDefinition : ResourceDefinition + { + public TestModelResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + } } } From 28e76fab4f91461c234be53b9884f05605b59f9e Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 15:18:07 +0200 Subject: [PATCH 13/51] docs: update routing related docs --- docs/usage/resource-graph.md | 75 ++++++++++++------ docs/usage/routing.md | 79 ++++++++++++------- .../Builders/JsonApiApplicationBuilder.cs | 2 +- 3 files changed, 102 insertions(+), 54 deletions(-) diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 529969d146..8b99f6ba5d 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -10,31 +10,30 @@ It is built at app startup and available as a singleton through Dependency Injec There are three ways the resource graph can be created: -1. Auto-discovery +1. Manually specifying each resource 2. Specifying an entire DbContext -3. Manually specifying each resource +3. Auto-discovery -### Auto-Discovery +It is also possible to combine the three of them at once. Be aware that some configuration might overlap, +for example you could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration +is prioritized by the order of the list above. -Auto-discovery refers to the process of reflecting on an assembly and -detecting all of the json:api resources and services. +### Manual Specification -The following command will build the resource graph using all `IIdentifiable` -implementations. It also injects resource definitions and service layer overrides which we will -cover in a later section. You can enable auto-discovery for the -current assembly by adding the following to your `Startup` class. +You can manually construct the graph. ```c# // Startup.cs public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi( - options => { /* ... */ }, - discovery => discovery.AddCurrentAssembly()); + services.AddJsonApi(resources: builder => + { + builder.AddResource(); + }); } ``` -### Entity Framework Core DbContext +### Specifying an Entity Framework Core DbContext If you are using Entity Framework Core as your ORM, you can add an entire `DbContext` with one line. @@ -58,33 +57,61 @@ public void ConfigureServices(IServiceCollection services) } ``` -### Manual Specification +### Auto-discovery -You can also manually construct the graph. +Auto-discovery refers to the process of reflecting on an assembly and +detecting all of the json:api resources and services. + +The following command will build the resource graph using all `IIdentifiable` +implementations. It also injects resource definitions and service layer overrides which we will +cover in a later section. You can enable auto-discovery for the +current assembly by adding the following to your `Startup` class. ```c# // Startup.cs public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi(resources: builder => - { - builder.AddResource(); - }); + services.AddJsonApi( + options => { /* ... */ }, + discovery => discovery.AddCurrentAssembly()); } ``` -### Public Resource Type Name +### Public Resource Name -The public resource type name is determined by the following criteria (in order of priority): +The public resource name is exposed in the json:api payload as the `type` member. +How this is exposed can be configured in with the following approaches (in order of priority): -1. The model is decorated with a `ResourceAttribute` +1. The `publicResourceName` option when manually adding a resource to the graph ```c# -[Resource("my-models")] +services.AddJsonApi(resources: builder => +{ + builder.AddResource(publicResourceName: "people"); +}); +``` + +2. The model is decorated with a `ResourceAttribute` +```c# +[Resource("myResources")] public class MyModel : Identifiable { /* ... */ } ``` -2. The configured naming convention (by default this is camel-case). +3. The configured naming convention (by default this is camel-case). ```c# // this will be registered as "myModels" public class MyModel : Identifiable { /* ... */ } ``` +This convention can be changed by setting the `SerializerSettings` property on `IJsonApiOptions`. +```c# +public void ConfigureServices(IServiceCollection services) +{ + services.AddJsonApi( + options => + { + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + } + }); +} +``` diff --git a/docs/usage/routing.md b/docs/usage/routing.md index f6ddc72e37..df2a706539 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -1,14 +1,55 @@ # Routing - -By default the library will configure routes for each controller. -Based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased. +The library will configure routes for each controller. By default, based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased. ```http GET /api/compoundModels HTTP/1.1 ``` -## Namespacing and Versioning URLs +There are two ways the library will try to create a route for a controller: +1. **By inspecting the controller for an associated resource**. The library will try to first use the public resource name of the resource associated to a controller. This means that the value of the `type` member of the json:api document for a resource will be equal to the route. +Note that this implies that it is possible to configure a route configuring the exposed resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how this can be achieved. +For example: +```c# +// controller +public class MyResourceController : JsonApiController { /* .... */ } + +// request +GET /myApiResources HTTP/1.1 + +// response +HTTP/1.1 200 OK +Content-Type: application/vnd.api+json + +{ + "data": [{ + "type": "myApiResources", + "id": "1", + "attributes": { ... } + }] +} +``` +2. **By using the name of the controller**. If no associated resource was detected for a controller, the library will construct a route from the name of the controller by using the configured naming strategy (*camelCase* by default, see [this section](~/usage/resource-graph.md#public-resource-name) on how to configure this). +In the following example the controller does not inherit from `BaseJsonApiController` and the library is unable associate a resource to it. +```c# +// controller +public class MyResourceController : ControllerBase { /* .... */ } + +// request +GET /myResources HTTP/1.1 +``` +## Customized the Routing Convention +It is possible to fully customize routing behaviour by registering a `IJsonApiRoutingConvention` implementation **before** calling `AddJsonApi( ... )`. +```c# +// Startup.cs +public void ConfigureServices(IServiceCollection services) +{ + services.AddSingleton(); + services.AddJsonApi( /* ... */ ); +} +``` + +## Namespacing and Versioning URLs You can add a namespace to all URLs by specifying it in ConfigureServices ```c# @@ -20,16 +61,16 @@ public void ConfigureServices(IServiceCollection services) ``` Which results in URLs like: https://yourdomain.com/api/v1/people -## Disable Convention - -You can disable the default casing convention and specify your own template by using the `DisableRoutingConvention` attribute. +## Disabling the Default Routing Convention +It is possible to completely bypass the default routing convention for a particular controller and specify a custom routing template by using the `DisableRoutingConvention` attribute. +In the following example, the `CamelCasedModel` resource can be accessed on `/myCustomResources` (assuming that the default naming strategy is used). ```c# [Route("[controller]")] [DisableRoutingConvention] -public class CamelCasedModelsController : JsonApiController +public class MyCustomResourceController : JsonApiController { - public CamelCasedModelsController( + public MyCustomResourceController( IJsonApiOptions jsonApiOptions, ILoggerFactory loggerFactory, IResourceService resourceService) @@ -37,23 +78,3 @@ public class CamelCasedModelsController : JsonApiController { } } ``` - -It is important to note that your routes must still end with the model name in the same format as the resource name. This is so that we can build accurate resource links in the json:api document. For example, if you define a resource as MyModels, the controller route must match. - -```c# -public void ConfigureServices(IServiceCollection services) -{ - services.AddJsonApi(resources: builder => - builder.AddResource("my-models")); // kebab-cased -} - -// controller definition -[Route("api/my-models"), DisableRoutingConvention] -public class MyModelsController : JsonApiController -{ - //... -} -``` - -See [this](~/usage/resource-graph.md#public-resource-type-name) for -more information on how the resource name is determined. diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index bbaf5d02e3..cb4264534d 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -97,8 +97,8 @@ public void ConfigureMvc() public void AddResourceGraph(Type dbContextType, Action configureResources) { using var intermediateProvider = _services.BuildServiceProvider(); - AddResourcesFromDbContext(dbContextType, intermediateProvider, _resourceGraphBuilder); AutoDiscoverResources(_serviceDiscoveryFacade); + AddResourcesFromDbContext(dbContextType, intermediateProvider, _resourceGraphBuilder); UserConfigureResources(configureResources, _resourceGraphBuilder); _services.AddSingleton(_resourceGraphBuilder.Build()); } From 978e9dd2ac64447ca148d1e7219f2de0bef63d07 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 15:21:40 +0200 Subject: [PATCH 14/51] docs: rephrase --- docs/usage/resource-graph.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 8b99f6ba5d..a3a01250a4 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -79,8 +79,7 @@ public void ConfigureServices(IServiceCollection services) ### Public Resource Name -The public resource name is exposed in the json:api payload as the `type` member. -How this is exposed can be configured in with the following approaches (in order of priority): +The public resource name is exposed through the `type` member in the json:api payload. This can be configured by the following approaches (in order of priority): 1. The `publicResourceName` option when manually adding a resource to the graph ```c# From 9d90952c51b37d641d220f0e3b4d75879be7e931 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 15:38:30 +0200 Subject: [PATCH 15/51] chore: review --- .../Builders/JsonApiApplicationBuilder.cs | 4 ++-- .../Extensions/ServiceCollectionExtensions.cs | 2 +- .../Graph/IServiceDiscoveryFacade.cs | 2 +- .../Graph/ServiceDiscoveryFacade.cs | 2 +- .../Internal/JsonApiRoutingConvention.cs | 14 ++++++++------ test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs | 6 +++--- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index cb4264534d..3dd634895a 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -166,9 +166,9 @@ public void ConfigureServices(Type dbContextType) /// /// Discovers DI registrable services in the assemblies marked for discovery. /// - public void DiscoverServices() + public void DiscoverInjectables() { - _serviceDiscoveryFacade.DiscoverServices(); + _serviceDiscoveryFacade.DiscoverInjectables(); } private void AddRepositoryLayer() diff --git a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs index 126e74897f..fea19dfaa0 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs @@ -58,7 +58,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); applicationBuilder.AddResourceGraph(dbContextType, configureResources); applicationBuilder.ConfigureMvc(); - applicationBuilder.DiscoverServices(); + applicationBuilder.DiscoverInjectables(); applicationBuilder.ConfigureServices(dbContextType); } diff --git a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs index 3eb998b86b..ce10bd8172 100644 --- a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs @@ -16,7 +16,7 @@ public interface IServiceDiscoveryFacade /// /// Discovers JsonApiDotNetCore services in the registered assemblies and adds them to the DI container. /// - internal void DiscoverServices(); + internal void DiscoverInjectables(); /// /// Discovers JsonApiDotNetCore resources in the registered assemblies and adds them to the resource graph. diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 01ed41ae01..7628a049a2 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -83,7 +83,7 @@ void IServiceDiscoveryFacade.DiscoverResources() } /// - void IServiceDiscoveryFacade.DiscoverServices() + void IServiceDiscoveryFacade.DiscoverInjectables() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { diff --git a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs index 8b4347820f..76f336bbd0 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs @@ -95,14 +95,16 @@ private bool RoutingConventionDisabled(ControllerModel controller) /// private string TemplateFromResource(ControllerModel model) { - if (!_registeredResources.TryGetValue(model.ControllerName, out var resourceContext)) + if (_registeredResources.TryGetValue(model.ControllerName, out var resourceContext)) { - return null; + var template = $"{_options.Namespace}/{resourceContext.ResourceName}"; + if (_registeredTemplates.Add(template)) + { + return template; + } } - - var template = $"{_options.Namespace}/{resourceContext.ResourceName}"; - - return _registeredTemplates.Add(template) ? template : null; + + return null; } /// diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index ce4b075e22..b9590f9220 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -96,7 +96,7 @@ public void AddCurrentAssembly_Adds_Services_To_Container() // Act facade.AddCurrentAssembly(); - facade.DiscoverServices(); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); @@ -112,7 +112,7 @@ public void AddCurrentAssembly_Adds_Repositories_To_Container() // Act facade.AddCurrentAssembly(); - facade.DiscoverServices(); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); @@ -127,7 +127,7 @@ public void AddCurrentAssembly_Adds_ResourceDefinitions_To_Container() // Act facade.AddCurrentAssembly(); - facade.DiscoverServices(); + facade.DiscoverInjectables(); // Assert var services = _services.BuildServiceProvider(); From d91bf768bd2f201b87011b54f428617ffd017cd1 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 27 Aug 2020 15:26:37 +0200 Subject: [PATCH 16/51] Update routing.md Rephrasing --- docs/usage/routing.md | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index df2a706539..7c34d5b2c6 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -7,11 +7,10 @@ GET /api/compoundModels HTTP/1.1 There are two ways the library will try to create a route for a controller: 1. **By inspecting the controller for an associated resource**. The library will try to first use the public resource name of the resource associated to a controller. This means that the value of the `type` member of the json:api document for a resource will be equal to the route. -Note that this implies that it is possible to configure a route configuring the exposed resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how this can be achieved. -For example: +Note that this implies that it is possible to configure a route configuring the exposed resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how this can be achieved. Example: ```c# // controller -public class MyResourceController : JsonApiController { /* .... */ } +public class MyResourceController : JsonApiController { /* .... */ } // note that the route is NOT "myResources", but "myApiResources" // request GET /myApiResources HTTP/1.1 @@ -28,8 +27,8 @@ Content-Type: application/vnd.api+json }] } ``` -2. **By using the name of the controller**. If no associated resource was detected for a controller, the library will construct a route from the name of the controller by using the configured naming strategy (*camelCase* by default, see [this section](~/usage/resource-graph.md#public-resource-name) on how to configure this). -In the following example the controller does not inherit from `BaseJsonApiController` and the library is unable associate a resource to it. +2. **By using the name of the controller**. If no associated resource was detected for a controller, the library will construct a route from the name of the controller by using the configured naming strategy (*camelCase* by default, see [this section](~/usage/resource-graph.md#public-resource-name) on how to configure this). This is in alignment with the default .NET Core MVC routing approach. +In the following example the controller is not associated to a resource by the library because it does not inherit from `BaseJsonApiController`. ```c# // controller public class MyResourceController : ControllerBase { /* .... */ } @@ -63,18 +62,9 @@ Which results in URLs like: https://yourdomain.com/api/v1/people ## Disabling the Default Routing Convention It is possible to completely bypass the default routing convention for a particular controller and specify a custom routing template by using the `DisableRoutingConvention` attribute. -In the following example, the `CamelCasedModel` resource can be accessed on `/myCustomResources` (assuming that the default naming strategy is used). +In the following example, the `CamelCasedModel` resource can be accessed on `/myCustomResources` (this assumes that the default naming strategy is unchanged). ```c# -[Route("[controller]")] -[DisableRoutingConvention] -public class MyCustomResourceController : JsonApiController -{ - public MyCustomResourceController( - IJsonApiOptions jsonApiOptions, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(jsonApiOptions, loggerFactory, resourceService) - { } -} +[Route("[controller]"), DisableRoutingConvention] +public class MyCustomResourceController : JsonApiController { /* ... */ } ``` From 08ab1acc75c006c06f96805158a55ac6d78ac39b Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 27 Aug 2020 15:27:53 +0200 Subject: [PATCH 17/51] Update routing.md fix example fix: bump appveyor build fix: undo c# 8 syntax for using statements fix: attempt to fix win dispose bug fix: bump build fix: appveyor bump fix: appveyor build fix final --- docs/usage/routing.md | 4 +- .../Builders/JsonApiApplicationBuilder.cs | 482 +++++++++--------- .../Internal/JsonApiRoutingConvention.cs | 2 +- 3 files changed, 251 insertions(+), 237 deletions(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 7c34d5b2c6..526c79b24c 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -62,9 +62,9 @@ Which results in URLs like: https://yourdomain.com/api/v1/people ## Disabling the Default Routing Convention It is possible to completely bypass the default routing convention for a particular controller and specify a custom routing template by using the `DisableRoutingConvention` attribute. -In the following example, the `CamelCasedModel` resource can be accessed on `/myCustomResources` (this assumes that the default naming strategy is unchanged). +In the following example, the `CamelCasedModel` resource can be accessed on `/my-custom-route`. ```c# -[Route("[controller]"), DisableRoutingConvention] +[Route("my-custom-route"), DisableRoutingConvention] public class MyCustomResourceController : JsonApiController { /* ... */ } ``` diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 3dd634895a..7848e7612e 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Data; using JsonApiDotNetCore.Formatters; @@ -33,277 +34,290 @@ namespace JsonApiDotNetCore.Builders /// internal sealed class JsonApiApplicationBuilder { - private readonly JsonApiOptions _options = new JsonApiOptions(); - private readonly IServiceCollection _services; - private IServiceDiscoveryFacade _serviceDiscoveryFacade; - private IResourceGraphBuilder _resourceGraphBuilder; - private readonly IMvcCoreBuilder _mvcBuilder; - - public JsonApiApplicationBuilder(IServiceCollection services, - IMvcCoreBuilder mvcBuilder) - { - _services = services; - _mvcBuilder = mvcBuilder; - } + private readonly JsonApiOptions _options = new JsonApiOptions(); + private readonly IServiceCollection _services; + private IServiceDiscoveryFacade _serviceDiscoveryFacade; + private IResourceGraphBuilder _resourceGraphBuilder; + private readonly IMvcCoreBuilder _mvcBuilder; + private ServiceProvider _serviceProviderToDispose; + + public JsonApiApplicationBuilder(IServiceCollection services, + IMvcCoreBuilder mvcBuilder) + { + _services = services; + _mvcBuilder = mvcBuilder; + } - /// - /// Executes the action provided by the user to configure - /// - public void ConfigureJsonApiOptions(Action configureOptions) - { - configureOptions?.Invoke(_options); - } - - /// - /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. - /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: - /// , , , - /// and . - /// - public void ConfigureMvc() - { - IJsonApiExceptionFilterProvider exceptionFilterProvider; - IJsonApiTypeMatchFilterProvider typeMatchFilterProvider; - IJsonApiRoutingConvention routingConvention; - - using (var intermediateProvider = _services.BuildServiceProvider()) - { - exceptionFilterProvider = intermediateProvider.GetRequiredService(); - typeMatchFilterProvider = intermediateProvider.GetRequiredService(); - routingConvention = intermediateProvider.GetRequiredService(); - _services.AddSingleton(routingConvention); - } - - _mvcBuilder.AddMvcOptions(options => - { - options.EnableEndpointRouting = true; - options.Filters.Add(exceptionFilterProvider.Get()); - options.Filters.Add(typeMatchFilterProvider.Get()); - options.Filters.Add(new ConvertEmptyActionResultFilter()); - options.InputFormatters.Insert(0, new JsonApiInputFormatter()); - options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); - options.Conventions.Insert(0, routingConvention); - }); - - if (_options.ValidateModelState) - { - _mvcBuilder.AddDataAnnotations(); - } - } + /// + /// Executes the action provided by the user to configure + /// + public void ConfigureJsonApiOptions(Action configureOptions) + { + configureOptions?.Invoke(_options); + } + + /// + /// Registers services that are required for the configuration of JsonApiDotNetCore during the start up. + /// + public void RegisterJsonApiStartupServices() + { + _services.AddSingleton(_options); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(sp => + new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); + _services.TryAddScoped(); + _services.TryAddScoped(); + } + + public void ConfigureAutoDiscovery(Action configureAutoDiscovery) + { + var intermediateProvider = _services.BuildServiceProvider(); + _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); + _resourceGraphBuilder = intermediateProvider.GetRequiredService(); + RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); + _serviceProviderToDispose = intermediateProvider; + } - /// - /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. - /// - public void AddResourceGraph(Type dbContextType, Action configureResources) + /// + /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. + /// + public void AddResourceGraph(Type dbContextType, Action configureResources) + { + using (var intermediateProvider = _services.BuildServiceProvider()) { - using var intermediateProvider = _services.BuildServiceProvider(); AutoDiscoverResources(_serviceDiscoveryFacade); AddResourcesFromDbContext(dbContextType, intermediateProvider, _resourceGraphBuilder); UserConfigureResources(configureResources, _resourceGraphBuilder); - _services.AddSingleton(_resourceGraphBuilder.Build()); + _services.AddSingleton(_resourceGraphBuilder.Build()); } - - public void ConfigureAutoDiscovery(Action configureAutoDiscovery) + } + + /// + /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. + /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: + /// , , , + /// and . + /// + public void ConfigureMvc() + { + IJsonApiExceptionFilterProvider exceptionFilterProvider; + IJsonApiTypeMatchFilterProvider typeMatchFilterProvider; + IJsonApiRoutingConvention routingConvention; + + using (var intermediateProvider = _services.BuildServiceProvider()) { - using var intermediateProvider = _services.BuildServiceProvider(); - _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); - _resourceGraphBuilder = intermediateProvider.GetRequiredService(); - RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); + exceptionFilterProvider = intermediateProvider.GetRequiredService(); + typeMatchFilterProvider = intermediateProvider.GetRequiredService(); + routingConvention = intermediateProvider.GetRequiredService(); } - - private void RegisterDiscoverableAssemblies(Action configureAutoDiscovery, IServiceDiscoveryFacade serviceDiscoveryFacade) + + _services.AddSingleton(routingConvention); + + _mvcBuilder.AddMvcOptions(options => { - configureAutoDiscovery?.Invoke(serviceDiscoveryFacade); - } - - /// - /// Registers the remaining internals. - /// - public void ConfigureServices(Type dbContextType) + options.EnableEndpointRouting = true; + options.Filters.Add(exceptionFilterProvider.Get()); + options.Filters.Add(typeMatchFilterProvider.Get()); + options.Filters.Add(new ConvertEmptyActionResultFilter()); + options.InputFormatters.Insert(0, new JsonApiInputFormatter()); + options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); + options.Conventions.Insert(0, routingConvention); + }); + + if (_options.ValidateModelState) { - if (dbContextType != null) - { - var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.TryAddScoped(typeof(IDbContextResolver), contextResolverType); - } - else - { - _services.AddScoped(); - _services.AddSingleton(new DbContextOptionsBuilder().Options); - } + _mvcBuilder.AddDataAnnotations(); + } + } - AddRepositoryLayer(); - AddServiceLayer(); - - _services.AddSingleton(); - _services.AddSingleton(sp => sp.GetRequiredService()); - _services.AddSingleton(); - - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - - AddServerSerialization(); - AddQueryStringParameterServices(); - if (_options.EnableResourceHooks) - { - AddResourceHooks(); - } + /// + /// Discovers DI registrable services in the assemblies marked for discovery. + /// + public void DiscoverInjectables() + { + _serviceDiscoveryFacade.DiscoverInjectables(); + _serviceProviderToDispose.Dispose(); + } - _services.AddScoped(); + /// + /// Registers the remaining internals. + /// + public void ConfigureServices(Type dbContextType) + { + if (dbContextType != null) + { + var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.TryAddScoped(typeof(IDbContextResolver), contextResolverType); } - - /// - /// Discovers DI registrable services in the assemblies marked for discovery. - /// - public void DiscoverInjectables() + else { - _serviceDiscoveryFacade.DiscoverInjectables(); + _services.AddScoped(); + _services.AddSingleton(new DbContextOptionsBuilder().Options); } - private void AddRepositoryLayer() + AddRepositoryLayer(); + AddServiceLayer(); + + _services.AddSingleton(); + _services.AddSingleton(sp => sp.GetRequiredService()); + _services.AddSingleton(); + + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + + AddServerSerialization(); + AddQueryStringParameterServices(); + if (_options.EnableResourceHooks) { - _services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>)); - _services.AddScoped(typeof(IResourceRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); - - _services.AddScoped(typeof(IResourceReadRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); - _services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + AddResourceHooks(); } - private void AddServiceLayer() - { - _services.AddScoped(typeof(ICreateService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(ICreateService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(); + } + + private void RegisterDiscoverableAssemblies(Action configureAutoDiscovery, + IServiceDiscoveryFacade serviceDiscoveryFacade) + { + configureAutoDiscovery?.Invoke(serviceDiscoveryFacade); + } - _services.AddScoped(typeof(IGetAllService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetAllService<,>), typeof(JsonApiResourceService<,>)); + private void AddRepositoryLayer() + { + _services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>)); + _services.AddScoped(typeof(IResourceRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); - _services.AddScoped(typeof(IGetByIdService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetByIdService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IResourceReadRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + _services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + } - _services.AddScoped(typeof(IGetRelationshipService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetRelationshipService<,>), typeof(JsonApiResourceService<,>)); + private void AddServiceLayer() + { + _services.AddScoped(typeof(ICreateService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(ICreateService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IGetSecondaryService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetSecondaryService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetAllService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetAllService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IUpdateService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IUpdateService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetByIdService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetByIdService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IDeleteService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IDeleteService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetRelationshipService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetRelationshipService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IResourceService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IResourceService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetSecondaryService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetSecondaryService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>)); - } + _services.AddScoped(typeof(IUpdateService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IUpdateService<,>), typeof(JsonApiResourceService<,>)); - private void AddQueryStringParameterServices() - { - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - - _services.AddScoped(); - _services.AddScoped(); - _services.AddSingleton(); - } + _services.AddScoped(typeof(IDeleteService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IDeleteService<,>), typeof(JsonApiResourceService<,>)); - private void AddResourceHooks() - { - _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); - _services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>)); - _services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor)); - _services.AddTransient(); - _services.AddTransient(); - } + _services.AddScoped(typeof(IResourceService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IResourceService<,>), typeof(JsonApiResourceService<,>)); - private void AddServerSerialization() - { - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); - _services.AddScoped(typeof(ResponseSerializer<>)); - _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); - _services.AddScoped(); - } + _services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>)); + } - /// - /// Registers services that are required for the configuration of JsonApiDotNetCore during the start up. - /// - public void RegisterJsonApiStartupServices() - { - _services.AddSingleton(_options); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(sp => new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); - _services.TryAddScoped(); - _services.TryAddScoped(); - } - - private void AddResourcesFromDbContext(Type dbContextType, ServiceProvider intermediateProvider, IResourceGraphBuilder builder) + private void AddQueryStringParameterServices() + { + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped( + sp => sp.GetService()); + + _services.AddScoped(); + _services.AddScoped(); + _services.AddSingleton(); + } + + private void AddResourceHooks() + { + _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); + _services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>)); + _services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor)); + _services.AddTransient(); + _services.AddTransient(); + } + + private void AddServerSerialization() + { + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); + _services.AddScoped(typeof(ResponseSerializer<>)); + _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); + _services.AddScoped(); + } + + private void AddResourcesFromDbContext(Type dbContextType, ServiceProvider intermediateProvider, + IResourceGraphBuilder builder) + { + if (dbContextType != null) { - if (dbContextType != null) - { - var dbContext = (DbContext) intermediateProvider.GetRequiredService(dbContextType); + var dbContext = (DbContext) intermediateProvider.GetRequiredService(dbContextType); - foreach (var entityType in dbContext.Model.GetEntityTypes()) - { - builder.AddResource(entityType.ClrType); - } + foreach (var entityType in dbContext.Model.GetEntityTypes()) + { + builder.AddResource(entityType.ClrType); } } + } - /// - /// Performs auto-discovery of JsonApiDotNetCore services. - /// - private void AutoDiscoverResources(IServiceDiscoveryFacade serviceDiscoveryFacade) - { - serviceDiscoveryFacade.DiscoverResources(); - } + /// + /// Performs auto-discovery of JsonApiDotNetCore services. + /// + private void AutoDiscoverResources(IServiceDiscoveryFacade serviceDiscoveryFacade) + { + serviceDiscoveryFacade.DiscoverResources(); + } - /// - /// Executes the action provided by the user to configure the resources using - /// - private void UserConfigureResources(Action configureResources, IResourceGraphBuilder resourceGraphBuilder) - { - configureResources?.Invoke(resourceGraphBuilder); - } + /// + /// Executes the action provided by the user to configure the resources using + /// + private void UserConfigureResources(Action configureResources, + IResourceGraphBuilder resourceGraphBuilder) + { + configureResources?.Invoke(resourceGraphBuilder); + } } } diff --git a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs index 76f336bbd0..1d0d74ad3c 100644 --- a/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/JsonApiRoutingConvention.cs @@ -81,7 +81,7 @@ public void Apply(ApplicationModel application) } /// - /// Verifies if routing convention should be enabled for this controller + /// Verifies if routing convention should be enabled for this controller. /// private bool RoutingConventionDisabled(ControllerModel controller) { From 6496a7f8673d22cc3c99d3e7cb408e36be296366 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 27 Aug 2020 18:31:03 +0200 Subject: [PATCH 18/51] chore: trigger build --- src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 7848e7612e..ad61bfae6b 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -49,7 +49,7 @@ public JsonApiApplicationBuilder(IServiceCollection services, } /// - /// Executes the action provided by the user to configure + /// Executes the action provided by the user to configure . /// public void ConfigureJsonApiOptions(Action configureOptions) { From aa3c122132fce11397dcf29d17aef68bac90a6dd Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 28 Aug 2020 10:22:31 +0200 Subject: [PATCH 19/51] fix: reduce amount of intermediate service providers --- .../Builders/JsonApiApplicationBuilder.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index ad61bfae6b..4cac666044 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -39,7 +39,7 @@ internal sealed class JsonApiApplicationBuilder private IServiceDiscoveryFacade _serviceDiscoveryFacade; private IResourceGraphBuilder _resourceGraphBuilder; private readonly IMvcCoreBuilder _mvcBuilder; - private ServiceProvider _serviceProviderToDispose; + private ServiceProvider _intermediateServiceProvider; public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) @@ -76,7 +76,7 @@ public void ConfigureAutoDiscovery(Action configureAuto _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); _resourceGraphBuilder = intermediateProvider.GetRequiredService(); RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); - _serviceProviderToDispose = intermediateProvider; + _intermediateServiceProvider = intermediateProvider; } /// @@ -84,13 +84,10 @@ public void ConfigureAutoDiscovery(Action configureAuto /// public void AddResourceGraph(Type dbContextType, Action configureResources) { - using (var intermediateProvider = _services.BuildServiceProvider()) - { - AutoDiscoverResources(_serviceDiscoveryFacade); - AddResourcesFromDbContext(dbContextType, intermediateProvider, _resourceGraphBuilder); - UserConfigureResources(configureResources, _resourceGraphBuilder); - _services.AddSingleton(_resourceGraphBuilder.Build()); - } + AutoDiscoverResources(_serviceDiscoveryFacade); + AddResourcesFromDbContext(dbContextType, _intermediateServiceProvider, _resourceGraphBuilder); + UserConfigureResources(configureResources, _resourceGraphBuilder); + _services.AddSingleton(_resourceGraphBuilder.Build()); } /// @@ -137,7 +134,7 @@ public void ConfigureMvc() public void DiscoverInjectables() { _serviceDiscoveryFacade.DiscoverInjectables(); - _serviceProviderToDispose.Dispose(); + _intermediateServiceProvider.Dispose(); } /// From 681d2cc7664d78d1aef882940d8c0bfedb8dd9ff Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 28 Aug 2020 13:07:55 +0200 Subject: [PATCH 20/51] feat: configure MvcOptions in phase two, removal of intermediate service provider --- .../Builders/IJsonApiApplicationBuilder.cs | 10 + .../Builders/JsonApiApplicationBuilder.cs | 491 +++++++++--------- .../ApplicationBuilderExtensions.cs | 15 +- .../ConvertEmptyActionResultFilter.cs | 29 +- .../Middleware/IJsonApiExceptionFilter.cs | 9 + .../IJsonApiExceptionFilterProvider.cs | 20 - .../Middleware/IJsonApiTypeMatchFilter.cs | 10 + .../IJsonApiTypeMatchFilterProvider.cs | 20 - .../Middleware/IQueryStringActionFilter.cs | 16 +- .../Middleware/JsonApiExceptionFilter.cs | 21 +- .../Middleware/JsonApiTypeMatchFilter.cs | 48 ++ .../Middleware/QueryStringActionFilter.cs | 10 +- 12 files changed, 378 insertions(+), 321 deletions(-) create mode 100644 src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs create mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs create mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs create mode 100644 src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs diff --git a/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs new file mode 100644 index 0000000000..6c38ff376f --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace JsonApiDotNetCore.Builders +{ + internal interface IJsonApiApplicationBuilder + { + public Action ConfigureMvcOptions { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 4cac666044..ba95f0a579 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -25,6 +25,7 @@ using JsonApiDotNetCore.RequestServices; using JsonApiDotNetCore.RequestServices.Contracts; using JsonApiDotNetCore.Services.Contract; +using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCore.Builders { @@ -32,289 +33,287 @@ namespace JsonApiDotNetCore.Builders /// A utility class that builds a JsonApi application. It registers all required services /// and allows the user to override parts of the startup configuration. /// - internal sealed class JsonApiApplicationBuilder + internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder { - private readonly JsonApiOptions _options = new JsonApiOptions(); - private readonly IServiceCollection _services; - private IServiceDiscoveryFacade _serviceDiscoveryFacade; - private IResourceGraphBuilder _resourceGraphBuilder; - private readonly IMvcCoreBuilder _mvcBuilder; - private ServiceProvider _intermediateServiceProvider; - - public JsonApiApplicationBuilder(IServiceCollection services, - IMvcCoreBuilder mvcBuilder) - { - _services = services; - _mvcBuilder = mvcBuilder; - } - - /// - /// Executes the action provided by the user to configure . - /// - public void ConfigureJsonApiOptions(Action configureOptions) - { - configureOptions?.Invoke(_options); - } - - /// - /// Registers services that are required for the configuration of JsonApiDotNetCore during the start up. - /// - public void RegisterJsonApiStartupServices() - { - _services.AddSingleton(_options); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(sp => - new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); - _services.TryAddScoped(); - _services.TryAddScoped(); - } - - public void ConfigureAutoDiscovery(Action configureAutoDiscovery) - { - var intermediateProvider = _services.BuildServiceProvider(); - _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); - _resourceGraphBuilder = intermediateProvider.GetRequiredService(); - RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); - _intermediateServiceProvider = intermediateProvider; - } - - /// - /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. - /// - public void AddResourceGraph(Type dbContextType, Action configureResources) - { - AutoDiscoverResources(_serviceDiscoveryFacade); - AddResourcesFromDbContext(dbContextType, _intermediateServiceProvider, _resourceGraphBuilder); - UserConfigureResources(configureResources, _resourceGraphBuilder); - _services.AddSingleton(_resourceGraphBuilder.Build()); - } - - /// - /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. - /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: - /// , , , - /// and . - /// - public void ConfigureMvc() - { - IJsonApiExceptionFilterProvider exceptionFilterProvider; - IJsonApiTypeMatchFilterProvider typeMatchFilterProvider; - IJsonApiRoutingConvention routingConvention; - - using (var intermediateProvider = _services.BuildServiceProvider()) + private readonly JsonApiOptions _options = new JsonApiOptions(); + private readonly IServiceCollection _services; + private IServiceDiscoveryFacade _serviceDiscoveryFacade; + private IResourceGraphBuilder _resourceGraphBuilder; + private readonly IMvcCoreBuilder _mvcBuilder; + private ServiceProvider _intermediateServiceProvider; + public Action ConfigureMvcOptions { get; set; } + + public JsonApiApplicationBuilder(IServiceCollection services, + IMvcCoreBuilder mvcBuilder) { - exceptionFilterProvider = intermediateProvider.GetRequiredService(); - typeMatchFilterProvider = intermediateProvider.GetRequiredService(); - routingConvention = intermediateProvider.GetRequiredService(); + _services = services; + _mvcBuilder = mvcBuilder; } - - _services.AddSingleton(routingConvention); - - _mvcBuilder.AddMvcOptions(options => + + /// + /// Executes the action provided by the user to configure . + /// + public void ConfigureJsonApiOptions(Action configureOptions) { - options.EnableEndpointRouting = true; - options.Filters.Add(exceptionFilterProvider.Get()); - options.Filters.Add(typeMatchFilterProvider.Get()); - options.Filters.Add(new ConvertEmptyActionResultFilter()); - options.InputFormatters.Insert(0, new JsonApiInputFormatter()); - options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); - options.Conventions.Insert(0, routingConvention); - }); - - if (_options.ValidateModelState) + configureOptions?.Invoke(_options); + } + + /// + /// Registers services that are required for the configuration of JsonApiDotNetCore during the start up. + /// + public void RegisterJsonApiStartupServices() { - _mvcBuilder.AddDataAnnotations(); + _services.AddSingleton(_options); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(sp => + new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); } - } - /// - /// Discovers DI registrable services in the assemblies marked for discovery. - /// - public void DiscoverInjectables() - { - _serviceDiscoveryFacade.DiscoverInjectables(); - _intermediateServiceProvider.Dispose(); - } + public void ConfigureAutoDiscovery(Action configureAutoDiscovery) + { + var intermediateProvider = _services.BuildServiceProvider(); + _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); + _resourceGraphBuilder = intermediateProvider.GetRequiredService(); + RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); + _intermediateServiceProvider = intermediateProvider; + } - /// - /// Registers the remaining internals. - /// - public void ConfigureServices(Type dbContextType) - { - if (dbContextType != null) + /// + /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. + /// + public void AddResourceGraph(Type dbContextType, Action configureResources) { - var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); - _services.TryAddScoped(typeof(IDbContextResolver), contextResolverType); + AutoDiscoverResources(_serviceDiscoveryFacade); + AddResourcesFromDbContext(dbContextType, _intermediateServiceProvider, _resourceGraphBuilder); + UserConfigureResources(configureResources, _resourceGraphBuilder); + _services.AddSingleton(_resourceGraphBuilder.Build()); } - else + + /// + /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. + /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: + /// , , , + /// and . + /// + public void ConfigureMvc() { - _services.AddScoped(); - _services.AddSingleton(new DbContextOptionsBuilder().Options); + _mvcBuilder.AddMvcOptions(options => + { + options.EnableEndpointRouting = true; + options.Filters.AddService(); + options.Filters.AddService(); + options.Filters.AddService(); + options.Filters.Add(new ConvertEmptyActionResultFilter()); + options.InputFormatters.Insert(0, new JsonApiInputFormatter()); + options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); + ConfigureMvcOptions?.Invoke(options); + }); + + if (_options.ValidateModelState) + { + _mvcBuilder.AddDataAnnotations(); + } } - AddRepositoryLayer(); - AddServiceLayer(); - - _services.AddSingleton(); - _services.AddSingleton(sp => sp.GetRequiredService()); - _services.AddSingleton(); - - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - - AddServerSerialization(); - AddQueryStringParameterServices(); - if (_options.EnableResourceHooks) + /// + /// Discovers DI registrable services in the assemblies marked for discovery. + /// + public void DiscoverInjectables() { - AddResourceHooks(); + _serviceDiscoveryFacade.DiscoverInjectables(); + _intermediateServiceProvider.Dispose(); } - _services.AddScoped(); - } - - private void RegisterDiscoverableAssemblies(Action configureAutoDiscovery, - IServiceDiscoveryFacade serviceDiscoveryFacade) - { - configureAutoDiscovery?.Invoke(serviceDiscoveryFacade); - } + /// + /// Registers the remaining internals. + /// + public void ConfigureServices(Type dbContextType) + { + if (dbContextType != null) + { + var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + _services.TryAddScoped(typeof(IDbContextResolver), contextResolverType); + } + else + { + _services.AddScoped(); + _services.AddSingleton(new DbContextOptionsBuilder().Options); + } - private void AddRepositoryLayer() - { - _services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>)); - _services.AddScoped(typeof(IResourceRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + AddRepositoryLayer(); + AddServiceLayer(); + + _services.AddSingleton(this); + _services.AddSingleton(sp => sp.GetService()); + + _services.TryAddSingleton(); + _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddScoped(); + + _services.AddSingleton(); + _services.AddSingleton(sp => sp.GetRequiredService()); + + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + + AddServerSerialization(); + AddQueryStringParameterServices(); + if (_options.EnableResourceHooks) + { + AddResourceHooks(); + } - _services.AddScoped(typeof(IResourceReadRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); - _services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); - } + _services.AddScoped(); + } - private void AddServiceLayer() - { - _services.AddScoped(typeof(ICreateService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(ICreateService<,>), typeof(JsonApiResourceService<,>)); + private void RegisterDiscoverableAssemblies(Action configureAutoDiscovery, + IServiceDiscoveryFacade serviceDiscoveryFacade) + { + configureAutoDiscovery?.Invoke(serviceDiscoveryFacade); + } - _services.AddScoped(typeof(IGetAllService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetAllService<,>), typeof(JsonApiResourceService<,>)); + private void AddRepositoryLayer() + { + _services.AddScoped(typeof(IResourceRepository<>), typeof(EntityFrameworkCoreRepository<>)); + _services.AddScoped(typeof(IResourceRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); - _services.AddScoped(typeof(IGetByIdService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetByIdService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IResourceReadRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + _services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(EntityFrameworkCoreRepository<,>)); + } - _services.AddScoped(typeof(IGetRelationshipService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetRelationshipService<,>), typeof(JsonApiResourceService<,>)); + private void AddServiceLayer() + { + _services.AddScoped(typeof(ICreateService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(ICreateService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IGetSecondaryService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IGetSecondaryService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetAllService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetAllService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IUpdateService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IUpdateService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetByIdService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetByIdService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IDeleteService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IDeleteService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetRelationshipService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetRelationshipService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IResourceService<>), typeof(JsonApiResourceService<>)); - _services.AddScoped(typeof(IResourceService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IGetSecondaryService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IGetSecondaryService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>)); - _services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>)); - } + _services.AddScoped(typeof(IUpdateService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IUpdateService<,>), typeof(JsonApiResourceService<,>)); - private void AddQueryStringParameterServices() - { - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); - - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped( - sp => sp.GetService()); - - _services.AddScoped(); - _services.AddScoped(); - _services.AddSingleton(); - } + _services.AddScoped(typeof(IDeleteService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IDeleteService<,>), typeof(JsonApiResourceService<,>)); - private void AddResourceHooks() - { - _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); - _services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>)); - _services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor)); - _services.AddTransient(); - _services.AddTransient(); - } + _services.AddScoped(typeof(IResourceService<>), typeof(JsonApiResourceService<>)); + _services.AddScoped(typeof(IResourceService<,>), typeof(JsonApiResourceService<,>)); - private void AddServerSerialization() - { - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); - _services.AddScoped(typeof(ResponseSerializer<>)); - _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); - _services.AddScoped(); - } + _services.AddScoped(typeof(IResourceQueryService<,>), typeof(JsonApiResourceService<,>)); + _services.AddScoped(typeof(IResourceCommandService<,>), typeof(JsonApiResourceService<,>)); + } - private void AddResourcesFromDbContext(Type dbContextType, ServiceProvider intermediateProvider, - IResourceGraphBuilder builder) - { - if (dbContextType != null) + private void AddQueryStringParameterServices() { - var dbContext = (DbContext) intermediateProvider.GetRequiredService(dbContextType); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services + .AddScoped(); + + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => + sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped( + sp => sp.GetService()); + + _services.AddScoped(); + _services.AddSingleton(); + } + + private void AddResourceHooks() + { + _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); + _services.AddScoped(typeof(IResourceHookContainer<>), typeof(ResourceDefinition<>)); + _services.AddTransient(typeof(IResourceHookExecutor), typeof(ResourceHookExecutor)); + _services.AddTransient(); + _services.AddTransient(); + } + + private void AddServerSerialization() + { + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(typeof(IMetaBuilder<>), typeof(MetaBuilder<>)); + _services.AddScoped(typeof(ResponseSerializer<>)); + _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); + _services.AddScoped(); + } - foreach (var entityType in dbContext.Model.GetEntityTypes()) + private void AddResourcesFromDbContext(Type dbContextType, ServiceProvider intermediateProvider, + IResourceGraphBuilder builder) + { + if (dbContextType != null) { - builder.AddResource(entityType.ClrType); + var dbContext = (DbContext) intermediateProvider.GetRequiredService(dbContextType); + + foreach (var entityType in dbContext.Model.GetEntityTypes()) + { + builder.AddResource(entityType.ClrType); + } } } - } - /// - /// Performs auto-discovery of JsonApiDotNetCore services. - /// - private void AutoDiscoverResources(IServiceDiscoveryFacade serviceDiscoveryFacade) - { - serviceDiscoveryFacade.DiscoverResources(); - } + /// + /// Performs auto-discovery of JsonApiDotNetCore services. + /// + private void AutoDiscoverResources(IServiceDiscoveryFacade serviceDiscoveryFacade) + { + serviceDiscoveryFacade.DiscoverResources(); + } + + /// + /// Executes the action provided by the user to configure the resources using + /// + private void UserConfigureResources(Action configureResources, + IResourceGraphBuilder resourceGraphBuilder) + { + configureResources?.Invoke(resourceGraphBuilder); + } - /// - /// Executes the action provided by the user to configure the resources using - /// - private void UserConfigureResources(Action configureResources, - IResourceGraphBuilder resourceGraphBuilder) - { - configureResources?.Invoke(resourceGraphBuilder); - } } } diff --git a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs index 8d943101e0..035c88b03c 100644 --- a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -1,5 +1,10 @@ +using System; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCore { @@ -9,6 +14,7 @@ public static class ApplicationBuilderExtensions /// Registers the JsonApiDotNetCore middleware. /// /// The to add the middleware to. + /// Configure .NET Core MVC options /// /// The code below is the minimal that is required for proper activation, /// which should be added to your Startup.Configure method. @@ -18,8 +24,15 @@ public static class ApplicationBuilderExtensions /// app.UseEndpoints(endpoints => endpoints.MapControllers()); /// ]]> /// - public static void UseJsonApi(this IApplicationBuilder builder) + public static void UseJsonApi(this IApplicationBuilder builder, Action configureMvcOptions = null) { + var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService(); + jsonApiApplicationBuilder.ConfigureMvcOptions = options => + { + options.Conventions.Insert(0, builder.ApplicationServices.GetRequiredService()); + configureMvcOptions?.Invoke(options); + }; + builder.UseMiddleware(); } } diff --git a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs index 328ed90491..204711f951 100644 --- a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs @@ -4,12 +4,16 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; namespace JsonApiDotNetCore.Middleware -{ +{ + /// + /// Converts action result without parameters into action result with null parameter. + /// For example: return NotFound() -> return NotFound(null) + /// This ensures our formatter is invoked, where we'll build a json:api compliant response. + /// For details, see: https://github.com/dotnet/aspnetcore/issues/16969 + /// public sealed class ConvertEmptyActionResultFilter : IAlwaysRunResultFilter { - public void OnResultExecuted(ResultExecutedContext context) - { - } + public void OnResultExecuted(ResultExecutedContext context) { /* noop */ } public void OnResultExecuting(ResultExecutingContext context) { @@ -18,18 +22,13 @@ public void OnResultExecuting(ResultExecutingContext context) return; } - if (context.Result is ObjectResult objectResult && objectResult.Value != null) - { - return; - } - - // Convert action result without parameters into action result with null parameter. - // For example: return NotFound() -> return NotFound(null) - // This ensures our formatter is invoked, where we'll build a json:api compliant response. - // For details, see: https://github.com/dotnet/aspnetcore/issues/16969 - if (context.Result is IStatusCodeActionResult statusCodeResult) + switch (context.Result) { - context.Result = new ObjectResult(null) {StatusCode = statusCodeResult.StatusCode}; + case ObjectResult objectResult when objectResult.Value != null: + return; + case IStatusCodeActionResult statusCodeResult: + context.Result = new ObjectResult(null) {StatusCode = statusCodeResult.StatusCode}; + break; } } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs new file mode 100644 index 0000000000..56143d4dee --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Global exception filter that wraps any thrown error with a JsonApiException. + /// + public interface IJsonApiExceptionFilter : IExceptionFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs deleted file mode 100644 index 53077d8315..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilterProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Provides the type of the global exception filter that is configured in MVC during startup. - /// This can be overridden to let JADNC use your own exception filter. The default exception filter used - /// is - /// - public interface IJsonApiExceptionFilterProvider - { - Type Get(); - } - - /// - public class JsonApiExceptionFilterProvider : IJsonApiExceptionFilterProvider - { - public Type Get() => typeof(JsonApiExceptionFilter); - } -} diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs new file mode 100644 index 0000000000..4d6ee496c5 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs @@ -0,0 +1,10 @@ +using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Action filter used to verify the incoming type matches the target type, else return a 409 + /// + public interface IJsonApiTypeMatchFilter : IActionFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs deleted file mode 100644 index c3232c636b..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilterProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Provides the type of the global action filter that is configured in MVC during startup. - /// This can be overridden to let JADNC use your own action filter. The default action filter used - /// is - /// - public interface IJsonApiTypeMatchFilterProvider - { - Type Get(); - } - - /// - public class JsonApiTypeMatchFilterProvider : IJsonApiTypeMatchFilterProvider - { - public Type Get() => typeof(IncomingTypeMatchFilter); - } -} diff --git a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs index f534bfddac..d29a76625f 100644 --- a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs @@ -1,10 +1,14 @@ -using System.Threading.Tasks; +using System.Reflection; +using System.Threading.Tasks; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.QueryStrings; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { - public interface IQueryStringActionFilter - { - Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next); - } -} \ No newline at end of file + /// + /// Entry point for processing all query string parameters. + /// + public interface IQueryStringActionFilter : IAsyncActionFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs index 75474a3f41..81d134b5e1 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs @@ -4,10 +4,7 @@ namespace JsonApiDotNetCore.Middleware { - /// - /// Global exception filter that wraps any thrown error with a JsonApiException. - /// - public class JsonApiExceptionFilter : ActionFilterAttribute, IExceptionFilter + public class JsonApiExceptionFilter : ActionFilterAttribute, IJsonApiExceptionFilter { private readonly IExceptionHandler _exceptionHandler; @@ -18,15 +15,17 @@ public JsonApiExceptionFilter(IExceptionHandler exceptionHandler) public void OnException(ExceptionContext context) { - if (context.HttpContext.IsJsonApiRequest()) + if (!context.HttpContext.IsJsonApiRequest()) { - var errorDocument = _exceptionHandler.HandleException(context.Exception); - - context.Result = new ObjectResult(errorDocument) - { - StatusCode = (int) errorDocument.GetErrorStatusCode() - }; + return; } + + var errorDocument = _exceptionHandler.HandleException(context.Exception); + + context.Result = new ObjectResult(errorDocument) + { + StatusCode = (int) errorDocument.GetErrorStatusCode() + }; } } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs new file mode 100644 index 0000000000..fea920a302 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Net.Http; +using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Internal.Contracts; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + public sealed class JsonApiTypeMatchFilter : IJsonApiTypeMatchFilter + { + private readonly IResourceContextProvider _provider; + + public JsonApiTypeMatchFilter(IResourceContextProvider provider) + { + _provider = provider; + } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (!context.HttpContext.IsJsonApiRequest()) + { + return; + } + + var request = context.HttpContext.Request; + if (request.Method != "PATCH" && request.Method != "POST") + { + return; + } + + var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); + var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; + + if (deserializedType == null || targetType == null || deserializedType == targetType) + { + return; + } + + var resourceFromEndpoint = _provider.GetResourceContext(targetType); + var resourceFromBody = _provider.GetResourceContext(deserializedType); + + throw new ResourceTypeMismatchException(new HttpMethod(request.Method), request.Path, resourceFromEndpoint, resourceFromBody); + } + + public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs index 2122e8d220..aa20e66950 100644 --- a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs @@ -1,12 +1,13 @@ using System.Reflection; using System.Threading.Tasks; using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.QueryStrings; using Microsoft.AspNetCore.Mvc.Filters; namespace JsonApiDotNetCore.Middleware { - public sealed class QueryStringActionFilter : IAsyncActionFilter, IQueryStringActionFilter + public sealed class QueryStringActionFilter : IQueryStringActionFilter { private readonly IQueryStringReader _queryStringReader; @@ -17,7 +18,12 @@ public QueryStringActionFilter(IQueryStringReader queryStringReader) public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - DisableQueryAttribute disableQueryAttribute = context.Controller.GetType().GetCustomAttribute(); + if (!context.HttpContext.IsJsonApiRequest()) + { + return; + } + + var disableQueryAttribute = context.Controller.GetType().GetCustomAttribute(); _queryStringReader.ReadAll(disableQueryAttribute); await next(); From de23e9f970675da7a67acd9ea2a7e6ba6a3ef015 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 28 Aug 2020 13:22:43 +0200 Subject: [PATCH 21/51] fix: tests --- src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs | 1 - src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs | 2 ++ .../Acceptance/NonJsonApiControllerTests.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index aaf2cb6795..09afc4c7f0 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -5,7 +5,6 @@ namespace JsonApiDotNetCore.Controllers { - [ServiceFilter(typeof(IQueryStringActionFilter))] public abstract class CoreJsonApiController : ControllerBase { protected IActionResult Error(Error error) diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs index def858fd3a..dfd6840b92 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs @@ -11,7 +11,9 @@ public sealed class JsonApiOutputFormatter : IOutputFormatter public bool CanWriteResult(OutputFormatterCanWriteContext context) { if (context == null) + { throw new ArgumentNullException(nameof(context)); + } return context.HttpContext.IsJsonApiRequest(); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs index 11553edb90..7a0a27163e 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs @@ -92,7 +92,7 @@ public async Task NonJsonApiController_Skips_Middleware_And_Formatters_On_Delete // Act var response = await client.SendAsync(request); - + var test = await response.Content.ReadAsStringAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString()); From 0b788b5ae23dee7d1b7f91a420ca3175f25adfff Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 28 Aug 2020 17:40:00 +0200 Subject: [PATCH 22/51] feat: remove intermediate service providers, consistent middleware component registration --- benchmarks/DependencyFactory.cs | 2 +- .../Startups/EmptyStartup.cs | 13 +-- .../Startups/Startup.cs | 2 +- .../Startups/TestStartup.cs | 5 +- .../Builders/IResourceGraphBuilder.cs | 44 ------- .../Builders/JsonApiApplicationBuilder.cs | 107 ++++++++---------- .../Builders/ResourceGraphBuilder.cs | 12 +- .../ApplicationBuilderExtensions.cs | 4 + .../Extensions/ServiceCollectionExtensions.cs | 19 ++-- .../Formatters/IJsonApiInputFormatter.cs | 6 + .../Formatters/IJsonApiOutputFormatter.cs | 6 + .../Formatters/JsonApiInputFormatter.cs | 2 +- .../Formatters/JsonApiOutputFormatter.cs | 2 +- .../Graph/IServiceDiscoveryFacade.cs | 26 ----- .../Graph/ServiceDiscoveryFacade.cs | 18 +-- .../ConvertEmptyActionResultFilter.cs | 11 +- .../IConvertEmptyActionResultFilter.cs | 12 ++ .../Middleware/IQueryStringActionFilter.cs | 2 +- .../Middleware/IncomingTypeMatchFilter.cs | 48 -------- .../Middleware/QueryStringActionFilter.cs | 9 +- .../ServiceDiscoveryFacadeTests.cs | 10 +- .../Acceptance/KebabCaseFormatterTests.cs | 4 +- .../LinksWithoutNamespaceTests.cs | 4 +- .../IntegrationTests/TestableStartup.cs | 4 +- 24 files changed, 128 insertions(+), 244 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs create mode 100644 src/JsonApiDotNetCore/Formatters/IJsonApiInputFormatter.cs create mode 100644 src/JsonApiDotNetCore/Formatters/IJsonApiOutputFormatter.cs delete mode 100644 src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs create mode 100644 src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 0492a9a185..df6d0ddba0 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -13,7 +13,7 @@ internal static class DependencyFactory { public static IResourceGraph CreateResourceGraph(IJsonApiOptions options) { - IResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + ResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); builder.AddResource(BenchmarkResourcePublicNames.Type); return builder.Build(); } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 297f1e8bfc..eedb175207 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample { @@ -11,16 +12,10 @@ namespace JsonApiDotNetCoreExample /// public abstract class EmptyStartup { - protected EmptyStartup(IConfiguration configuration) - { - } + protected EmptyStartup(IConfiguration configuration) { } - public virtual void ConfigureServices(IServiceCollection services) - { - } + public virtual void ConfigureServices(IServiceCollection services) { } - public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) - { - } + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 3426b94a67..767a22b9f0 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -10,6 +10,7 @@ using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCoreExample.Services; using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -28,7 +29,6 @@ public Startup(IConfiguration configuration) : base(configuration) public override void ConfigureServices(IServiceCollection services) { ConfigureClock(services); - services.AddScoped(); services.AddScoped(sp => sp.GetService()); diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs index e1af39084c..630734d693 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs @@ -2,14 +2,13 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample { public class TestStartup : Startup { - public TestStartup(IConfiguration configuration) : base(configuration) - { - } + public TestStartup(IConfiguration configuration) : base(configuration) { } protected override void ConfigureClock(IServiceCollection services) { diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs deleted file mode 100644 index 29eaf6a7ce..0000000000 --- a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Builders -{ - public interface IResourceGraphBuilder - { - /// - /// Construct the - /// - IResourceGraph Build(); - /// - /// Add a json:api resource - /// - /// The resource model type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// - IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable; - /// - /// Add a json:api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// - IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable; - /// - /// Add a Json:Api resource - /// - /// The resource model type - /// The resource model identifier type - /// - /// The pluralized name that should be exposed by the API. - /// If nothing is specified, the configured name formatter will be used. - /// - IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string publicResourceName = null); - } -} diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index ba95f0a579..e81a9282ba 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -26,6 +26,7 @@ using JsonApiDotNetCore.RequestServices.Contracts; using JsonApiDotNetCore.Services.Contract; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Builders { @@ -37,17 +38,20 @@ internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder { private readonly JsonApiOptions _options = new JsonApiOptions(); private readonly IServiceCollection _services; - private IServiceDiscoveryFacade _serviceDiscoveryFacade; - private IResourceGraphBuilder _resourceGraphBuilder; private readonly IMvcCoreBuilder _mvcBuilder; - private ServiceProvider _intermediateServiceProvider; + private readonly ResourceGraphBuilder _resourceGraphBuilder; + private readonly ServiceDiscoveryFacade _serviceDiscoveryFacade; + private readonly ServiceProvider _intermediateProvider; public Action ConfigureMvcOptions { get; set; } - public JsonApiApplicationBuilder(IServiceCollection services, - IMvcCoreBuilder mvcBuilder) + public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { _services = services; _mvcBuilder = mvcBuilder; + _intermediateProvider = services.BuildServiceProvider(); + var loggerFactory = _intermediateProvider.GetService(); + _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); + _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory); } /// @@ -57,35 +61,22 @@ public void ConfigureJsonApiOptions(Action configureOptions) { configureOptions?.Invoke(_options); } - - /// - /// Registers services that are required for the configuration of JsonApiDotNetCore during the start up. - /// - public void RegisterJsonApiStartupServices() - { - _services.AddSingleton(_options); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(sp => - new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); - } - - public void ConfigureAutoDiscovery(Action configureAutoDiscovery) + + public void ConfigureAutoDiscovery(Action configureAutoDiscovery) { - var intermediateProvider = _services.BuildServiceProvider(); - _serviceDiscoveryFacade = intermediateProvider.GetRequiredService(); - _resourceGraphBuilder = intermediateProvider.GetRequiredService(); - RegisterDiscoverableAssemblies(configureAutoDiscovery, _serviceDiscoveryFacade); - _intermediateServiceProvider = intermediateProvider; + configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade); } /// /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. /// - public void AddResourceGraph(Type dbContextType, Action configureResources) + public void AddResourceGraph(Type dbContextType, Action configureResources) { AutoDiscoverResources(_serviceDiscoveryFacade); - AddResourcesFromDbContext(dbContextType, _intermediateServiceProvider, _resourceGraphBuilder); + if (dbContextType != null) + { + AddResourcesFromDbContext((DbContext)_intermediateProvider.GetService(dbContextType), _resourceGraphBuilder); + } UserConfigureResources(configureResources, _resourceGraphBuilder); _services.AddSingleton(_resourceGraphBuilder.Build()); } @@ -93,7 +84,7 @@ public void AddResourceGraph(Type dbContextType, Action c /// /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: - /// , , , + /// , , , /// and . /// public void ConfigureMvc() @@ -104,9 +95,7 @@ public void ConfigureMvc() options.Filters.AddService(); options.Filters.AddService(); options.Filters.AddService(); - options.Filters.Add(new ConvertEmptyActionResultFilter()); - options.InputFormatters.Insert(0, new JsonApiInputFormatter()); - options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); + options.Filters.AddService(); ConfigureMvcOptions?.Invoke(options); }); @@ -122,7 +111,7 @@ public void ConfigureMvc() public void DiscoverInjectables() { _serviceDiscoveryFacade.DiscoverInjectables(); - _intermediateServiceProvider.Dispose(); + _intermediateProvider.Dispose(); } /// @@ -143,27 +132,14 @@ public void ConfigureServices(Type dbContextType) AddRepositoryLayer(); AddServiceLayer(); + AddMiddlewareLayer(); - _services.AddSingleton(this); - _services.AddSingleton(sp => sp.GetService()); - - _services.TryAddSingleton(); - _services.TryAddScoped(); - _services.TryAddScoped(); - _services.TryAddScoped(); - - _services.AddSingleton(); _services.AddSingleton(sp => sp.GetRequiredService()); - _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(); _services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>)); - _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(); _services.AddScoped(); @@ -179,10 +155,25 @@ public void ConfigureServices(Type dbContextType) _services.AddScoped(); } - private void RegisterDiscoverableAssemblies(Action configureAutoDiscovery, - IServiceDiscoveryFacade serviceDiscoveryFacade) + private void AddMiddlewareLayer() { - configureAutoDiscovery?.Invoke(serviceDiscoveryFacade); + _services.AddSingleton(_options); + _services.AddSingleton(this); + _services.TryAddSingleton(); + _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.AddSingleton(sp => sp.GetService()); + _services.AddSingleton(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); } private void AddRepositoryLayer() @@ -284,33 +275,27 @@ private void AddServerSerialization() _services.AddScoped(); } - private void AddResourcesFromDbContext(Type dbContextType, ServiceProvider intermediateProvider, - IResourceGraphBuilder builder) + private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder builder) { - if (dbContextType != null) + foreach (var entityType in dbContext.Model.GetEntityTypes()) { - var dbContext = (DbContext) intermediateProvider.GetRequiredService(dbContextType); - - foreach (var entityType in dbContext.Model.GetEntityTypes()) - { - builder.AddResource(entityType.ClrType); - } + builder.AddResource(entityType.ClrType); } } /// /// Performs auto-discovery of JsonApiDotNetCore services. /// - private void AutoDiscoverResources(IServiceDiscoveryFacade serviceDiscoveryFacade) + private void AutoDiscoverResources(ServiceDiscoveryFacade serviceDiscoveryFacade) { serviceDiscoveryFacade.DiscoverResources(); } /// - /// Executes the action provided by the user to configure the resources using + /// Executes the action provided by the user to configure the resources using /// - private void UserConfigureResources(Action configureResources, - IResourceGraphBuilder resourceGraphBuilder) + private void UserConfigureResources(Action configureResources, + ResourceGraphBuilder resourceGraphBuilder) { configureResources?.Invoke(resourceGraphBuilder); } diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index e7658c176a..fc1f558c78 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Builders { - public class ResourceGraphBuilder : IResourceGraphBuilder + public class ResourceGraphBuilder { private readonly IJsonApiOptions _options; private readonly ILogger _logger; @@ -22,7 +22,7 @@ public class ResourceGraphBuilder : IResourceGraphBuilder public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { _options = options; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory?.CreateLogger(); } /// @@ -44,15 +44,15 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } /// - public IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable + public ResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable => AddResource(publicResourceName); /// - public IResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable + public ResourceGraphBuilder AddResource(string publicResourceName = null) where TResource : class, IIdentifiable => AddResource(typeof(TResource), typeof(TId), publicResourceName); /// - public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string publicResourceName = null) + public ResourceGraphBuilder AddResource(Type resourceType, Type idType = null, string publicResourceName = null) { if (_resources.All(e => e.ResourceType != resourceType)) { @@ -65,7 +65,7 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, } else { - _logger.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); + _logger?.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); } } diff --git a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs index 035c88b03c..9a1bcec7e1 100644 --- a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; @@ -29,7 +30,10 @@ public static void UseJsonApi(this IApplicationBuilder builder, Action(); jsonApiApplicationBuilder.ConfigureMvcOptions = options => { + options.InputFormatters.Insert(0, builder.ApplicationServices.GetRequiredService()); + options.OutputFormatters.Insert(0, builder.ApplicationServices.GetRequiredService()); options.Conventions.Insert(0, builder.ApplicationServices.GetRequiredService()); + configureMvcOptions?.Invoke(options); }; diff --git a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs index fea19dfaa0..2f2826bd08 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Internal.Contracts; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore { @@ -21,8 +22,8 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options = null, - Action discovery = null, - Action resources = null, + Action discovery = null, + Action resources = null, IMvcCoreBuilder mvcBuilder = null) { SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, null); @@ -36,25 +37,25 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, /// public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options = null, - Action discovery = null, - Action resources = null, - IMvcCoreBuilder mvcBuilder = null) + Action discovery = null, + Action resources = null, + IMvcCoreBuilder mvcBuilder = null, + ILoggerFactory loggerFactory = null) where TDbContext : DbContext { SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); ResolveInverseRelationships(services); return services; - } + } private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, - Action configureAutoDiscovery, - Action configureResources, IMvcCoreBuilder mvcBuilder, Type dbContextType) + Action configureAutoDiscovery, + Action configureResources, IMvcCoreBuilder mvcBuilder, Type dbContextType) { var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); applicationBuilder.ConfigureJsonApiOptions(configureOptions); - applicationBuilder.RegisterJsonApiStartupServices(); applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); applicationBuilder.AddResourceGraph(dbContextType, configureResources); applicationBuilder.ConfigureMvc(); diff --git a/src/JsonApiDotNetCore/Formatters/IJsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Formatters/IJsonApiInputFormatter.cs new file mode 100644 index 0000000000..385e7941f1 --- /dev/null +++ b/src/JsonApiDotNetCore/Formatters/IJsonApiInputFormatter.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace JsonApiDotNetCore.Formatters +{ + public interface IJsonApiInputFormatter : IInputFormatter { } +} diff --git a/src/JsonApiDotNetCore/Formatters/IJsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/IJsonApiOutputFormatter.cs new file mode 100644 index 0000000000..e0c9dba0fe --- /dev/null +++ b/src/JsonApiDotNetCore/Formatters/IJsonApiOutputFormatter.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Mvc.Formatters; + +namespace JsonApiDotNetCore.Formatters +{ + public interface IJsonApiOutputFormatter : IOutputFormatter { } +} diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs index 815f5a75d2..71532940ee 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Formatters { - public sealed class JsonApiInputFormatter : IInputFormatter + public sealed class JsonApiInputFormatter : IJsonApiInputFormatter { public bool CanRead(InputFormatterContext context) { diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs index dfd6840b92..15aa03a967 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiOutputFormatter.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Formatters { - public sealed class JsonApiOutputFormatter : IOutputFormatter + public sealed class JsonApiOutputFormatter : IJsonApiOutputFormatter { public bool CanWriteResult(OutputFormatterCanWriteContext context) { diff --git a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs deleted file mode 100644 index ce10bd8172..0000000000 --- a/src/JsonApiDotNetCore/Graph/IServiceDiscoveryFacade.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; - -namespace JsonApiDotNetCore.Graph -{ - public interface IServiceDiscoveryFacade - { - /// - /// Registers the designated assembly for discovery of JsonApiDotNetCore services and resources. - /// - ServiceDiscoveryFacade AddAssembly(Assembly assembly); - /// - /// Registers the current assembly for discovery of JsonApiDotNetCore services and resources. - /// - ServiceDiscoveryFacade AddCurrentAssembly(); - - /// - /// Discovers JsonApiDotNetCore services in the registered assemblies and adds them to the DI container. - /// - internal void DiscoverInjectables(); - - /// - /// Discovers JsonApiDotNetCore resources in the registered assemblies and adds them to the resource graph. - /// - internal void DiscoverResources(); - } -} diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index 7628a049a2..1b42e64c63 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -9,10 +9,11 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Graph { - public class ServiceDiscoveryFacade : IServiceDiscoveryFacade + public class ServiceDiscoveryFacade { internal static readonly HashSet ServiceInterfaces = new HashSet { typeof(IResourceService<>), @@ -47,14 +48,16 @@ public class ServiceDiscoveryFacade : IServiceDiscoveryFacade }; private readonly IServiceCollection _services; - private readonly IResourceGraphBuilder _resourceGraphBuilder; + private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); private readonly Dictionary> _resourceDescriptorsPerAssemblyCache = new Dictionary>(); - - public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder resourceGraphBuilder) + private readonly ILogger _logger; + + public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, ILoggerFactory loggerFactory) { _services = services; _resourceGraphBuilder = resourceGraphBuilder; + _logger = loggerFactory?.CreateLogger(); } /// @@ -64,12 +67,13 @@ public ServiceDiscoveryFacade(IServiceCollection services, IResourceGraphBuilder public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { _resourceDescriptorsPerAssemblyCache.Add(assembly, null); - + _logger?.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); + return this; } /// - void IServiceDiscoveryFacade.DiscoverResources() + internal void DiscoverResources() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { @@ -83,7 +87,7 @@ void IServiceDiscoveryFacade.DiscoverResources() } /// - void IServiceDiscoveryFacade.DiscoverInjectables() + internal void DiscoverInjectables() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) { diff --git a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs index 204711f951..3a3420bcbf 100644 --- a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs @@ -4,14 +4,9 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; namespace JsonApiDotNetCore.Middleware -{ - /// - /// Converts action result without parameters into action result with null parameter. - /// For example: return NotFound() -> return NotFound(null) - /// This ensures our formatter is invoked, where we'll build a json:api compliant response. - /// For details, see: https://github.com/dotnet/aspnetcore/issues/16969 - /// - public sealed class ConvertEmptyActionResultFilter : IAlwaysRunResultFilter +{ + /// + public sealed class ConvertEmptyActionResultFilter : IConvertEmptyActionResultFilter { public void OnResultExecuted(ResultExecutedContext context) { /* noop */ } diff --git a/src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs new file mode 100644 index 0000000000..9c7b621410 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Converts action result without parameters into action result with null parameter. + /// For example: return NotFound() -> return NotFound(null) + /// This ensures our formatter is invoked, where we'll build a json:api compliant response. + /// For details, see: https://github.com/dotnet/aspnetcore/issues/16969 + /// + public interface IConvertEmptyActionResultFilter : IAlwaysRunResultFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs index d29a76625f..286bbd0e58 100644 --- a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs @@ -10,5 +10,5 @@ namespace JsonApiDotNetCore.Middleware /// /// Entry point for processing all query string parameters. /// - public interface IQueryStringActionFilter : IAsyncActionFilter { } + public interface IQueryStringActionFilter : IActionFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs deleted file mode 100644 index e46805cecc..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Linq; -using System.Net.Http; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Action filter used to verify the incoming type matches the target type, else return a 409 - /// - public sealed class IncomingTypeMatchFilter : IActionFilter - { - private readonly IResourceContextProvider _provider; - - public IncomingTypeMatchFilter(IResourceContextProvider provider) - { - _provider = provider; - } - - public void OnActionExecuting(ActionExecutingContext context) - { - if (!context.HttpContext.IsJsonApiRequest()) - { - return; - } - - var request = context.HttpContext.Request; - if (request.Method == "PATCH" || request.Method == "POST") - { - var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); - var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; - - if (deserializedType != null && targetType != null && deserializedType != targetType) - { - ResourceContext resourceFromEndpoint = _provider.GetResourceContext(targetType); - ResourceContext resourceFromBody = _provider.GetResourceContext(deserializedType); - - throw new ResourceTypeMismatchException(new HttpMethod(request.Method), request.Path, resourceFromEndpoint, resourceFromBody); - } - } - } - - public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs index aa20e66950..fc47816bc2 100644 --- a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs @@ -10,13 +10,15 @@ namespace JsonApiDotNetCore.Middleware public sealed class QueryStringActionFilter : IQueryStringActionFilter { private readonly IQueryStringReader _queryStringReader; - + public QueryStringActionFilter(IQueryStringReader queryStringReader) { _queryStringReader = queryStringReader; } + + public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + public void OnActionExecuting(ActionExecutingContext context) { if (!context.HttpContext.IsJsonApiRequest()) { @@ -24,9 +26,8 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE } var disableQueryAttribute = context.Controller.GetType().GetCustomAttribute(); - + _queryStringReader.ReadAll(disableQueryAttribute); - await next(); } } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index b9590f9220..15d753e8de 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -59,7 +59,7 @@ public ServiceDiscoveryFacadeTests() public void AddAssembly_Adds_All_Resources_To_Graph() { // Arrange - IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); facade.AddAssembly(typeof(Person).Assembly); facade.DiscoverResources(); @@ -76,7 +76,7 @@ public void AddAssembly_Adds_All_Resources_To_Graph() public void AddCurrentAssembly_Adds_Resources_To_Graph() { // Arrange - IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); // Act facade.AddCurrentAssembly(); @@ -92,7 +92,7 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() public void AddCurrentAssembly_Adds_Services_To_Container() { // Arrange - IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); // Act facade.AddCurrentAssembly(); @@ -108,7 +108,7 @@ public void AddCurrentAssembly_Adds_Services_To_Container() public void AddCurrentAssembly_Adds_Repositories_To_Container() { // Arrange - IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); // Act facade.AddCurrentAssembly(); @@ -123,7 +123,7 @@ public void AddCurrentAssembly_Adds_Repositories_To_Container() public void AddCurrentAssembly_Adds_ResourceDefinitions_To_Container() { // Arrange - IServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); // Act facade.AddCurrentAssembly(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs index 6b936df648..00cbf98e0b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs @@ -181,9 +181,7 @@ private IRequestSerializer GetSerializer(Expression public sealed class NoNamespaceStartup : TestStartup { - public NoNamespaceStartup(IConfiguration configuration) : base(configuration) - { - } + public NoNamespaceStartup(IConfiguration configuration) : base(configuration) { } protected override void ConfigureJsonApiOptions(JsonApiOptions options) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs index aac419c83a..72d5f44615 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs @@ -13,9 +13,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests public sealed class TestableStartup : EmptyStartup where TDbContext : DbContext { - public TestableStartup(IConfiguration configuration) : base(configuration) - { - } + public TestableStartup(IConfiguration configuration) : base(configuration) { } public override void ConfigureServices(IServiceCollection services) { From 9a7ebe0973c7651f4057afc04ed16164e8658b73 Mon Sep 17 00:00:00 2001 From: maurei Date: Mon, 31 Aug 2020 11:08:41 +0200 Subject: [PATCH 23/51] fix: remove unnecessary parameter usejsonapi --- src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs | 1 + src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs | 2 +- .../Extensions/ApplicationBuilderExtensions.cs | 5 +---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index 767a22b9f0..9bab1e16b9 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -29,6 +29,7 @@ public Startup(IConfiguration configuration) : base(configuration) public override void ConfigureServices(IServiceCollection services) { ConfigureClock(services); + services.AddScoped(); services.AddScoped(sp => sp.GetService()); diff --git a/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs index 6c38ff376f..40fb6e60af 100644 --- a/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IJsonApiApplicationBuilder.cs @@ -5,6 +5,6 @@ namespace JsonApiDotNetCore.Builders { internal interface IJsonApiApplicationBuilder { - public Action ConfigureMvcOptions { get; set; } + public Action ConfigureMvcOptions { set; } } } diff --git a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs index 9a1bcec7e1..9b6fcbecbe 100644 --- a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -15,7 +15,6 @@ public static class ApplicationBuilderExtensions /// Registers the JsonApiDotNetCore middleware. /// /// The to add the middleware to. - /// Configure .NET Core MVC options /// /// The code below is the minimal that is required for proper activation, /// which should be added to your Startup.Configure method. @@ -25,7 +24,7 @@ public static class ApplicationBuilderExtensions /// app.UseEndpoints(endpoints => endpoints.MapControllers()); /// ]]> /// - public static void UseJsonApi(this IApplicationBuilder builder, Action configureMvcOptions = null) + public static void UseJsonApi(this IApplicationBuilder builder) { var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService(); jsonApiApplicationBuilder.ConfigureMvcOptions = options => @@ -33,8 +32,6 @@ public static void UseJsonApi(this IApplicationBuilder builder, Action()); options.OutputFormatters.Insert(0, builder.ApplicationServices.GetRequiredService()); options.Conventions.Insert(0, builder.ApplicationServices.GetRequiredService()); - - configureMvcOptions?.Invoke(options); }; builder.UseMiddleware(); From 6ff5883141803a3224c6ee60bdb9b03384abd37e Mon Sep 17 00:00:00 2001 From: maurei Date: Mon, 31 Aug 2020 17:17:33 +0200 Subject: [PATCH 24/51] fix: review round 31/08 --- .../Builders/JsonApiApplicationBuilder.cs | 2 +- .../Extensions/ApplicationBuilderExtensions.cs | 5 +++++ .../Extensions/ServiceCollectionExtensions.cs | 2 -- .../Internal/InverseRelationships.cs | 13 ++++++++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index e81a9282ba..38712cc476 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -152,7 +152,7 @@ public void ConfigureServices(Type dbContextType) AddResourceHooks(); } - _services.AddScoped(); + _services.TryAddScoped(); } private void AddMiddlewareLayer() diff --git a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs index 9b6fcbecbe..ee2832c9d4 100644 --- a/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ApplicationBuilderExtensions.cs @@ -26,6 +26,11 @@ public static class ApplicationBuilderExtensions /// public static void UseJsonApi(this IApplicationBuilder builder) { + + using var scope = builder.ApplicationServices.GetRequiredService().CreateScope(); + var inverseRelationshipResolver = scope.ServiceProvider.GetRequiredService(); + inverseRelationshipResolver.Resolve(); + var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService(); jsonApiApplicationBuilder.ConfigureMvcOptions = options => { diff --git a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs index 2f2826bd08..8015904fb7 100644 --- a/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/ServiceCollectionExtensions.cs @@ -27,7 +27,6 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, IMvcCoreBuilder mvcBuilder = null) { SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, null); - ResolveInverseRelationships(services); return services; } @@ -44,7 +43,6 @@ public static IServiceCollection AddJsonApi(this IServiceCollection where TDbContext : DbContext { SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); - ResolveInverseRelationships(services); return services; } diff --git a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs index 49b36053e6..1550aa93a6 100644 --- a/src/JsonApiDotNetCore/Internal/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Internal/InverseRelationships.cs @@ -48,13 +48,20 @@ public void Resolve() foreach (ResourceContext ce in _provider.GetResourceContexts()) { IEntityType meta = context.Model.FindEntityType(ce.ResourceType); - if (meta == null) continue; + if (meta == null) + { + continue; + } + foreach (var attr in ce.Relationships) { - if (attr is HasManyThroughAttribute) continue; + if (attr is HasManyThroughAttribute) + { + continue; + } INavigation inverseNavigation = meta.FindNavigation(attr.Property.Name)?.FindInverse(); attr.InverseNavigation = inverseNavigation?.Name; - } + } } } } From a0d4cfcc1a926c435fa61ba5e22a6b77f7c4f0a8 Mon Sep 17 00:00:00 2001 From: maurei Date: Mon, 31 Aug 2020 17:47:18 +0200 Subject: [PATCH 25/51] fix: docs --- docs/usage/routing.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 639233645d..526c79b24c 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -43,17 +43,8 @@ It is possible to fully customize routing behaviour by registering a `IJsonApiRo // Startup.cs public void ConfigureServices(IServiceCollection services) { -<<<<<<< HEAD services.AddSingleton(); services.AddJsonApi( /* ... */ ); -======= - public CamelCasedModelsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } ->>>>>>> master } ``` From 60b9640b6d35cef61ac0f6545f6ecb28d6860489 Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 1 Sep 2020 09:28:23 +0200 Subject: [PATCH 26/51] fix: merge I --- .../Configuration/ApplicationBuilderExtensions.cs | 3 ++- .../Configuration/IJsonApiApplicationBuilder.cs | 2 +- .../Configuration/JsonApiApplicationBuilder.cs | 3 +-- src/JsonApiDotNetCore/Configuration/ResourceGraph.cs | 10 ++++++++-- .../Configuration/ServiceCollectionExtensions.cs | 2 -- .../Configuration/ServiceDiscoveryFacade.cs | 2 -- .../Middleware/JsonApiRoutingConvention.cs | 10 +++++++--- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs index fd08fcacbb..24f22c262a 100644 --- a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs @@ -1,4 +1,3 @@ -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; @@ -34,6 +33,8 @@ public static void UseJsonApi(this IApplicationBuilder builder) options.OutputFormatters.Insert(0, builder.ApplicationServices.GetRequiredService()); options.Conventions.Insert(0, builder.ApplicationServices.GetRequiredService()); }; + + builder.UseMiddleware(); } } } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiApplicationBuilder.cs index 40fb6e60af..53b84e36d1 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiApplicationBuilder.cs @@ -1,7 +1,7 @@ using System; using Microsoft.AspNetCore.Mvc; -namespace JsonApiDotNetCore.Builders +namespace JsonApiDotNetCore.Configuration { internal interface IJsonApiApplicationBuilder { diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 7deaa2c72f..df1b97ec7d 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -1,4 +1,3 @@ - using System; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Formatters; @@ -23,7 +22,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; -namespace JsonApiDotNetCore.Builders +namespace JsonApiDotNetCore.Configuration { /// /// A utility class that builds a JsonApi application. It registers all required services diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index f8367af7f5..78ae0e65af 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -24,7 +24,10 @@ public ResourceGraph(IReadOnlyCollection resources) /// public ResourceContext GetResourceContext(string resourceName) { - if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); + if (resourceName == null) + { + throw new ArgumentNullException(nameof(resourceName)); + } return _resources.SingleOrDefault(e => e.ResourceName == resourceName); } @@ -32,7 +35,10 @@ public ResourceContext GetResourceContext(string resourceName) /// public ResourceContext GetResourceContext(Type resourceType) { - if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); + if (resourceType == null) + { + throw new ArgumentNullException(nameof(resourceType)); + } return IsLazyLoadingProxyForResourceType(resourceType) ? _resources.SingleOrDefault(e => e.ResourceType == resourceType.BaseType) diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index bb3ec4a8b0..a4e341b012 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -1,9 +1,7 @@ - using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Client.Internal; diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index e8902d9288..b68bcf3054 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +7,6 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index c80e36ff00..0ac6142f6c 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -62,11 +62,15 @@ public void Apply(ApplicationModel application) foreach (var controller in application.Controllers) { var resourceType = ExtractResourceTypeFromController(controller.ControllerType); - var resourceContext = _resourceGraph.GetResourceContext(resourceType); - if (resourceContext != null) + if (resourceType != null) { - _registeredResources.Add(controller.ControllerName, resourceContext); + var resourceContext = _resourceGraph.GetResourceContext(resourceType); + + if (resourceContext != null) + { + _registeredResources.Add(controller.ControllerName, resourceContext); + } } if (RoutingConventionDisabled(controller) == false) From 526b0c68e2410d368295cd0c46be8481b492dcec Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 1 Sep 2020 09:47:13 +0200 Subject: [PATCH 27/51] fix: merge II --- .../Configuration/JsonApiApplicationBuilder.cs | 4 ++-- .../Configuration/ResourceGraphBuilder.cs | 2 +- .../Configuration/ServiceDiscoveryFacade.cs | 4 ++-- .../Middleware/IQueryStringActionFilter.cs | 2 +- .../Middleware/JsonApiExceptionFilter.cs | 2 +- .../Middleware/JsonApiRoutingConvention.cs | 8 ++++---- .../Models/Annotation/ResourceAttribute.cs | 15 --------------- 7 files changed, 11 insertions(+), 26 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index df1b97ec7d..bf82e12b48 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -40,8 +40,8 @@ internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { - _services = services; - _mvcBuilder = mvcBuilder; + _services = services ?? throw new ArgumentNullException(nameof(services)); + _mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder)); _intermediateProvider = services.BuildServiceProvider(); var loggerFactory = _intermediateProvider.GetService(); _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 6ea0f8f94d..3a96f96d3c 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -17,7 +17,7 @@ public class ResourceGraphBuilder public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = loggerFactory?.CreateLogger(); } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index b68bcf3054..8211b467e1 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -54,8 +54,8 @@ public class ServiceDiscoveryFacade public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, ILoggerFactory loggerFactory) { - _services = services; - _resourceGraphBuilder = resourceGraphBuilder; + _services = services ?? throw new ArgumentNullException(nameof(services)); + _resourceGraphBuilder = resourceGraphBuilder ?? throw new ArgumentNullException(nameof(resourceGraphBuilder)); _logger = loggerFactory?.CreateLogger(); } diff --git a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs index 2d8adf12ef..463150c44c 100644 --- a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Entry point for processing all query string parameters. + /// Extensibility point for processing request query strings. /// public interface IQueryStringActionFilter : IActionFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs index 2b121f51e6..a77bcb800c 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs @@ -23,7 +23,7 @@ public void OnException(ExceptionContext context) throw new ArgumentNullException(nameof(context)); } - if (context.HttpContext.IsJsonApiRequest()) + if (!context.HttpContext.IsJsonApiRequest()) { return; } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 0ac6142f6c..8766a50257 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -39,8 +39,8 @@ public class JsonApiRoutingConvention : IJsonApiRoutingConvention public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph) { - _options = options; - _resourceGraph = resourceGraph; + _options = options ?? throw new ArgumentNullException(nameof(options)); + _resourceGraph = resourceGraph ?? throw new ArgumentNullException(nameof(resourceGraph)); } /// @@ -73,7 +73,7 @@ public void Apply(ApplicationModel application) } } - if (RoutingConventionDisabled(controller) == false) + if (!RoutingConventionDisabled(controller)) { continue; } @@ -85,7 +85,7 @@ public void Apply(ApplicationModel application) $"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); } - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel {Template = template}; + controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; } } diff --git a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs b/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs deleted file mode 100644 index 03632e8318..0000000000 --- a/src/JsonApiDotNetCore/Models/Annotation/ResourceAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Models.Annotation -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] - public sealed class ResourceAttribute : Attribute - { - public ResourceAttribute(string publicResourceName) - { - ResourceName = publicResourceName; - } - - public string ResourceName { get; } - } -} From 21ed99085a20f1543e5dcf8e9b6a695c294eb6e1 Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 1 Sep 2020 10:50:57 +0200 Subject: [PATCH 28/51] fix: docs, publicName --- docs/usage/extensibility/middleware.md | 80 ++++++++++++++++++- docs/usage/options.md | 1 + docs/usage/resource-graph.md | 4 +- docs/usage/routing.md | 3 +- .../Configuration/ResourceGraphBuilder.cs | 18 ++--- 5 files changed, 89 insertions(+), 17 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index dc1c7a6e23..25fe71140e 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -1,14 +1,86 @@ # Middleware -Add the following to your Startup.ConfigureServices method. Replace AppDbContext with your DbContext. -```c3 +The following is the default configuration of JsonApiDotNetCore: +1. Call one of the `AddJsonApi( ... )` overloads in the ` Startup.ConfigureServices` method. In this example uses a `DbContext` to build the resource graph + +```c# services.AddJsonApi(); ``` -Add the middleware to the Startup.Configure method. +2. In the Startup.Configure method, configure your application to use routing, to add the JsonApiMiddleware and to configure endpoint routing. -```c3 +```c# app.UseRouting(); app.UseJsonApi(); app.UseEndpoints(endpoints => endpoints.MapControllers()); ``` + +The following middleware components, in respective order, are registered: + + + +Filters: +- `IJsonApiExceptionFilter` +- `IJsonApiTypeMatchFilter` +- `IQueryStringActionFilter` +- `IConvertEmptyActionResultFilter` + +Formatters: +- `IJsonApiInputFormatter` +- `IJsonApiOutputFormatter` + +Routing convention: +- `IJsonApiRoutingConvention` + +Middleware: +- `JsonApiMiddleware` + +All of these components (except for `JsonApiMiddleware`) can be customized by registering your own implementation of these services. For example: + +```c# +services.AddSingleton(); +``` + +It is also possible to directly access the .NET Core `MvcOptions` object and have full controll over which components are registered. + +## Configuring MvcOptions + +JsonApiDotNetCore internally configures `MvcOptions` when calling `AddJsonApi( ... )`. However, it is still possible to register a custom configuration callback. To achieve this it is recommended to register this callback *after* the `AddJsonApi( ... )` call. It is also possible to do it earlier, but your configuration might be overridden by the JsonApiDotNetCore configuration. + +The following example replaces all internally registered filters by retrieving a custom filter from the DI container. +```c# +public class Startup +{ + private Action _postConfigureMvcOptions; + + public void ConfigureServices(IServiceCollection services) + { + ... + + var builder = services.AddMvcCore(); + services.AddJsonApi( ... , mvcBuilder: builder); + mvcCoreBuilder.AddMvcOptions(x => + { + // execute the mvc configuration callback after the JsonApiDotNetCore callback as been executed. + _postConfigureMvcOptions?.Invoke(x); + }); + + ... + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + + ... + + // Using a callback, we can defer to later (when service collection has become available). + _postConfigureMvcOptions = mvcOptions => + { + mvcOptions.Filters.Clear(); + mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); + }; + + ... + } +} +``` diff --git a/docs/usage/options.md b/docs/usage/options.md index fe5751aa60..a91c68b944 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -92,6 +92,7 @@ options.SerializerSettings.Formatting = Formatting.Indented; Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored. + ## Enable ModelState Validation If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState = true`. By default, no model validation is performed. diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index a3a01250a4..f7c36dd59c 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -81,11 +81,11 @@ public void ConfigureServices(IServiceCollection services) The public resource name is exposed through the `type` member in the json:api payload. This can be configured by the following approaches (in order of priority): -1. The `publicResourceName` option when manually adding a resource to the graph +1. The `publicName` option when manually adding a resource to the graph ```c# services.AddJsonApi(resources: builder => { - builder.AddResource(publicResourceName: "people"); + builder.AddResource(publicName: "people"); }); ``` diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 526c79b24c..8dd05a67b1 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -38,13 +38,12 @@ GET /myResources HTTP/1.1 ``` ## Customized the Routing Convention -It is possible to fully customize routing behaviour by registering a `IJsonApiRoutingConvention` implementation **before** calling `AddJsonApi( ... )`. +It is possible to fully customize routing behaviour by registering a `IJsonApiRoutingConvention` implementation. ```c# // Startup.cs public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); - services.AddJsonApi( /* ... */ ); } ``` diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 3a96f96d3c..0abd958ea7 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -40,15 +40,15 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } /// - public ResourceGraphBuilder Add(string publicResourceName = null) where TResource : class, IIdentifiable - => Add(publicResourceName); + public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable + => Add(publicName); /// - public ResourceGraphBuilder Add(string publicResourceName = null) where TResource : class, IIdentifiable - => Add(typeof(TResource), typeof(TId), publicResourceName); + public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable + => Add(typeof(TResource), typeof(TId), publicName); /// - public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicResourceName = null) + public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) { if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); @@ -56,9 +56,9 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu { if (TypeHelper.IsOrImplementsInterface(resourceType, typeof(IIdentifiable))) { - publicResourceName ??= FormatResourceName(resourceType); + publicName ??= FormatResourceName(resourceType); idType ??= TypeLocator.GetIdType(resourceType); - var resourceContext = CreateResourceContext(publicResourceName, resourceType, idType); + var resourceContext = CreateResourceContext(publicName, resourceType, idType); _resources.Add(resourceContext); } else @@ -70,9 +70,9 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu return this; } - private ResourceContext CreateResourceContext(string publicResourceName, Type resourceType, Type idType) => new ResourceContext + private ResourceContext CreateResourceContext(string publicName, Type resourceType, Type idType) => new ResourceContext { - ResourceName = publicResourceName, + ResourceName = publicName, ResourceType = resourceType, IdentityType = idType, Attributes = GetAttributes(resourceType), From f519342f19c80605800078e95bf2dcaed14c2ddd Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 1 Sep 2020 10:52:06 +0200 Subject: [PATCH 29/51] fix: whitespace --- docs/usage/extensibility/middleware.md | 2 -- docs/usage/options.md | 1 - 2 files changed, 3 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 25fe71140e..aba91a4db0 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -17,8 +17,6 @@ app.UseEndpoints(endpoints => endpoints.MapControllers()); The following middleware components, in respective order, are registered: - - Filters: - `IJsonApiExceptionFilter` - `IJsonApiTypeMatchFilter` diff --git a/docs/usage/options.md b/docs/usage/options.md index a91c68b944..fe5751aa60 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -92,7 +92,6 @@ options.SerializerSettings.Formatting = Formatting.Indented; Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored. - ## Enable ModelState Validation If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState = true`. By default, no model validation is performed. From c51eea3f25863b3034cd628802b2e2ffb710ec6e Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 2 Sep 2020 16:21:29 +0200 Subject: [PATCH 30/51] chore: review --- src/JsonApiDotNetCore/AssemblyInfo.cs | 7 --- .../ApplicationBuilderExtensions.cs | 12 +++-- .../JsonApiApplicationBuilder.cs | 23 +++++---- .../Configuration/ResourceGraphBuilder.cs | 50 +++++++++++++------ .../ServiceCollectionExtensions.cs | 24 ++++----- .../Configuration/ServiceDiscoveryFacade.cs | 26 +++++++--- .../ConvertEmptyActionResultFilter.cs | 7 ++- .../IJsonApiInputFormatter.cs | 2 +- .../IJsonApiOutputFormatter.cs | 2 +- .../Middleware/IJsonApiTypeMatchFilter.cs | 2 +- .../Middleware/IncomingTypeMatchFilter.cs | 49 ------------------ .../Middleware/JsonApiInputFormatter.cs | 1 - .../Middleware/JsonApiOutputFormatter.cs | 1 - .../Middleware/JsonApiRoutingConvention.cs | 10 +++- .../Middleware/JsonApiTypeMatchFilter.cs | 6 +++ .../Middleware/QueryStringActionFilter.cs | 7 +-- .../Properties/AssemblyInfo.cs | 3 +- 17 files changed, 110 insertions(+), 122 deletions(-) delete mode 100644 src/JsonApiDotNetCore/AssemblyInfo.cs rename src/JsonApiDotNetCore/{Serialization => Middleware}/IJsonApiInputFormatter.cs (74%) rename src/JsonApiDotNetCore/{Serialization => Middleware}/IJsonApiOutputFormatter.cs (74%) delete mode 100644 src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs diff --git a/src/JsonApiDotNetCore/AssemblyInfo.cs b/src/JsonApiDotNetCore/AssemblyInfo.cs deleted file mode 100644 index dc72a2c8e4..0000000000 --- a/src/JsonApiDotNetCore/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("UnitTests")] -[assembly:InternalsVisibleTo("DiscoveryTests")] -[assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] -[assembly:InternalsVisibleTo("NoEntityFrameworkTests")] -[assembly:InternalsVisibleTo("Benchmarks")] -[assembly:InternalsVisibleTo("ResourceEntitySeparationExampleTests")] diff --git a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs index 24f22c262a..83902c054f 100644 --- a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs @@ -1,4 +1,3 @@ -using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -29,9 +28,14 @@ public static void UseJsonApi(this IApplicationBuilder builder) var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService(); jsonApiApplicationBuilder.ConfigureMvcOptions = options => { - options.InputFormatters.Insert(0, builder.ApplicationServices.GetRequiredService()); - options.OutputFormatters.Insert(0, builder.ApplicationServices.GetRequiredService()); - options.Conventions.Insert(0, builder.ApplicationServices.GetRequiredService()); + var inputFormatter = builder.ApplicationServices.GetRequiredService(); + options.InputFormatters.Insert(0, inputFormatter); + + var outputFormatter = builder.ApplicationServices.GetRequiredService(); + options.OutputFormatters.Insert(0, outputFormatter); + + var routingConvention = builder.ApplicationServices.GetRequiredService(); + options.Conventions.Insert(0, routingConvention); }; builder.UseMiddleware(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index bf82e12b48..c8998713b9 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -1,6 +1,4 @@ using System; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Hooks.Internal; using JsonApiDotNetCore.Hooks.Internal.Discovery; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -28,7 +26,7 @@ namespace JsonApiDotNetCore.Configuration /// A utility class that builds a JsonApi application. It registers all required services /// and allows the user to override parts of the startup configuration. /// - internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder + internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, IDisposable { private readonly JsonApiOptions _options = new JsonApiOptions(); private readonly IServiceCollection _services; @@ -36,14 +34,17 @@ internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly ServiceDiscoveryFacade _serviceDiscoveryFacade; private readonly ServiceProvider _intermediateProvider; + public Action ConfigureMvcOptions { get; set; } public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { _services = services ?? throw new ArgumentNullException(nameof(services)); _mvcBuilder = mvcBuilder ?? throw new ArgumentNullException(nameof(mvcBuilder)); + _intermediateProvider = services.BuildServiceProvider(); var loggerFactory = _intermediateProvider.GetService(); + _resourceGraphBuilder = new ResourceGraphBuilder(_options, loggerFactory); _serviceDiscoveryFacade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, loggerFactory); } @@ -62,16 +63,16 @@ public void ConfigureAutoDiscovery(Action configureAutoD } /// - /// Configures and build the resource graph with resources from the provided sources and adds it to the DI container. + /// Configures and builds the resource graph with resources from the provided sources and adds it to the DI container. /// - public void AddResourceGraph(Type dbContextType, Action configureResources) + public void AddResourceGraph(Type dbContextType, Action configureResourceGraph) { AutoDiscoverResources(_serviceDiscoveryFacade); if (dbContextType != null) { AddResourcesFromDbContext((DbContext)_intermediateProvider.GetService(dbContextType), _resourceGraphBuilder); } - UserConfigureResources(configureResources, _resourceGraphBuilder); + ExecuteManualConfigurationOfResources(configureResourceGraph, _resourceGraphBuilder); _services.AddSingleton(_resourceGraphBuilder.Build()); } @@ -105,7 +106,6 @@ public void ConfigureMvc() public void DiscoverInjectables() { _serviceDiscoveryFacade.DiscoverInjectables(); - _intermediateProvider.Dispose(); } /// @@ -140,6 +140,7 @@ public void ConfigureServices(Type dbContextType) AddServerSerialization(); AddQueryStringParameterServices(); + if (_options.EnableResourceHooks) { AddResourceHooks(); @@ -288,11 +289,15 @@ private void AutoDiscoverResources(ServiceDiscoveryFacade serviceDiscoveryFacade /// /// Executes the action provided by the user to configure the resources using /// - private void UserConfigureResources(Action configureResources, + private void ExecuteManualConfigurationOfResources(Action configureResources, ResourceGraphBuilder resourceGraphBuilder) { configureResources?.Invoke(resourceGraphBuilder); } - + + public void Dispose() + { + _intermediateProvider?.Dispose(); + } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 0abd958ea7..a11aa53553 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -11,17 +11,24 @@ namespace JsonApiDotNetCore.Configuration { public class ResourceGraphBuilder { - private readonly IJsonApiOptions _options; private readonly ILogger _logger; + private readonly IJsonApiOptions _options; private readonly List _resources = new List(); public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _logger = loggerFactory.CreateLogger(); _options = options ?? throw new ArgumentNullException(nameof(options)); - _logger = loggerFactory?.CreateLogger(); } - /// + /// + /// Constructs the . + /// public IResourceGraph Build() { _resources.ForEach(SetResourceLinksOptions); @@ -39,15 +46,28 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } } - /// - public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable - => Add(publicName); - - /// + /// + /// Adds a json:api resource to the resource graph. + /// + /// The resource type. + /// The identifier type of the resource + /// + /// The name under which the resource is publicly exposed by the API. + /// If nothing is specified, the configured casing convention formatter will be applied. + /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable => Add(typeof(TResource), typeof(TId), publicName); + + /// + /// + /// + public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable + => Add(publicName); + - /// + /// + /// + /// public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) { if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); @@ -63,7 +83,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu } else { - _logger?.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); + _logger.LogWarning($"Entity '{resourceType}' does not implement '{nameof(IIdentifiable)}'."); } } @@ -81,7 +101,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu ResourceDefinitionType = GetResourceDefinitionType(resourceType) }; - protected virtual IReadOnlyCollection GetAttributes(Type resourceType) + private IReadOnlyCollection GetAttributes(Type resourceType) { if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); @@ -122,7 +142,7 @@ protected virtual IReadOnlyCollection GetAttributes(Type resource return attributes; } - protected virtual IReadOnlyCollection GetRelationships(Type resourceType) + private IReadOnlyCollection GetRelationships(Type resourceType) { if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); @@ -180,7 +200,7 @@ protected virtual IReadOnlyCollection GetRelationships(Ty return attributes; } - private static Type TryGetThroughType(PropertyInfo throughProperty) + private Type TryGetThroughType(PropertyInfo throughProperty) { if (throughProperty.PropertyType.IsGenericType) { @@ -198,7 +218,7 @@ private static Type TryGetThroughType(PropertyInfo throughProperty) return null; } - protected virtual Type GetRelationshipType(RelationshipAttribute relationship, PropertyInfo property) + private Type GetRelationshipType(RelationshipAttribute relationship, PropertyInfo property) { if (relationship == null) throw new ArgumentNullException(nameof(relationship)); if (property == null) throw new ArgumentNullException(nameof(property)); @@ -231,7 +251,7 @@ private IReadOnlyCollection GetEagerLoads(Type resourceType, return attributes; } - private static Type TypeOrElementType(Type type) + private Type TypeOrElementType(Type type) { var interfaces = type.GetInterfaces() .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).ToArray(); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index a4e341b012..5523bf7824 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -36,11 +36,13 @@ public static IServiceCollection AddJsonApi(this IServiceCollection Action options = null, Action discovery = null, Action resources = null, - IMvcCoreBuilder mvcBuilder = null, - ILoggerFactory loggerFactory = null) + IMvcCoreBuilder mvcBuilder = null) where TDbContext : DbContext { - if (services == null) throw new ArgumentNullException(nameof(services)); + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); @@ -49,27 +51,19 @@ public static IServiceCollection AddJsonApi(this IServiceCollection private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, Action configureAutoDiscovery, - Action configureResources, IMvcCoreBuilder mvcBuilder, Type dbContextType) + Action configureResourceGraph, IMvcCoreBuilder mvcBuilder, Type dbContextType) { var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); applicationBuilder.ConfigureJsonApiOptions(configureOptions); applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); - applicationBuilder.AddResourceGraph(dbContextType, configureResources); + applicationBuilder.AddResourceGraph(dbContextType, configureResourceGraph); applicationBuilder.ConfigureMvc(); applicationBuilder.DiscoverInjectables(); applicationBuilder.ConfigureServices(dbContextType); + applicationBuilder.Dispose(); } - - private static void ResolveInverseRelationships(IServiceCollection services) - { - using var intermediateProvider = services.BuildServiceProvider(); - using var scope = intermediateProvider.CreateScope(); - - var inverseRelationshipResolver = scope.ServiceProvider.GetService(); - inverseRelationshipResolver?.Resolve(); - } - + /// /// Enables client serializers for sending requests and receiving responses /// in json:api format. Internally only used for testing. diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 8211b467e1..1c0cdd4602 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -12,6 +12,9 @@ namespace JsonApiDotNetCore.Configuration { + /// + /// Scans for types like resources, services, repositories and resource definitions in an assembly and registers them to the IoC container. + /// public class ServiceDiscoveryFacade { internal static readonly HashSet ServiceInterfaces = new HashSet { @@ -46,32 +49,40 @@ public class ServiceDiscoveryFacade typeof(IResourceReadRepository<,>) }; + private readonly ILogger _logger; private readonly IServiceCollection _services; private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); private readonly Dictionary> _resourceDescriptorsPerAssemblyCache = new Dictionary>(); - private readonly ILogger _logger; public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, ILoggerFactory loggerFactory) { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _logger = loggerFactory.CreateLogger(); _services = services ?? throw new ArgumentNullException(nameof(services)); _resourceGraphBuilder = resourceGraphBuilder ?? throw new ArgumentNullException(nameof(resourceGraphBuilder)); - _logger = loggerFactory?.CreateLogger(); } - /// + /// + /// Scan the calling assembly. + /// public ServiceDiscoveryFacade AddCurrentAssembly() => AddAssembly(Assembly.GetCallingAssembly()); - /// + /// + /// Scan the specified assembly. + /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { _resourceDescriptorsPerAssemblyCache.Add(assembly, null); - _logger?.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); + _logger.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); return this; } - - /// + internal void DiscoverResources() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) @@ -85,7 +96,6 @@ internal void DiscoverResources() } } - /// internal void DiscoverInjectables() { foreach (var (assembly, discoveredResourceDescriptors) in _resourceDescriptorsPerAssemblyCache.ToArray()) diff --git a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs index cbc9f7ee8e..57e2bfc316 100644 --- a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs @@ -12,7 +12,10 @@ public void OnResultExecuted(ResultExecutedContext context) { /* noop */ } public void OnResultExecuting(ResultExecutingContext context) { - if (context == null) throw new ArgumentNullException(nameof(context)); + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } if (!context.HttpContext.IsJsonApiRequest()) { @@ -24,7 +27,7 @@ public void OnResultExecuting(ResultExecutingContext context) case ObjectResult objectResult when objectResult.Value != null: return; case IStatusCodeActionResult statusCodeResult: - context.Result = new ObjectResult(null) {StatusCode = statusCodeResult.StatusCode}; + context.Result = new ObjectResult(null) { StatusCode = statusCodeResult.StatusCode }; break; } } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs similarity index 74% rename from src/JsonApiDotNetCore/Serialization/IJsonApiInputFormatter.cs rename to src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs index 385e7941f1..a984eb9441 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.Formatters; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Middleware { public interface IJsonApiInputFormatter : IInputFormatter { } } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs similarity index 74% rename from src/JsonApiDotNetCore/Serialization/IJsonApiOutputFormatter.cs rename to src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs index e0c9dba0fe..e30eca1aa5 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.Formatters; -namespace JsonApiDotNetCore.Formatters +namespace JsonApiDotNetCore.Middleware { public interface IJsonApiOutputFormatter : IOutputFormatter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs index cd5b95eaa1..3ba1fa2641 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Action filter used to verify the incoming type matches the target type, else return a 409 + /// Action filter used to verify the incoming type matches the target type, else return a 409. /// public interface IJsonApiTypeMatchFilter : IActionFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs deleted file mode 100644 index 8ee36e7e6c..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IncomingTypeMatchFilter.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Action filter used to verify the incoming resource type matches the target type, else return a 409. - /// - public sealed class IncomingTypeMatchFilter : IActionFilter - { - private readonly IResourceContextProvider _provider; - - public IncomingTypeMatchFilter(IResourceContextProvider provider) - { - _provider = provider; - } - - public void OnActionExecuting(ActionExecutingContext context) - { - if (context == null) throw new ArgumentNullException(nameof(context)); - - if (!context.HttpContext.IsJsonApiRequest()) - { - return; - } - - var request = context.HttpContext.Request; - if (request.Method == "PATCH" || request.Method == "POST") - { - var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); - var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; - - if (deserializedType != null && targetType != null && deserializedType != targetType) - { - ResourceContext resourceFromEndpoint = _provider.GetResourceContext(targetType); - ResourceContext resourceFromBody = _provider.GetResourceContext(deserializedType); - - throw new ResourceTypeMismatchException(new HttpMethod(request.Method), request.Path, resourceFromEndpoint, resourceFromBody); - } - } - } - - public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index 6f90f07df1..dacb80ba0c 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 4535ccd969..71fbb94de3 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using JsonApiDotNetCore.Formatters; using JsonApiDotNetCore.Serialization; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Extensions.DependencyInjection; diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 8766a50257..a6d3a84fd3 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -46,6 +46,11 @@ public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resource /// public Type GetAssociatedResource(string controllerName) { + if (controllerName == null) + { + throw new ArgumentNullException(nameof(controllerName)); + } + if (_registeredResources.TryGetValue(controllerName, out var resourceContext)) { return resourceContext.ResourceType; @@ -57,7 +62,10 @@ public Type GetAssociatedResource(string controllerName) /// public void Apply(ApplicationModel application) { - if (application == null) throw new ArgumentNullException(nameof(application)); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } foreach (var controller in application.Controllers) { diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs index d7aaec4212..0cd505e7a0 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Net.Http; using JsonApiDotNetCore.Configuration; @@ -17,6 +18,11 @@ public JsonApiTypeMatchFilter(IResourceContextProvider provider) public void OnActionExecuting(ActionExecutingContext context) { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (!context.HttpContext.IsJsonApiRequest()) { return; diff --git a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs index 978e8d4edb..fa860c4e00 100644 --- a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs @@ -29,12 +29,7 @@ public void OnActionExecuting(ActionExecutingContext context) return; } - var disableQueryAttribute = context.Controller.GetType().GetCustomAttribute(); - - _queryStringReader.ReadAll(disableQueryAttribute); - - DisableQueryStringAttribute disableQueryStringAttribute = context.Controller.GetType().GetCustomAttribute(); - + var disableQueryStringAttribute = context.Controller.GetType().GetCustomAttribute(); _queryStringReader.ReadAll(disableQueryStringAttribute); } } diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index aea73b3126..2d032b98d6 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly:InternalsVisibleTo("Benchmarks")] -[assembly: InternalsVisibleTo("IntegrationTests")] +[assembly:InternalsVisibleTo("IntegrationTests")] [assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] [assembly:InternalsVisibleTo("UnitTests")] +[assembly:InternalsVisibleTo("DiscoveryTests")] From 234cc868454923c229a683ead4ecc4adc6824717 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 2 Sep 2020 19:32:42 +0200 Subject: [PATCH 31/51] chore: review 2 --- docs/usage/extensibility/middleware.md | 10 +-- docs/usage/options.md | 6 ++ docs/usage/resource-graph.md | 23 ++----- docs/usage/routing.md | 66 ++++++++----------- .../Startups/EmptyStartup.cs | 1 - .../Startups/TestStartup.cs | 1 - .../Acceptance/NonJsonApiControllerTests.cs | 1 - 7 files changed, 43 insertions(+), 65 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index aba91a4db0..4b28dcfecb 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -39,11 +39,11 @@ All of these components (except for `JsonApiMiddleware`) can be customized by re services.AddSingleton(); ``` -It is also possible to directly access the .NET Core `MvcOptions` object and have full controll over which components are registered. +It is also possible to directly access the .NET Core `MvcOptions` object and have full control over which components are registered. ## Configuring MvcOptions -JsonApiDotNetCore internally configures `MvcOptions` when calling `AddJsonApi( ... )`. However, it is still possible to register a custom configuration callback. To achieve this it is recommended to register this callback *after* the `AddJsonApi( ... )` call. It is also possible to do it earlier, but your configuration might be overridden by the JsonApiDotNetCore configuration. +JsonApiDotNetCore internally configures `MvcOptions` when calling `AddJsonApi( ... )`. However, it is still possible to register a custom configuration callback. To achieve this it is recommended to register this callback *after* the `AddJsonApi( ... )` call. It is also possible to do it earlier, but your configuration might be overwritten by the JsonApiDotNetCore configuration. The following example replaces all internally registered filters by retrieving a custom filter from the DI container. ```c# @@ -57,10 +57,10 @@ public class Startup var builder = services.AddMvcCore(); services.AddJsonApi( ... , mvcBuilder: builder); - mvcCoreBuilder.AddMvcOptions(x => + mvcCoreBuilder.AddMvcOptions(mvcOptions => { - // execute the mvc configuration callback after the JsonApiDotNetCore callback as been executed. - _postConfigureMvcOptions?.Invoke(x); + // Execute the mvcOptions configuration callback after the JsonApiDotNetCore callback as been executed. + _postConfigureMvcOptions?.Invoke(mvcOptions); }); ... diff --git a/docs/usage/options.md b/docs/usage/options.md index fe5751aa60..4a6c914f74 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -90,8 +90,14 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter()); options.SerializerSettings.Formatting = Formatting.Indented; ``` +The default naming convention as used in the routes and public resources names is also determined here, and can be changed (default is camel-case): +```c# +options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() }; +``` + Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored. + ## Enable ModelState Validation If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState = true`. By default, no model validation is performed. diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index f7c36dd59c..02ea82684e 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -28,7 +28,7 @@ public void ConfigureServices(IServiceCollection services) { services.AddJsonApi(resources: builder => { - builder.AddResource(); + builder.Add(); }); } ``` @@ -60,7 +60,7 @@ public void ConfigureServices(IServiceCollection services) ### Auto-discovery Auto-discovery refers to the process of reflecting on an assembly and -detecting all of the json:api resources and services. +detecting all of the json:api resources, resource definitions, resource services and repositories. The following command will build the resource graph using all `IIdentifiable` implementations. It also injects resource definitions and service layer overrides which we will @@ -81,11 +81,11 @@ public void ConfigureServices(IServiceCollection services) The public resource name is exposed through the `type` member in the json:api payload. This can be configured by the following approaches (in order of priority): -1. The `publicName` option when manually adding a resource to the graph +1. The `publicName` parameter when manually adding a resource to the graph ```c# services.AddJsonApi(resources: builder => { - builder.AddResource(publicName: "people"); + builder.Add(publicName: "people"); }); ``` @@ -100,17 +100,4 @@ public class MyModel : Identifiable { /* ... */ } // this will be registered as "myModels" public class MyModel : Identifiable { /* ... */ } ``` -This convention can be changed by setting the `SerializerSettings` property on `IJsonApiOptions`. -```c# -public void ConfigureServices(IServiceCollection services) -{ - services.AddJsonApi( - options => - { - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new KebabCaseNamingStrategy() - } - }); -} -``` +This convention can be changed by setting the `SerializerSettings.ContractResolver` property on `IJsonApiOptions`. See the [options documentation](./options.md#custom-serializer-settings). diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 8dd05a67b1..86a10156bc 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -1,44 +1,44 @@ # Routing + +## Namespacing and Versioning URLs +You can add a namespace to all URLs by specifying it in ConfigureServices + +```c# +public void ConfigureServices(IServiceCollection services) +{ + services.AddJsonApi( + options => options.Namespace = "api/v1"); +} +``` +Which results in URLs like: https://yourdomain.com/api/v1/people + +## Customizing Routes + The library will configure routes for each controller. By default, based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased. ```http GET /api/compoundModels HTTP/1.1 ``` -There are two ways the library will try to create a route for a controller: -1. **By inspecting the controller for an associated resource**. The library will try to first use the public resource name of the resource associated to a controller. This means that the value of the `type` member of the json:api document for a resource will be equal to the route. -Note that this implies that it is possible to configure a route configuring the exposed resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how this can be achieved. Example: -```c# -// controller -public class MyResourceController : JsonApiController { /* .... */ } // note that the route is NOT "myResources", but "myApiResources" +1. **Using the public name of the resource associated to a controller**. -// request -GET /myApiResources HTTP/1.1 +```c# +public class MyResourceController : JsonApiController { /* .... */ } +``` +Note that the route +- is `/myApiResources`, which matches the public resouce name in the json:api payload (`{ "type": "myApiResources", ... }`) +- can be configured by configuring the public resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how to do that. -// response -HTTP/1.1 200 OK -Content-Type: application/vnd.api+json -{ - "data": [{ - "type": "myApiResources", - "id": "1", - "attributes": { ... } - }] -} -``` -2. **By using the name of the controller**. If no associated resource was detected for a controller, the library will construct a route from the name of the controller by using the configured naming strategy (*camelCase* by default, see [this section](~/usage/resource-graph.md#public-resource-name) on how to configure this). This is in alignment with the default .NET Core MVC routing approach. -In the following example the controller is not associated to a resource by the library because it does not inherit from `BaseJsonApiController`. +2. **Using the controller name**. +If a controller does not have an associated resource, the name of the controller will be used following the configured naming strategy. ```c# -// controller public class MyResourceController : ControllerBase { /* .... */ } - -// request -GET /myResources HTTP/1.1 ``` +Note that the route is `myResources` can be changed by renaming the controller. This approach is the default .NET Core MVC routing approach. -## Customized the Routing Convention -It is possible to fully customize routing behaviour by registering a `IJsonApiRoutingConvention` implementation. +## Customizing the Routing Convention +It is possible to fully customize routing behavior by registering a `IJsonApiRoutingConvention` implementation. ```c# // Startup.cs public void ConfigureServices(IServiceCollection services) @@ -47,18 +47,6 @@ public void ConfigureServices(IServiceCollection services) } ``` -## Namespacing and Versioning URLs -You can add a namespace to all URLs by specifying it in ConfigureServices - -```c# -public void ConfigureServices(IServiceCollection services) -{ - services.AddJsonApi( - options => options.Namespace = "api/v1"); -} -``` -Which results in URLs like: https://yourdomain.com/api/v1/people - ## Disabling the Default Routing Convention It is possible to completely bypass the default routing convention for a particular controller and specify a custom routing template by using the `DisableRoutingConvention` attribute. In the following example, the `CamelCasedModel` resource can be accessed on `/my-custom-route`. diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 25ecffa2b8..dad2518245 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample { diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs index 630734d693..d808b83043 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs index 7a0a27163e..9e2b04b6ee 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs @@ -92,7 +92,6 @@ public async Task NonJsonApiController_Skips_Middleware_And_Formatters_On_Delete // Act var response = await client.SendAsync(request); - var test = await response.Content.ReadAsStringAsync(); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString()); From 21dfc9b9b256d3b1e87d780564f24aa5303e7f72 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 09:16:20 +0200 Subject: [PATCH 32/51] docs: simplify middleware extensibility docs --- docs/usage/extensibility/middleware.md | 93 ++++++++------------------ 1 file changed, 27 insertions(+), 66 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 4b28dcfecb..7c46513fb1 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -1,84 +1,45 @@ # Middleware -The following is the default configuration of JsonApiDotNetCore: -1. Call one of the `AddJsonApi( ... )` overloads in the ` Startup.ConfigureServices` method. In this example uses a `DbContext` to build the resource graph +It is possible to execute your own middleware before or after `JsonApiMiddleware` by registering it accordingly. ```c# -services.AddJsonApi(); -``` - -2. In the Startup.Configure method, configure your application to use routing, to add the JsonApiMiddleware and to configure endpoint routing. - -```c# -app.UseRouting(); +/// In Startup.Configure +app.UseMiddleware(); app.UseJsonApi(); -app.UseEndpoints(endpoints => endpoints.MapControllers()); +app.UseMiddleware(); ``` -The following middleware components, in respective order, are registered: - -Filters: -- `IJsonApiExceptionFilter` -- `IJsonApiTypeMatchFilter` -- `IQueryStringActionFilter` -- `IConvertEmptyActionResultFilter` - -Formatters: -- `IJsonApiInputFormatter` -- `IJsonApiOutputFormatter` - -Routing convention: -- `IJsonApiRoutingConvention` - -Middleware: -- `JsonApiMiddleware` - -All of these components (except for `JsonApiMiddleware`) can be customized by registering your own implementation of these services. For example: - +It is also possible to replace any other JsonApiDotNetCore middleware component. The following example replaces the internal exception filter with a custom implementation ```c# -services.AddSingleton(); +/// In Startup.ConfigureServices +services.AddService() ``` -It is also possible to directly access the .NET Core `MvcOptions` object and have full control over which components are registered. +Alternatively, you can add additional middleware components by configuring `MvcOptions` directly. ## Configuring MvcOptions -JsonApiDotNetCore internally configures `MvcOptions` when calling `AddJsonApi( ... )`. However, it is still possible to register a custom configuration callback. To achieve this it is recommended to register this callback *after* the `AddJsonApi( ... )` call. It is also possible to do it earlier, but your configuration might be overwritten by the JsonApiDotNetCore configuration. +JsonApiDotNetCore configures `MvcOptions` internally when calling `AddJsonApi()`. Additionaly, it is possible to perform a custom configuration of `MvcOptions`. To prevent the library from overwriting your configuration, it is recommended to configure it *after* the library is done configuring `MvcOptions`. -The following example replaces all internally registered filters by retrieving a custom filter from the DI container. +The following example demonstrates this by clearing all internal filters and registering a custom one. ```c# -public class Startup +/// In Startup.ConfigureServices +services.AddSingleton(); +var builder = services.AddMvcCore(); +services.AddJsonApi(mvcBuilder: builder); +// Ensure the configuration action is registered after the `AddJsonApiCall`. +builder.AddMvcOptions( mvcOptions => { - private Action _postConfigureMvcOptions; - - public void ConfigureServices(IServiceCollection services) - { - ... - - var builder = services.AddMvcCore(); - services.AddJsonApi( ... , mvcBuilder: builder); - mvcCoreBuilder.AddMvcOptions(mvcOptions => - { - // Execute the mvcOptions configuration callback after the JsonApiDotNetCore callback as been executed. - _postConfigureMvcOptions?.Invoke(mvcOptions); - }); + // Execute the MvcOptions configuration callback after the JsonApiDotNetCore callback as been executed. + _postConfigureMvcOptions?.Invoke(mvcOptions); +}); - ... - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment environment) - { - - ... - - // Using a callback, we can defer to later (when service collection has become available). - _postConfigureMvcOptions = mvcOptions => - { - mvcOptions.Filters.Clear(); - mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); - }; - - ... - } -} +/// In Startup.Configure +app.UseJsonApi(); +// Ensure the configuration callback is set after calling `UseJsonApi()`. +_postConfigureMvcOptions = mvcOptions => +{ + mvcOptions.Filters.Clear(); + mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); +}; ``` From 4262410d69da33074c6e30054d313a26be253e89 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 3 Sep 2020 10:33:24 +0200 Subject: [PATCH 33/51] Changed filters and formatters to async --- .../JsonApiApplicationBuilder.cs | 20 +++---- .../AsyncConvertEmptyActionResultFilter.cs | 32 +++++++++++ .../Middleware/AsyncJsonApiExceptionFilter.cs | 36 +++++++++++++ .../AsyncQueryStringActionFilter.cs | 35 ++++++++++++ .../AsyncResourceTypeMatchFilter.cs | 51 ++++++++++++++++++ .../ConvertEmptyActionResultFilter.cs | 35 ------------ ...> IAsyncConvertEmptyActionResultFilter.cs} | 2 +- .../IAsyncJsonApiExceptionFilter.cs | 9 ++++ .../IAsyncQueryStringActionFilter.cs | 9 ++++ .../IAsyncResourceTypeMatchFilter.cs | 9 ++++ .../Middleware/IJsonApiExceptionFilter.cs | 9 ---- .../Middleware/IJsonApiInputFormatter.cs | 3 ++ .../Middleware/IJsonApiOutputFormatter.cs | 3 ++ .../Middleware/IJsonApiTypeMatchFilter.cs | 9 ---- .../Middleware/IQueryStringActionFilter.cs | 9 ---- .../Middleware/JsonApiExceptionFilter.cs | 39 -------------- .../Middleware/JsonApiInputFormatter.cs | 6 +-- .../Middleware/JsonApiOutputFormatter.cs | 6 +-- .../Middleware/JsonApiTypeMatchFilter.cs | 53 ------------------- .../Middleware/QueryStringActionFilter.cs | 36 ------------- 20 files changed, 204 insertions(+), 207 deletions(-) create mode 100644 src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs create mode 100644 src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs create mode 100644 src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs create mode 100644 src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs rename src/JsonApiDotNetCore/Middleware/{IConvertEmptyActionResultFilter.cs => IAsyncConvertEmptyActionResultFilter.cs} (82%) create mode 100644 src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs create mode 100644 src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs create mode 100644 src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs delete mode 100644 src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index c8998713b9..35e2d8ea78 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -79,18 +79,18 @@ public void AddResourceGraph(Type dbContextType, Action co /// /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: - /// , , , - /// and . + /// , , , + /// and . /// public void ConfigureMvc() { _mvcBuilder.AddMvcOptions(options => { options.EnableEndpointRouting = true; - options.Filters.AddService(); - options.Filters.AddService(); - options.Filters.AddService(); - options.Filters.AddService(); + options.Filters.AddService(); + options.Filters.AddService(); + options.Filters.AddService(); + options.Filters.AddService(); ConfigureMvcOptions?.Invoke(options); }); @@ -154,10 +154,10 @@ private void AddMiddlewareLayer() _services.AddSingleton(_options); _services.AddSingleton(this); _services.TryAddSingleton(); - _services.TryAddScoped(); - _services.TryAddScoped(); - _services.TryAddScoped(); - _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddScoped(); + _services.TryAddScoped(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); diff --git a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs new file mode 100644 index 0000000000..d7c78a94a8 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public sealed class AsyncConvertEmptyActionResultFilter : IAsyncConvertEmptyActionResultFilter + { + /// + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (next == null) throw new ArgumentNullException(nameof(next)); + + if (context.HttpContext.IsJsonApiRequest()) + { + if (!(context.Result is ObjectResult objectResult) || objectResult.Value == null) + { + if (context.Result is IStatusCodeActionResult statusCodeResult) + { + context.Result = new ObjectResult(null) {StatusCode = statusCodeResult.StatusCode}; + } + } + } + + await next(); + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs new file mode 100644 index 0000000000..0334f3a9eb --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public class AsyncJsonApiExceptionFilter : IAsyncJsonApiExceptionFilter + { + private readonly IExceptionHandler _exceptionHandler; + + public AsyncJsonApiExceptionFilter(IExceptionHandler exceptionHandler) + { + _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); + } + + /// + public Task OnExceptionAsync(ExceptionContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + + if (context.HttpContext.IsJsonApiRequest()) + { + var errorDocument = _exceptionHandler.HandleException(context.Exception); + + context.Result = new ObjectResult(errorDocument) + { + StatusCode = (int) errorDocument.GetErrorStatusCode() + }; + } + + return Task.CompletedTask; + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs new file mode 100644 index 0000000000..4bb9f88c32 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using JsonApiDotNetCore.Controllers.Annotations; +using JsonApiDotNetCore.QueryStrings; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public sealed class AsyncQueryStringActionFilter : IAsyncQueryStringActionFilter + { + private readonly IQueryStringReader _queryStringReader; + + public AsyncQueryStringActionFilter(IQueryStringReader queryStringReader) + { + _queryStringReader = queryStringReader ?? throw new ArgumentNullException(nameof(queryStringReader)); + } + + /// + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (next == null) throw new ArgumentNullException(nameof(next)); + + if (context.HttpContext.IsJsonApiRequest()) + { + var disableQueryStringAttribute = context.Controller.GetType().GetCustomAttribute(); + _queryStringReader.ReadAll(disableQueryStringAttribute); + } + + await next(); + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs new file mode 100644 index 0000000000..b42b0ef087 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/AsyncResourceTypeMatchFilter.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Errors; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + public sealed class AsyncResourceTypeMatchFilter : IAsyncResourceTypeMatchFilter + { + private readonly IResourceContextProvider _provider; + + public AsyncResourceTypeMatchFilter(IResourceContextProvider provider) + { + _provider = provider; + } + + /// + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (next == null) throw new ArgumentNullException(nameof(next)); + + if (context.HttpContext.IsJsonApiRequest() && IsPatchOrPostRequest(context.HttpContext.Request)) + { + var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); + var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; + + if (deserializedType != null && targetType != null && deserializedType != targetType) + { + var resourceFromEndpoint = _provider.GetResourceContext(targetType); + var resourceFromBody = _provider.GetResourceContext(deserializedType); + + throw new ResourceTypeMismatchException(new HttpMethod(context.HttpContext.Request.Method), context.HttpContext.Request.Path, + resourceFromEndpoint, resourceFromBody); + } + } + + await next(); + } + + private static bool IsPatchOrPostRequest(HttpRequest request) + { + return request.Method == "PATCH" || request.Method == "POST"; + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs deleted file mode 100644 index 57e2bfc316..0000000000 --- a/src/JsonApiDotNetCore/Middleware/ConvertEmptyActionResultFilter.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Infrastructure; - -namespace JsonApiDotNetCore.Middleware -{ - /// - public sealed class ConvertEmptyActionResultFilter : IConvertEmptyActionResultFilter - { - public void OnResultExecuted(ResultExecutedContext context) { /* noop */ } - - public void OnResultExecuting(ResultExecutingContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.HttpContext.IsJsonApiRequest()) - { - return; - } - - switch (context.Result) - { - case ObjectResult objectResult when objectResult.Value != null: - return; - case IStatusCodeActionResult statusCodeResult: - context.Result = new ObjectResult(null) { StatusCode = statusCodeResult.StatusCode }; - break; - } - } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs similarity index 82% rename from src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs rename to src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs index 9c7b621410..01466cf05e 100644 --- a/src/JsonApiDotNetCore/Middleware/IConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs @@ -8,5 +8,5 @@ namespace JsonApiDotNetCore.Middleware /// This ensures our formatter is invoked, where we'll build a json:api compliant response. /// For details, see: https://github.com/dotnet/aspnetcore/issues/16969 /// - public interface IConvertEmptyActionResultFilter : IAlwaysRunResultFilter { } + public interface IAsyncConvertEmptyActionResultFilter : IAsyncAlwaysRunResultFilter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs new file mode 100644 index 0000000000..dc78f44c4e --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Application-wide exception filter that invokes for json:api requests. + /// + public interface IAsyncJsonApiExceptionFilter : IAsyncExceptionFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs new file mode 100644 index 0000000000..ecc94db010 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Application-wide entry point for processing json:api request query strings. + /// + public interface IAsyncQueryStringActionFilter : IAsyncActionFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs new file mode 100644 index 0000000000..405c7b30a1 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IAsyncResourceTypeMatchFilter.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + /// + /// Verifies the incoming resource type in json:api request body matches the resource type at the current endpoint URL. + /// + public interface IAsyncResourceTypeMatchFilter : IAsyncActionFilter { } +} diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs deleted file mode 100644 index 56143d4dee..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiExceptionFilter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Global exception filter that wraps any thrown error with a JsonApiException. - /// - public interface IJsonApiExceptionFilter : IExceptionFilter { } -} diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs index a984eb9441..73027c3d2a 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs @@ -2,5 +2,8 @@ namespace JsonApiDotNetCore.Middleware { + /// + /// Application-wide entry point for reading json:api request bodies. + /// public interface IJsonApiInputFormatter : IInputFormatter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs index e30eca1aa5..0f2ed7c65d 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs @@ -2,5 +2,8 @@ namespace JsonApiDotNetCore.Middleware { + /// + /// Application-wide entry point for writing json:api response bodies. + /// public interface IJsonApiOutputFormatter : IOutputFormatter { } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs deleted file mode 100644 index 3ba1fa2641..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiTypeMatchFilter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Action filter used to verify the incoming type matches the target type, else return a 409. - /// - public interface IJsonApiTypeMatchFilter : IActionFilter { } -} diff --git a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs deleted file mode 100644 index 463150c44c..0000000000 --- a/src/JsonApiDotNetCore/Middleware/IQueryStringActionFilter.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Extensibility point for processing request query strings. - /// - public interface IQueryStringActionFilter : IActionFilter { } -} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs deleted file mode 100644 index a77bcb800c..0000000000 --- a/src/JsonApiDotNetCore/Middleware/JsonApiExceptionFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - /// - /// Global exception filter that wraps any thrown error with a JsonApiException. - /// - public class JsonApiExceptionFilter : ActionFilterAttribute, IJsonApiExceptionFilter - { - private readonly IExceptionHandler _exceptionHandler; - - public JsonApiExceptionFilter(IExceptionHandler exceptionHandler) - { - _exceptionHandler = exceptionHandler ?? throw new ArgumentNullException(nameof(exceptionHandler)); - } - - public void OnException(ExceptionContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.HttpContext.IsJsonApiRequest()) - { - return; - } - - var errorDocument = _exceptionHandler.HandleException(context.Exception); - - context.Result = new ObjectResult(errorDocument) - { - StatusCode = (int) errorDocument.GetErrorStatusCode() - }; - } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs index dacb80ba0c..044431b8a5 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiInputFormatter.cs @@ -6,11 +6,10 @@ namespace JsonApiDotNetCore.Middleware { - /// - /// Extensibility point for reading incoming HTTP request. - /// + /// public sealed class JsonApiInputFormatter : IJsonApiInputFormatter { + /// public bool CanRead(InputFormatterContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); @@ -18,6 +17,7 @@ public bool CanRead(InputFormatterContext context) return context.HttpContext.IsJsonApiRequest(); } + /// public async Task ReadAsync(InputFormatterContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 71fbb94de3..8562f82db7 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -6,11 +6,10 @@ namespace JsonApiDotNetCore.Middleware { - /// - /// Extensibility point for writing outgoing HTTP response. - /// + /// public sealed class JsonApiOutputFormatter : IJsonApiOutputFormatter { + /// public bool CanWriteResult(OutputFormatterCanWriteContext context) { if (context == null) @@ -21,6 +20,7 @@ public bool CanWriteResult(OutputFormatterCanWriteContext context) return context.HttpContext.IsJsonApiRequest(); } + /// public async Task WriteAsync(OutputFormatterWriteContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs deleted file mode 100644 index 0cd505e7a0..0000000000 --- a/src/JsonApiDotNetCore/Middleware/JsonApiTypeMatchFilter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Errors; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - public sealed class JsonApiTypeMatchFilter : IJsonApiTypeMatchFilter - { - private readonly IResourceContextProvider _provider; - - public JsonApiTypeMatchFilter(IResourceContextProvider provider) - { - _provider = provider; - } - - public void OnActionExecuting(ActionExecutingContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.HttpContext.IsJsonApiRequest()) - { - return; - } - - var request = context.HttpContext.Request; - if (request.Method != "PATCH" && request.Method != "POST") - { - return; - } - - var deserializedType = context.ActionArguments.FirstOrDefault().Value?.GetType(); - var targetType = context.ActionDescriptor.Parameters.FirstOrDefault()?.ParameterType; - - if (deserializedType == null || targetType == null || deserializedType == targetType) - { - return; - } - - var resourceFromEndpoint = _provider.GetResourceContext(targetType); - var resourceFromBody = _provider.GetResourceContext(deserializedType); - - throw new ResourceTypeMismatchException(new HttpMethod(request.Method), request.Path, resourceFromEndpoint, resourceFromBody); - } - - public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs deleted file mode 100644 index fa860c4e00..0000000000 --- a/src/JsonApiDotNetCore/Middleware/QueryStringActionFilter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Reflection; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.QueryStrings; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace JsonApiDotNetCore.Middleware -{ - public sealed class QueryStringActionFilter : IQueryStringActionFilter - { - private readonly IQueryStringReader _queryStringReader; - - public QueryStringActionFilter(IQueryStringReader queryStringReader) - { - _queryStringReader = queryStringReader ?? throw new ArgumentNullException(nameof(queryStringReader)); - } - - public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } - - public void OnActionExecuting(ActionExecutingContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!context.HttpContext.IsJsonApiRequest()) - { - return; - } - - var disableQueryStringAttribute = context.Controller.GetType().GetCustomAttribute(); - _queryStringReader.ReadAll(disableQueryStringAttribute); - } - } -} From e9e24ebba8970be606f3f50655f5d2536962fb14 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 10:53:38 +0200 Subject: [PATCH 34/51] docs: simpify routing docs --- docs/usage/routing.md | 48 +++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 86a10156bc..e44da49aad 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -1,7 +1,7 @@ # Routing ## Namespacing and Versioning URLs -You can add a namespace to all URLs by specifying it in ConfigureServices +You can add a namespace to all URLs by specifying it in ConfigureServices. ```c# public void ConfigureServices(IServiceCollection services) @@ -14,44 +14,44 @@ Which results in URLs like: https://yourdomain.com/api/v1/people ## Customizing Routes -The library will configure routes for each controller. By default, based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased. +The library will configure routes for all controllers in your project. -```http -GET /api/compoundModels HTTP/1.1 -``` +### Json:api endpoints -1. **Using the public name of the resource associated to a controller**. +By default, for json:api controllers, +- routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec. +- the route of a controller will match the public name of the resource that is associated it. This means that routes can be customized by [configuring the public name of the associated resource](~/usage/resource-graph.md#public-resource-name). ```c# public class MyResourceController : JsonApiController { /* .... */ } ``` -Note that the route -- is `/myApiResources`, which matches the public resouce name in the json:api payload (`{ "type": "myApiResources", ... }`) -- can be configured by configuring the public resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how to do that. +The route for this example will be `/myApiResources`, which will match the type in the json:api payload: `{ "type": "myApiResources", ... }`. + +### Non-json:api endpoints -2. **Using the controller name**. -If a controller does not have an associated resource, the name of the controller will be used following the configured naming strategy. +If a controller does not have an associated resource, the [configured naming strategy](./options#custom-serializer-settings) will be applied to the name of the controller. ```c# public class MyResourceController : ControllerBase { /* .... */ } ``` -Note that the route is `myResources` can be changed by renaming the controller. This approach is the default .NET Core MVC routing approach. +The route for this example is `/myResources`, which can be changed by renaming the controller. -## Customizing the Routing Convention -It is possible to fully customize routing behavior by registering a `IJsonApiRoutingConvention` implementation. -```c# -// Startup.cs -public void ConfigureServices(IServiceCollection services) -{ - services.AddSingleton(); -} -``` ## Disabling the Default Routing Convention -It is possible to completely bypass the default routing convention for a particular controller and specify a custom routing template by using the `DisableRoutingConvention` attribute. -In the following example, the `CamelCasedModel` resource can be accessed on `/my-custom-route`. +It is possible to by-pass the default routing convention for a controller by combining the `Route` and `DisableRoutingConvention`attributes. Any usage of `Route` without `DisableRoutingConvention` is ignored. ```c# -[Route("my-custom-route"), DisableRoutingConvention] +[Route("v1/camelCasedModels"), DisableRoutingConvention] public class MyCustomResourceController : JsonApiController { /* ... */ } ``` +This example exposes a versioned `CamelCasedModel` endpoint. To ensure guarantee valid link building, it is *highly recommended* to match your custom url with the public name of the associated resource. + +## Advanced Usage: Custom Routing Convention. + +It is possible to use a [custom routing convention](add-link) by registering a custom `IJsonApiRoutingConvention` implementation. This is generally not recommended and for advanced usage only. +```c# +public void ConfigureServices(IServiceCollection services) +{ + services.AddSingleton(); +} +``` \ No newline at end of file From 5c055030be7c15ea109dd46c45c3998b5dffe33d Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:04:56 +0200 Subject: [PATCH 35/51] chore: improve diff readability --- .../Startups/EmptyStartup.cs | 12 +++-- .../Startups/TestStartup.cs | 4 +- .../Configuration/InverseRelationships.cs | 13 ++---- .../Configuration/ResourceGraph.cs | 10 +---- .../Configuration/ResourceGraphBuilder.cs | 44 ++++++++++++------- .../Configuration/ServiceDiscoveryFacade.cs | 4 +- 6 files changed, 46 insertions(+), 41 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index dad2518245..18d40e9ccd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -11,10 +11,16 @@ namespace JsonApiDotNetCoreExample /// public abstract class EmptyStartup { - protected EmptyStartup(IConfiguration configuration) { } + protected EmptyStartup(IConfiguration configuration) + { + } - public virtual void ConfigureServices(IServiceCollection services) { } + public virtual void ConfigureServices(IServiceCollection services) + { + } - public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { } + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) + { + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs index d808b83043..e1af39084c 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs @@ -7,7 +7,9 @@ namespace JsonApiDotNetCoreExample { public class TestStartup : Startup { - public TestStartup(IConfiguration configuration) : base(configuration) { } + public TestStartup(IConfiguration configuration) : base(configuration) + { + } protected override void ConfigureClock(IServiceCollection services) { diff --git a/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs b/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs index cc8b25dd97..5c217373dd 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseRelationships.cs @@ -28,20 +28,13 @@ public void Resolve() foreach (ResourceContext ce in _provider.GetResourceContexts()) { IEntityType meta = context.Model.FindEntityType(ce.ResourceType); - if (meta == null) - { - continue; - } - + if (meta == null) continue; foreach (var attr in ce.Relationships) { - if (attr is HasManyThroughAttribute) - { - continue; - } + if (attr is HasManyThroughAttribute) continue; INavigation inverseNavigation = meta.FindNavigation(attr.Property.Name)?.FindInverse(); attr.InverseNavigation = inverseNavigation?.Name; - } + } } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 78ae0e65af..f8367af7f5 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -24,10 +24,7 @@ public ResourceGraph(IReadOnlyCollection resources) /// public ResourceContext GetResourceContext(string resourceName) { - if (resourceName == null) - { - throw new ArgumentNullException(nameof(resourceName)); - } + if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); return _resources.SingleOrDefault(e => e.ResourceName == resourceName); } @@ -35,10 +32,7 @@ public ResourceContext GetResourceContext(string resourceName) /// public ResourceContext GetResourceContext(Type resourceType) { - if (resourceType == null) - { - throw new ArgumentNullException(nameof(resourceType)); - } + if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); return IsLazyLoadingProxyForResourceType(resourceType) ? _resources.SingleOrDefault(e => e.ResourceType == resourceType.BaseType) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index a11aa53553..0994edcc0c 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -9,6 +9,9 @@ namespace JsonApiDotNetCore.Configuration { + /// + /// Builds and configures the . + /// public class ResourceGraphBuilder { private readonly ILogger _logger; @@ -17,11 +20,8 @@ public class ResourceGraphBuilder public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } - + if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); + _logger = loggerFactory.CreateLogger(); _options = options ?? throw new ArgumentNullException(nameof(options)); } @@ -45,29 +45,39 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) resourceContext.TopLevelLinks = attribute.TopLevelLinks; } } - + /// - /// Adds a json:api resource to the resource graph. + /// Adds a json:api resource. /// - /// The resource type. - /// The identifier type of the resource + /// The resource model type. /// - /// The name under which the resource is publicly exposed by the API. + /// The pluralized name, under which the resource is publicly exposed by the API. /// If nothing is specified, the configured casing convention formatter will be applied. /// - public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable - => Add(typeof(TResource), typeof(TId), publicName); + public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable + => Add(publicName); /// - /// + /// Adds a json:api resource. /// - public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable - => Add(publicName); + /// The resource model type. + /// The resource model identifier type. + /// + /// The pluralized name, under which the resource is publicly exposed by the API. + /// If nothing is specified, the configured casing convention formatter will be applied. + /// + public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable + => Add(typeof(TResource), typeof(TId), publicName); - /// - /// + /// Adds a json:api resource. /// + /// The resource model type. + /// The resource model identifier type. + /// + /// The pluralized name, under which the resource is publicly exposed by the API. + /// If nothing is specified, the configured casing convention formatter will be applied. + /// public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) { if (resourceType == null) throw new ArgumentNullException(nameof(resourceType)); diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 1c0cdd4602..23c6092d23 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -68,12 +68,12 @@ public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder } /// - /// Scan the calling assembly. + /// Mark the calling assembly for scanning of resources and injectables. /// public ServiceDiscoveryFacade AddCurrentAssembly() => AddAssembly(Assembly.GetCallingAssembly()); /// - /// Scan the specified assembly. + /// Mark the specified assembly for scanning of resources and injectables. /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { From 685eef8d4a64aa122c8330c6519c88a16f180763 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:21:41 +0200 Subject: [PATCH 36/51] chore: diff readability, fixes --- .../ApplicationBuilderExtensions.cs | 3 +++ .../Configuration/ResourceContext.cs | 4 ++-- .../Configuration/ResourceGraph.cs | 2 +- .../Configuration/ResourceGraphBuilder.cs | 6 ++--- .../ServiceCollectionExtensions.cs | 8 ++----- .../Configuration/ServiceDiscoveryFacade.cs | 4 ++-- .../Errors/ResourceTypeMismatchException.cs | 2 +- .../Middleware/JsonApiMiddleware.cs | 2 +- .../Middleware/JsonApiRoutingConvention.cs | 2 +- .../Parsing/ResourceFieldChainResolver.cs | 24 +++++++++---------- .../IncludeQueryStringParameterReader.cs | 4 ++-- .../Building/IncludedResourceObjectBuilder.cs | 2 +- .../Serialization/Building/LinkBuilder.cs | 8 +++---- .../Building/ResourceObjectBuilder.cs | 4 ++-- .../Services/JsonApiResourceService.cs | 4 ++-- .../Spec/FunctionalTestCollection.cs | 2 +- .../Builders/ContextGraphBuilder_Tests.cs | 2 +- test/UnitTests/Builders/LinkBuilderTests.cs | 2 +- .../IServiceCollectionExtensionsTests.cs | 4 ++-- .../Middleware/JsonApiMiddlewareTests.cs | 4 ++-- 20 files changed, 46 insertions(+), 47 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs index 83902c054f..0d4d20f59c 100644 --- a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using System; using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -21,6 +22,8 @@ public static class ApplicationBuilderExtensions /// public static void UseJsonApi(this IApplicationBuilder builder) { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + using var scope = builder.ApplicationServices.GetRequiredService().CreateScope(); var inverseRelationshipResolver = scope.ServiceProvider.GetRequiredService(); inverseRelationshipResolver.Resolve(); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index eb5af30a56..b1c4803b30 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -14,7 +14,7 @@ public class ResourceContext /// /// The publicly exposed resource name. /// - public string ResourceName { get; set; } + public string PublicName { get; set; } /// /// The CLR type of the resource. @@ -84,7 +84,7 @@ public class ResourceContext public override string ToString() { - return ResourceName; + return PublicName; } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index f8367af7f5..a1acc938d8 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -26,7 +26,7 @@ public ResourceContext GetResourceContext(string resourceName) { if (resourceName == null) throw new ArgumentNullException(nameof(resourceName)); - return _resources.SingleOrDefault(e => e.ResourceName == resourceName); + return _resources.SingleOrDefault(e => e.PublicName == resourceName); } /// diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 0994edcc0c..9e7922618b 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -14,16 +14,16 @@ namespace JsonApiDotNetCore.Configuration /// public class ResourceGraphBuilder { - private readonly ILogger _logger; private readonly IJsonApiOptions _options; + private readonly ILogger _logger; private readonly List _resources = new List(); public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactory) { if (loggerFactory == null) throw new ArgumentNullException(nameof(loggerFactory)); - _logger = loggerFactory.CreateLogger(); _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = loggerFactory.CreateLogger(); } /// @@ -102,7 +102,7 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu private ResourceContext CreateResourceContext(string publicName, Type resourceType, Type idType) => new ResourceContext { - ResourceName = publicName, + PublicName = publicName, ResourceType = resourceType, IdentityType = idType, Attributes = GetAttributes(resourceType), diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 5523bf7824..8dcce69777 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -39,10 +39,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection IMvcCoreBuilder mvcBuilder = null) where TDbContext : DbContext { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + if (services == null) throw new ArgumentNullException(nameof(services)); SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); @@ -53,7 +50,7 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< Action configureAutoDiscovery, Action configureResourceGraph, IMvcCoreBuilder mvcBuilder, Type dbContextType) { - var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); + using var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); applicationBuilder.ConfigureJsonApiOptions(configureOptions); applicationBuilder.ConfigureAutoDiscovery(configureAutoDiscovery); @@ -61,7 +58,6 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< applicationBuilder.ConfigureMvc(); applicationBuilder.DiscoverInjectables(); applicationBuilder.ConfigureServices(dbContextType); - applicationBuilder.Dispose(); } /// diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index 23c6092d23..f16e52377e 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -49,7 +49,7 @@ public class ServiceDiscoveryFacade typeof(IResourceReadRepository<,>) }; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServiceCollection _services; private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly IdentifiableTypeCache _typeCache = new IdentifiableTypeCache(); @@ -62,7 +62,7 @@ public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder throw new ArgumentNullException(nameof(loggerFactory)); } - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _services = services ?? throw new ArgumentNullException(nameof(services)); _resourceGraphBuilder = resourceGraphBuilder ?? throw new ArgumentNullException(nameof(resourceGraphBuilder)); } diff --git a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs index 42159c9214..5edec07bb3 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs @@ -14,7 +14,7 @@ public ResourceTypeMismatchException(HttpMethod method, string requestPath, Reso : base(new Error(HttpStatusCode.Conflict) { Title = "Resource type mismatch between request body and endpoint URL.", - Detail = $"Expected resource of type '{expected.ResourceName}' in {method} request body at endpoint '{requestPath}', instead of '{actual?.ResourceName}'." + Detail = $"Expected resource of type '{expected.PublicName}' in {method} request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." }) { } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 853386a7e8..3cfca33693 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -165,7 +165,7 @@ private static void SetupRequest(JsonApiRequest request, ResourceContext primary request.Kind = EndpointKind.Primary; request.PrimaryResource = primaryResourceContext; request.PrimaryId = GetPrimaryRequestId(routeValues); - request.BasePath = GetBasePath(primaryResourceContext.ResourceName, options, httpRequest); + request.BasePath = GetBasePath(primaryResourceContext.PublicName, options, httpRequest); var relationshipName = GetRelationshipNameForSecondaryRequest(routeValues); if (relationshipName != null) diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index a6d3a84fd3..5e927f3036 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -114,7 +114,7 @@ private string TemplateFromResource(ControllerModel model) { if (_registeredResources.TryGetValue(model.ControllerName, out var resourceContext)) { - var template = $"{_options.Namespace}/{resourceContext.ResourceName}"; + var template = $"{_options.Namespace}/{resourceContext.PublicName}"; if (_registeredTemplates.Add(template)) { return template; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index a28236f0e1..863cc26b8c 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -180,8 +180,8 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr if (lastField is HasManyAttribute) { throw new QueryParseException(path == lastName - ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource '{resourceContext.ResourceName}'." - : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource '{resourceContext.ResourceName}'."); + ? $"Field '{lastName}' must be an attribute or a to-one relationship on resource '{resourceContext.PublicName}'." + : $"Field '{lastName}' in '{path}' must be an attribute or a to-one relationship on resource '{resourceContext.PublicName}'."); } validateCallback?.Invoke(lastField, resourceContext, path); @@ -197,8 +197,8 @@ public RelationshipAttribute GetRelationship(string publicName, ResourceContext if (relationship == null) { throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' does not exist on resource '{resourceContext.ResourceName}'." - : $"Relationship '{publicName}' in '{path}' does not exist on resource '{resourceContext.ResourceName}'."); + ? $"Relationship '{publicName}' does not exist on resource '{resourceContext.PublicName}'." + : $"Relationship '{publicName}' in '{path}' does not exist on resource '{resourceContext.PublicName}'."); } return relationship; @@ -211,8 +211,8 @@ public RelationshipAttribute GetToManyRelationship(string publicName, ResourceCo if (!(relationship is HasManyAttribute)) { throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' must be a to-many relationship on resource '{resourceContext.ResourceName}'." - : $"Relationship '{publicName}' in '{path}' must be a to-many relationship on resource '{resourceContext.ResourceName}'."); + ? $"Relationship '{publicName}' must be a to-many relationship on resource '{resourceContext.PublicName}'." + : $"Relationship '{publicName}' in '{path}' must be a to-many relationship on resource '{resourceContext.PublicName}'."); } return relationship; @@ -225,8 +225,8 @@ public RelationshipAttribute GetToOneRelationship(string publicName, ResourceCon if (!(relationship is HasOneAttribute)) { throw new QueryParseException(path == publicName - ? $"Relationship '{publicName}' must be a to-one relationship on resource '{resourceContext.ResourceName}'." - : $"Relationship '{publicName}' in '{path}' must be a to-one relationship on resource '{resourceContext.ResourceName}'."); + ? $"Relationship '{publicName}' must be a to-one relationship on resource '{resourceContext.PublicName}'." + : $"Relationship '{publicName}' in '{path}' must be a to-one relationship on resource '{resourceContext.PublicName}'."); } return relationship; @@ -239,8 +239,8 @@ public AttrAttribute GetAttribute(string publicName, ResourceContext resourceCon if (attribute == null) { throw new QueryParseException(path == publicName - ? $"Attribute '{publicName}' does not exist on resource '{resourceContext.ResourceName}'." - : $"Attribute '{publicName}' in '{path}' does not exist on resource '{resourceContext.ResourceName}'."); + ? $"Attribute '{publicName}' does not exist on resource '{resourceContext.PublicName}'." + : $"Attribute '{publicName}' in '{path}' does not exist on resource '{resourceContext.PublicName}'."); } return attribute; @@ -253,8 +253,8 @@ public ResourceFieldAttribute GetField(string publicName, ResourceContext resour if (field == null) { throw new QueryParseException(path == publicName - ? $"Field '{publicName}' does not exist on resource '{resourceContext.ResourceName}'." - : $"Field '{publicName}' in '{path}' does not exist on resource '{resourceContext.ResourceName}'."); + ? $"Field '{publicName}' does not exist on resource '{resourceContext.PublicName}'." + : $"Field '{publicName}' in '{path}' does not exist on resource '{resourceContext.PublicName}'."); } return field; diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index c6d2767ad6..d79b24b223 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -34,8 +34,8 @@ protected void ValidateSingleRelationship(RelationshipAttribute relationship, Re throw new InvalidQueryStringParameterException(_lastParameterName, "Including the requested relationship is not allowed.", path == relationship.PublicName - ? $"Including the relationship '{relationship.PublicName}' on '{resourceContext.ResourceName}' is not allowed." - : $"Including the relationship '{relationship.PublicName}' in '{path}' on '{resourceContext.ResourceName}' is not allowed."); + ? $"Including the relationship '{relationship.PublicName}' on '{resourceContext.PublicName}' is not allowed." + : $"Including the relationship '{relationship.PublicName}' in '{path}' on '{resourceContext.PublicName}' is not allowed."); } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index fe25ee0550..8ff303c40c 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -122,7 +122,7 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r private ResourceObject GetOrBuildResourceObject(IIdentifiable parent, RelationshipAttribute relationship) { var type = parent.GetType(); - var resourceName = ResourceContextProvider.GetResourceContext(type).ResourceName; + var resourceName = ResourceContextProvider.GetResourceContext(type).PublicName; var entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId); if (entry == null) { diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index b3d58fe84c..02321695b8 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -93,7 +93,7 @@ private string GetSelfTopLevelLink(ResourceContext resourceContext) var builder = new StringBuilder(); builder.Append(_request.BasePath); builder.Append("/"); - builder.Append(resourceContext.ResourceName); + builder.Append(resourceContext.PublicName); string resourceId = _request.PrimaryId; if (resourceId != null) @@ -121,7 +121,7 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, Page parameters["page[number]"] = pageOffset.ToString(); }); - return $"{_request.BasePath}/{resourceContext.ResourceName}" + queryString; + return $"{_request.BasePath}/{resourceContext.PublicName}" + queryString; } private string BuildQueryString(Action> updateAction) @@ -164,13 +164,13 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship RelationshipLinks links = null; if (ShouldAddRelationshipLink(parentResourceContext, relationship, LinkTypes.Related)) { - links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation) }; + links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.PublicName, parent.StringId, childNavigation) }; } if (ShouldAddRelationshipLink(parentResourceContext, relationship, LinkTypes.Self)) { links ??= new RelationshipLinks(); - links.Self = GetSelfRelationshipLink(parentResourceContext.ResourceName, parent.StringId, childNavigation); + links.Self = GetSelfRelationshipLink(parentResourceContext.PublicName, parent.StringId, childNavigation); } return links; diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index 31c9f2555d..bb4453d64e 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -30,7 +30,7 @@ public ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attr.Property.Name != nameof(Identifiable.Id)).ToArray()).Any()) @@ -105,7 +105,7 @@ private List GetRelatedResourceLinkageForHasMany(HasMa /// private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) { - var resourceName = ResourceContextProvider.GetResourceContext(resource.GetType()).ResourceName; + var resourceName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; return new ResourceIdentifierObject { Type = resourceName, diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 2a101f22c9..41e61b8452 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -353,7 +353,7 @@ private void AssertPrimaryResourceExists(TResource resource) { if (resource == null) { - throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResource.ResourceName); + throw new ResourceNotFoundException(_request.PrimaryId, _request.PrimaryResource.PublicName); } } @@ -362,7 +362,7 @@ private void AssertRelationshipExists(string relationshipName) var relationship = _request.Relationship; if (relationship == null) { - throw new RelationshipNotFoundException(relationshipName, _request.PrimaryResource.ResourceName); + throw new RelationshipNotFoundException(relationshipName, _request.PrimaryResource.PublicName); } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs index 870a2af9fa..18fdf77398 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FunctionalTestCollection.cs @@ -74,7 +74,7 @@ protected IResponseDeserializer GetDeserializer() { continue; } - builder.Add(rc.ResourceType, rc.IdentityType, rc.ResourceName); + builder.Add(rc.ResourceType, rc.IdentityType, rc.PublicName); } builder.Add(formatter.FormatResourceName(typeof(TodoItem))); builder.Add(formatter.FormatResourceName(typeof(TodoItemCollection))); diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index 22c9b9e927..fb28bf4227 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -61,7 +61,7 @@ public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() // Assert var resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("testResources", resource.ResourceName); + Assert.Equal("testResources", resource.PublicName); } [Fact] diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs index 21f90d062b..8396a5c595 100644 --- a/test/UnitTests/Builders/LinkBuilderTests.cs +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -222,7 +222,7 @@ private ResourceContext GetArticleResourceContext(LinkTypes resourceLinks = Link ResourceLinks = resourceLinks, TopLevelLinks = topLevelLinks, RelationshipLinks = relationshipLinks, - ResourceName = "articles" + PublicName = "articles" }; } diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index c8dd510bf6..ccddd24f1f 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -76,7 +76,7 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() var resourceContext = graph.GetResourceContext(); // Assert - Assert.Equal("authors", resourceContext.ResourceName); + Assert.Equal("authors", resourceContext.PublicName); } [Fact] @@ -152,7 +152,7 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( var provider = services.BuildServiceProvider(); var resourceGraph = provider.GetService(); var resource = resourceGraph.GetResourceContext(typeof(IntResource)); - Assert.Equal("intResources", resource.ResourceName); + Assert.Equal("intResources", resource.PublicName); } public sealed class IntResource : Identifiable { } diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 75130f1b3f..5ab2fcd1e0 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -152,7 +152,7 @@ private Mock CreateMockResourceGraph( string resourceName, bool var mockGraph = new Mock(); var resourceContext = new ResourceContext { - ResourceName = resourceName, + PublicName = resourceName, IdentityType = typeof(string) }; var seq = mockGraph.SetupSequence(d => d.GetResourceContext(It.IsAny())).Returns(resourceContext); @@ -160,7 +160,7 @@ private Mock CreateMockResourceGraph( string resourceName, bool { var relResourceContext = new ResourceContext { - ResourceName = "todoItems", + PublicName = "todoItems", IdentityType = typeof(string) }; seq.Returns(relResourceContext); From 9f6a4f6d380cf3b6eeaaadcba15c2395cc5e2e8a Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:33:40 +0200 Subject: [PATCH 37/51] fix: more diff readability --- .../Middleware/JsonApiOutputFormatter.cs | 6 ++---- .../Middleware/JsonApiRoutingConvention.cs | 19 +++++++++---------- .../ServiceDiscoveryFacadeTests.cs | 15 ++++++++------- .../LinksWithoutNamespaceTests.cs | 4 +++- .../ClientGeneratedIdsApplicationFactory.cs | 3 ++- .../IntegrationTests/TestableStartup.cs | 4 +++- 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 8562f82db7..58fdb0d782 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -12,10 +12,8 @@ public sealed class JsonApiOutputFormatter : IJsonApiOutputFormatter /// public bool CanWriteResult(OutputFormatterCanWriteContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + if (context == null) throw new ArgumentNullException(nameof(context)); + return context.HttpContext.IsJsonApiRequest(); } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 5e927f3036..9d78b6a299 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -46,10 +46,7 @@ public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resource /// public Type GetAssociatedResource(string controllerName) { - if (controllerName == null) - { - throw new ArgumentNullException(nameof(controllerName)); - } + if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); if (_registeredResources.TryGetValue(controllerName, out var resourceContext)) { @@ -62,10 +59,7 @@ public Type GetAssociatedResource(string controllerName) /// public void Apply(ApplicationModel application) { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } + if (application == null) throw new ArgumentNullException(nameof(application)); foreach (var controller in application.Controllers) { @@ -129,12 +123,17 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - var controllerName = + string controllerName = _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); var template = $"{_options.Namespace}/{controllerName}"; - return _registeredTemplates.Add(template) ? template : null; + if (_registeredTemplates.Add(template)) + { + return template; + } + + return null; } /// diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 47ee4ad1f3..9386a4415d 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -51,15 +51,16 @@ public ServiceDiscoveryFacadeTests() public void AddAssembly_Adds_All_Resources_To_Graph() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); - facade.AddAssembly(typeof(Person).Assembly); - facade.DiscoverResources(); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); // Act + facade.AddAssembly(typeof(Person).Assembly); + facade.DiscoverResources(); var resourceGraph = _resourceGraphBuilder.Build(); var personResource = resourceGraph.GetResourceContext(typeof(Person)); var articleResource = resourceGraph.GetResourceContext(typeof(Article)); + // Assert Assert.NotNull(personResource); Assert.NotNull(articleResource); } @@ -68,7 +69,7 @@ public void AddAssembly_Adds_All_Resources_To_Graph() public void AddCurrentAssembly_Adds_Resources_To_Graph() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); // Act facade.AddCurrentAssembly(); @@ -84,7 +85,7 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() public void AddCurrentAssembly_Adds_Services_To_Container() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); // Act facade.AddCurrentAssembly(); @@ -100,7 +101,7 @@ public void AddCurrentAssembly_Adds_Services_To_Container() public void AddCurrentAssembly_Adds_Repositories_To_Container() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); // Act facade.AddCurrentAssembly(); @@ -115,7 +116,7 @@ public void AddCurrentAssembly_Adds_Repositories_To_Container() public void AddCurrentAssembly_Adds_ResourceDefinitions_To_Container() { // Arrange - ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, new NullLoggerFactory()); + ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); // Act facade.AddCurrentAssembly(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs index 03583c6986..9670d5003a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DocumentTests/LinksWithoutNamespaceTests.cs @@ -85,7 +85,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public sealed class NoNamespaceStartup : TestStartup { - public NoNamespaceStartup(IConfiguration configuration) : base(configuration) { } + public NoNamespaceStartup(IConfiguration configuration) : base(configuration) + { + } protected override void ConfigureJsonApiOptions(JsonApiOptions options) { diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs index 47057b8d10..1a428e20df 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/ClientGeneratedIdsApplicationFactory.cs @@ -24,7 +24,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) options.DefaultPageSize = new PageSize(5); options.IncludeTotalResourceCount = true; options.AllowClientGeneratedIds = true; - }, discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + }, + discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); }); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs index 58e5f8b7fa..ba110a7399 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/TestableStartup.cs @@ -13,7 +13,9 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests public sealed class TestableStartup : EmptyStartup where TDbContext : DbContext { - public TestableStartup(IConfiguration configuration) : base(configuration) { } + public TestableStartup(IConfiguration configuration) : base(configuration) + { + } public override void ConfigureServices(IServiceCollection services) { From 66964921e66999268a7ce17c3b3b08d65b2bf292 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:35:45 +0200 Subject: [PATCH 38/51] fix: nullcheck --- .../Configuration/ServiceDiscoveryFacade.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index f16e52377e..278f0a104d 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -77,6 +77,11 @@ public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder /// public ServiceDiscoveryFacade AddAssembly(Assembly assembly) { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + _resourceDescriptorsPerAssemblyCache.Add(assembly, null); _logger.LogDebug($"Registering assembly '{assembly.FullName}' for discovery of resources and injectables."); From 540bc8a8ef36b344ee4bce30f32729027a71a16f Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:48:13 +0200 Subject: [PATCH 39/51] docs: resource graph builder inaccuracy --- .../Configuration/ResourceGraphBuilder.cs | 8 ++++---- .../Acceptance/KebabCaseFormatterTests.cs | 4 +++- .../Factories/ClientGeneratedIdsApplicationFactory.cs | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 9e7922618b..0fd73bd204 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -47,11 +47,11 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) } /// - /// Adds a json:api resource. + /// Adds a json:api resource with int as the identifier type. /// /// The resource model type. /// - /// The pluralized name, under which the resource is publicly exposed by the API. + /// The name under which the resource is publicly exposed by the API. /// If nothing is specified, the configured casing convention formatter will be applied. /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable @@ -63,7 +63,7 @@ public ResourceGraphBuilder Add(string publicName = null) where TReso /// The resource model type. /// The resource model identifier type. /// - /// The pluralized name, under which the resource is publicly exposed by the API. + /// The name under which the resource is publicly exposed by the API. /// If nothing is specified, the configured casing convention formatter will be applied. /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable @@ -75,7 +75,7 @@ public ResourceGraphBuilder Add(string publicName = null) where /// The resource model type. /// The resource model identifier type. /// - /// The pluralized name, under which the resource is publicly exposed by the API. + /// The name under which the resource is publicly exposed by the API. /// If nothing is specified, the configured casing convention formatter will be applied. /// public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs index 0b44b55af2..3d0ff53b5d 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs @@ -181,7 +181,9 @@ private IRequestSerializer GetSerializer(Expression discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); + }, + discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample)))); }); } } From 4268be110ce205de9d2f35106162951bbb284823 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:52:55 +0200 Subject: [PATCH 40/51] fix: broken links markdown --- docs/usage/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index e44da49aad..e78e7ff0a9 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -20,7 +20,7 @@ The library will configure routes for all controllers in your project. By default, for json:api controllers, - routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec. -- the route of a controller will match the public name of the resource that is associated it. This means that routes can be customized by [configuring the public name of the associated resource](~/usage/resource-graph.md#public-resource-name). +- the route of a controller will match the public name of the resource that is associated it. This means that routes can be customized by [configuring the public name of the associated resource](./resource-graph.md#public-resource-name). ```c# public class MyResourceController : JsonApiController { /* .... */ } From 234e0ec51b68142fdd00654a66e6236677378963 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:53:42 +0200 Subject: [PATCH 41/51] fix: broken markdown links --- docs/usage/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index e78e7ff0a9..a672a48f21 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -30,7 +30,7 @@ The route for this example will be `/myApiResources`, which will match the type ### Non-json:api endpoints -If a controller does not have an associated resource, the [configured naming strategy](./options#custom-serializer-settings) will be applied to the name of the controller. +If a controller does not have an associated resource, the [configured naming strategy](./options.md#custom-serializer-settings) will be applied to the name of the controller. ```c# public class MyResourceController : ControllerBase { /* .... */ } ``` From 39cfddec51f829e9347301daa55ff2e0be88029b Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 13:56:12 +0200 Subject: [PATCH 42/51] fix: restore whitespace --- .../Acceptance/NonJsonApiControllerTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs index 9e2b04b6ee..397914ba7b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/NonJsonApiControllerTests.cs @@ -92,6 +92,7 @@ public async Task NonJsonApiController_Skips_Middleware_And_Formatters_On_Delete // Act var response = await client.SendAsync(request); + // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("text/plain; charset=utf-8", response.Content.Headers.ContentType.ToString()); From d60ced344c6a2762c0c31b78911f3cdbcd1142cc Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 3 Sep 2020 14:08:11 +0200 Subject: [PATCH 43/51] fix: diff --- src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs | 3 +-- .../Acceptance/KebabCaseFormatterTests.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs index 58fdb0d782..644e829538 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiOutputFormatter.cs @@ -13,8 +13,7 @@ public sealed class JsonApiOutputFormatter : IJsonApiOutputFormatter public bool CanWriteResult(OutputFormatterCanWriteContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); - - + return context.HttpContext.IsJsonApiRequest(); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs index 3d0ff53b5d..49748e4f54 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/KebabCaseFormatterTests.cs @@ -181,7 +181,7 @@ private IRequestSerializer GetSerializer(Expression Date: Thu, 3 Sep 2020 14:11:09 +0200 Subject: [PATCH 44/51] fix: whitespace --- .../Configuration/ServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index 8dcce69777..5b5b7a652a 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -44,7 +44,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, typeof(TDbContext)); return services; - } + } private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, Action configureAutoDiscovery, From 7c65d3817d5e1c3f6b2c7dd908528fd5150c84e0 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 9 Sep 2020 09:29:47 +0200 Subject: [PATCH 45/51] chore: review --- docs/usage/extensibility/middleware.md | 25 ++++------ docs/usage/resource-graph.md | 48 +++++++++---------- docs/usage/resources/attributes.md | 8 +--- .../JsonApiApplicationBuilder.cs | 12 ++--- 4 files changed, 37 insertions(+), 56 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 7c46513fb1..705695fd5a 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -1,34 +1,26 @@ # Middleware -It is possible to execute your own middleware before or after `JsonApiMiddleware` by registering it accordingly. +It is possible to replace JsonApiDotNetCore middleware components by configuring the IoC container and by configuring `MvcOptions`. -```c# -/// In Startup.Configure -app.UseMiddleware(); -app.UseJsonApi(); -app.UseMiddleware(); -``` +## Configuring the IoC container -It is also possible to replace any other JsonApiDotNetCore middleware component. The following example replaces the internal exception filter with a custom implementation +The following example replaces the internal exception filter with a custom implementation ```c# /// In Startup.ConfigureServices services.AddService() ``` -Alternatively, you can add additional middleware components by configuring `MvcOptions` directly. +## Configuring `MvcOptions` -## Configuring MvcOptions - -JsonApiDotNetCore configures `MvcOptions` internally when calling `AddJsonApi()`. Additionaly, it is possible to perform a custom configuration of `MvcOptions`. To prevent the library from overwriting your configuration, it is recommended to configure it *after* the library is done configuring `MvcOptions`. - -The following example demonstrates this by clearing all internal filters and registering a custom one. +To prevent the library from overwriting your configuration, perform your configuration *after* the library is done configuring `MvcOptions`. The following example replaces all internal filters with a custom filter. ```c# /// In Startup.ConfigureServices services.AddSingleton(); var builder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: builder); -// Ensure the configuration action is registered after the `AddJsonApiCall`. -builder.AddMvcOptions( mvcOptions => + +// Ensure the configuration action is registered after the `AddJsonApi()` call. +builder.AddMvcOptions(mvcOptions => { // Execute the MvcOptions configuration callback after the JsonApiDotNetCore callback as been executed. _postConfigureMvcOptions?.Invoke(mvcOptions); @@ -36,6 +28,7 @@ builder.AddMvcOptions( mvcOptions => /// In Startup.Configure app.UseJsonApi(); + // Ensure the configuration callback is set after calling `UseJsonApi()`. _postConfigureMvcOptions = mvcOptions => { diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 02ea82684e..311214ef9e 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -10,32 +10,37 @@ It is built at app startup and available as a singleton through Dependency Injec There are three ways the resource graph can be created: -1. Manually specifying each resource +1. Auto-discovery 2. Specifying an entire DbContext -3. Auto-discovery +3. Manually specifying each resource It is also possible to combine the three of them at once. Be aware that some configuration might overlap, -for example you could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration -is prioritized by the order of the list above. +for example one could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration +is prioritized by the list above in descending order. -### Manual Specification +### Auto-discovery -You can manually construct the graph. +Auto-discovery refers to the process of reflecting on an assembly and +detecting all of the json:api resources, resource definitions, resource services and repositories. + +The following command will build the resource graph using all `IIdentifiable` +implementations. It also injects resource definitions and service layer overrides which we will +cover in a later section. You can enable auto-discovery for the +current assembly by adding the following to your `Startup` class. ```c# // Startup.cs public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi(resources: builder => - { - builder.Add(); - }); + services.AddJsonApi( + options => { /* ... */ }, + discovery => discovery.AddCurrentAssembly()); } ``` ### Specifying an Entity Framework Core DbContext -If you are using Entity Framework Core as your ORM, you can add an entire `DbContext` with one line. +If you are using Entity Framework Core as your ORM, you can add all the models of a `DbContext` to the resource graph. ```c# // Startup.cs @@ -45,7 +50,7 @@ public void ConfigureServices(IServiceCollection services) } ``` -Be aware that the previous command does not inject resource definitions and service layer overrides. You can combine it with auto-discovery to register them. +Be aware that this does not register resource definitions, resource services and repositories. You can combine it with auto-discovery to achieve this. ```c# // Startup.cs @@ -57,27 +62,22 @@ public void ConfigureServices(IServiceCollection services) } ``` -### Auto-discovery - -Auto-discovery refers to the process of reflecting on an assembly and -detecting all of the json:api resources, resource definitions, resource services and repositories. +### Manual Specification -The following command will build the resource graph using all `IIdentifiable` -implementations. It also injects resource definitions and service layer overrides which we will -cover in a later section. You can enable auto-discovery for the -current assembly by adding the following to your `Startup` class. +You can manually construct the graph. ```c# // Startup.cs public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi( - options => { /* ... */ }, - discovery => discovery.AddCurrentAssembly()); + services.AddJsonApi(resources: builder => + { + builder.Add(); + }); } ``` -### Public Resource Name +## Public Resource Name The public resource name is exposed through the `type` member in the json:api payload. This can be configured by the following approaches (in order of priority): diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index dfbef4aefd..4a304873bd 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -14,13 +14,7 @@ public class Person : Identifiable There are two ways the public attribute name is determined: -1. By convention, specified in @JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_SerializerSettings -```c# -options.SerializerSettings.ContractResolver = new DefaultContractResolver -{ - NamingStrategy = new KebabCaseNamingStrategy() // default: CamelCaseNamingStrategy -}; -``` +1. By convention, specified by configuring the [naming strategy](./options.md#custom-serializer-settings). 2. Individually using the attribute's constructor ```c# diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 35e2d8ea78..129217f3f2 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -77,10 +77,7 @@ public void AddResourceGraph(Type dbContextType, Action co } /// - /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. - /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: - /// , , , - /// and . + /// Configures built-in ASP.NET Core MVC components. Most of this configuration can be adjusted for the developers' need. /// public void ConfigureMvc() { @@ -277,10 +274,7 @@ private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder builder.Add(entityType.ClrType); } } - - /// - /// Performs auto-discovery of JsonApiDotNetCore services. - /// + private void AutoDiscoverResources(ServiceDiscoveryFacade serviceDiscoveryFacade) { serviceDiscoveryFacade.DiscoverResources(); @@ -297,7 +291,7 @@ private void ExecuteManualConfigurationOfResources(Action public void Dispose() { - _intermediateProvider?.Dispose(); + _intermediateProvider.Dispose(); } } } From e53fe67f2f4c3f0b5bc7742d5f08d5650d57013d Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 9 Sep 2020 10:03:48 +0200 Subject: [PATCH 46/51] fix: typo in docs --- docs/usage/routing.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/usage/routing.md b/docs/usage/routing.md index a672a48f21..90c058ea0b 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -12,43 +12,42 @@ public void ConfigureServices(IServiceCollection services) ``` Which results in URLs like: https://yourdomain.com/api/v1/people -## Customizing Routes +## Default Routing Convention -The library will configure routes for all controllers in your project. - -### Json:api endpoints - -By default, for json:api controllers, -- routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec. -- the route of a controller will match the public name of the resource that is associated it. This means that routes can be customized by [configuring the public name of the associated resource](./resource-graph.md#public-resource-name). +The library will configure routes for all controllers in your project. By default, routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec. The public name of the resource ([which can be customized](./resource-graph.md#public-resource-name)) is used for the route. ```c# -public class MyResourceController : JsonApiController { /* .... */ } +public class OrderLine : Identifiable { } + +public class OrderController : JsonApiController { /* .... */ } ``` -The route for this example will be `/myApiResources`, which will match the type in the json:api payload: `{ "type": "myApiResources", ... }`. +```http +GET /orderLines HTTP/1.1 +``` -### Non-json:api endpoints +### Non-json:api controllers -If a controller does not have an associated resource, the [configured naming strategy](./options.md#custom-serializer-settings) will be applied to the name of the controller. +If a controller is not associated to a resource, the [configured naming strategy](./options.md#custom-serializer-settings) will be applied to the name of the controller. ```c# -public class MyResourceController : ControllerBase { /* .... */ } +public class OrderLineController : ControllerBase { /* .... */ } +``` +```http +GET /orderLines HTTP/1.1 ``` -The route for this example is `/myResources`, which can be changed by renaming the controller. - ## Disabling the Default Routing Convention -It is possible to by-pass the default routing convention for a controller by combining the `Route` and `DisableRoutingConvention`attributes. Any usage of `Route` without `DisableRoutingConvention` is ignored. +It is possible to by-pass the default routing convention for a controller. ```c# [Route("v1/camelCasedModels"), DisableRoutingConvention] public class MyCustomResourceController : JsonApiController { /* ... */ } ``` -This example exposes a versioned `CamelCasedModel` endpoint. To ensure guarantee valid link building, it is *highly recommended* to match your custom url with the public name of the associated resource. +In order to have valid link building, it is required to match your custom url with the public name of the associated resource. ## Advanced Usage: Custom Routing Convention. -It is possible to use a [custom routing convention](add-link) by registering a custom `IJsonApiRoutingConvention` implementation. This is generally not recommended and for advanced usage only. +It is possible to register a [custom routing convention]](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`. This is generally not recommended and for advanced usage only. ```c# public void ConfigureServices(IServiceCollection services) { From 57eb82e453b6b066a6c9032aad5f43e56190c24e Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 9 Sep 2020 16:01:27 +0200 Subject: [PATCH 47/51] chore: discovery tests fix --- .../ServiceDiscoveryFacadeTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 9386a4415d..23063e4932 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -48,31 +48,31 @@ public ServiceDiscoveryFacadeTests() } [Fact] - public void AddAssembly_Adds_All_Resources_To_Graph() + public void DiscoverResources_Adds_Resources_From_Added_Assembly_To_Graph() { // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + facade.AddAssembly(typeof(Person).Assembly); // Act - facade.AddAssembly(typeof(Person).Assembly); facade.DiscoverResources(); + + // Assert var resourceGraph = _resourceGraphBuilder.Build(); var personResource = resourceGraph.GetResourceContext(typeof(Person)); var articleResource = resourceGraph.GetResourceContext(typeof(Article)); - - // Assert Assert.NotNull(personResource); Assert.NotNull(articleResource); } [Fact] - public void AddCurrentAssembly_Adds_Resources_To_Graph() + public void DiscoverResources_Adds_Resources_From_Current_Assembly_To_Graph() { // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + facade.AddCurrentAssembly(); // Act - facade.AddCurrentAssembly(); facade.DiscoverResources(); // Assert @@ -82,13 +82,13 @@ public void AddCurrentAssembly_Adds_Resources_To_Graph() } [Fact] - public void AddCurrentAssembly_Adds_Services_To_Container() + public void DiscoverInjectables_Adds_Resource_Services_From_Current_Assembly_To_Container() { // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + facade.AddCurrentAssembly(); // Act - facade.AddCurrentAssembly(); facade.DiscoverInjectables(); // Assert @@ -98,13 +98,13 @@ public void AddCurrentAssembly_Adds_Services_To_Container() } [Fact] - public void AddCurrentAssembly_Adds_Repositories_To_Container() + public void DiscoverInjectables_Adds_Resource_Repositories_From_Current_Assembly_To_Container() { // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + facade.AddCurrentAssembly(); // Act - facade.AddCurrentAssembly(); facade.DiscoverInjectables(); // Assert @@ -113,13 +113,13 @@ public void AddCurrentAssembly_Adds_Repositories_To_Container() } [Fact] - public void AddCurrentAssembly_Adds_ResourceDefinitions_To_Container() + public void AddCurrentAssembly_Adds_Resource_Definitions_From_Current_Assembly_To_Container() { // Arrange ServiceDiscoveryFacade facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, NullLoggerFactory.Instance); + facade.AddCurrentAssembly(); // Act - facade.AddCurrentAssembly(); facade.DiscoverInjectables(); // Assert From cd1222a4f1c57ceca227e480e9b251a1d6969453 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 9 Sep 2020 16:29:32 +0200 Subject: [PATCH 48/51] docs: improvements --- docs/usage/extensibility/middleware.md | 13 ++++++------- docs/usage/options.md | 2 +- docs/usage/resource-graph.md | 5 +++-- docs/usage/resources/attributes.md | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 705695fd5a..271db2b381 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -7,32 +7,31 @@ It is possible to replace JsonApiDotNetCore middleware components by configuring The following example replaces the internal exception filter with a custom implementation ```c# /// In Startup.ConfigureServices -services.AddService() +services.AddService() ``` ## Configuring `MvcOptions` -To prevent the library from overwriting your configuration, perform your configuration *after* the library is done configuring `MvcOptions`. The following example replaces all internal filters with a custom filter. +The following example replaces all internal filters with a custom filter. ```c# /// In Startup.ConfigureServices -services.AddSingleton(); +services.AddSingleton(); + var builder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: builder); -// Ensure the configuration action is registered after the `AddJsonApi()` call. +// Ensure this call is placed AFTER the `AddJsonApi()` call, or JsonApiDotNetCore will overwrite your configurations. builder.AddMvcOptions(mvcOptions => { - // Execute the MvcOptions configuration callback after the JsonApiDotNetCore callback as been executed. _postConfigureMvcOptions?.Invoke(mvcOptions); }); /// In Startup.Configure app.UseJsonApi(); -// Ensure the configuration callback is set after calling `UseJsonApi()`. _postConfigureMvcOptions = mvcOptions => { mvcOptions.Filters.Clear(); - mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); + mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); }; ``` diff --git a/docs/usage/options.md b/docs/usage/options.md index 4a6c914f74..b7ce4678a6 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -90,7 +90,7 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter()); options.SerializerSettings.Formatting = Formatting.Indented; ``` -The default naming convention as used in the routes and public resources names is also determined here, and can be changed (default is camel-case): +The default casing convention as used in the routes and public resources names is also determined here, and can be changed (default is camel-case): ```c# options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() }; ``` diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 311214ef9e..7e30929b45 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -95,9 +95,10 @@ services.AddJsonApi(resources: builder => public class MyModel : Identifiable { /* ... */ } ``` -3. The configured naming convention (by default this is camel-case). +3. The configured casing convention (by default this is camel-case). ```c# // this will be registered as "myModels" public class MyModel : Identifiable { /* ... */ } ``` -This convention can be changed by setting the `SerializerSettings.ContractResolver` property on `IJsonApiOptions`. See the [options documentation](./options.md#custom-serializer-settings). + +The default casing convention can be changed in [options](./options.md#custom-serializer-settings). diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index 4a304873bd..47b60cfdb4 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -14,7 +14,8 @@ public class Person : Identifiable There are two ways the public attribute name is determined: -1. By convention, specified by configuring the [naming strategy](./options.md#custom-serializer-settings). +1. Using the configured [casing convention](./options.md#custom-serializer-settings). + 2. Individually using the attribute's constructor ```c# From 0b623be3852936c934c042b6339851c7db6b3344 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 10 Sep 2020 11:51:20 +0200 Subject: [PATCH 49/51] chore: review --- docs/usage/extensibility/middleware.md | 6 +-- docs/usage/options.md | 2 +- docs/usage/routing.md | 22 +++++---- .../JsonApiApplicationBuilder.cs | 48 +++++++------------ 4 files changed, 33 insertions(+), 45 deletions(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 271db2b381..b168e30e36 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -4,10 +4,10 @@ It is possible to replace JsonApiDotNetCore middleware components by configuring ## Configuring the IoC container -The following example replaces the internal exception filter with a custom implementation +The following example replaces the internal exception filter with a custom implementation. ```c# /// In Startup.ConfigureServices -services.AddService() +services.AddService() ``` ## Configuring `MvcOptions` @@ -20,7 +20,7 @@ services.AddSingleton(); var builder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: builder); -// Ensure this call is placed AFTER the `AddJsonApi()` call, or JsonApiDotNetCore will overwrite your configurations. +// Ensure this call is placed after the AddJsonApi() call. builder.AddMvcOptions(mvcOptions => { _postConfigureMvcOptions?.Invoke(mvcOptions); diff --git a/docs/usage/options.md b/docs/usage/options.md index b7ce4678a6..93bcd47c96 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -90,7 +90,7 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter()); options.SerializerSettings.Formatting = Formatting.Indented; ``` -The default casing convention as used in the routes and public resources names is also determined here, and can be changed (default is camel-case): +The default casing convention (as used in the routes and public resources names) is also determined here, and can be changed (default is camel-case): ```c# options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() }; ``` diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 90c058ea0b..08d25de34e 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -14,21 +14,23 @@ Which results in URLs like: https://yourdomain.com/api/v1/people ## Default Routing Convention -The library will configure routes for all controllers in your project. By default, routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec. The public name of the resource ([which can be customized](./resource-graph.md#public-resource-name)) is used for the route. +The library will configure routes for all controllers in your project. By default, routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec. ```c# public class OrderLine : Identifiable { } -public class OrderController : JsonApiController { /* .... */ } +public class OrderLineController : JsonApiController { /* .... */ } ``` ```http GET /orderLines HTTP/1.1 ``` +The public name of the resource ([which can be customized](./resource-graph.md#public-resource-name)) is used for the route, instead of the controller name. + ### Non-json:api controllers -If a controller is not associated to a resource, the [configured naming strategy](./options.md#custom-serializer-settings) will be applied to the name of the controller. +If a controller does not inherit from `JsonApiController`, the [configured casing convention](./options.md#custom-serializer-settings) is applied to the name of the controller. ```c# public class OrderLineController : ControllerBase { /* .... */ } ``` @@ -38,19 +40,19 @@ GET /orderLines HTTP/1.1 ## Disabling the Default Routing Convention -It is possible to by-pass the default routing convention for a controller. +It is possible to bypass the default routing convention for a controller. ```c# -[Route("v1/camelCasedModels"), DisableRoutingConvention] -public class MyCustomResourceController : JsonApiController { /* ... */ } +[Route("v1/custom/route/orderLines"), DisableRoutingConvention] +public class OrderLineController : JsonApiController { /* ... */ } ``` -In order to have valid link building, it is required to match your custom url with the public name of the associated resource. +It is required to match your custom url with the public name of the associated resource. -## Advanced Usage: Custom Routing Convention. +## Advanced Usage: Custom Routing Convention -It is possible to register a [custom routing convention]](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`. This is generally not recommended and for advanced usage only. +It is possible to replace the built-in routing convention with a [custom routing convention]](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`. ```c# public void ConfigureServices(IServiceCollection services) { - services.AddSingleton(); + services.AddSingleton(); } ``` \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 129217f3f2..a550a06e7e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -57,6 +57,9 @@ public void ConfigureJsonApiOptions(Action configureOptions) configureOptions?.Invoke(_options); } + /// + /// Executes the action provided by the user to configure . + /// public void ConfigureAutoDiscovery(Action configureAutoDiscovery) { configureAutoDiscovery?.Invoke(_serviceDiscoveryFacade); @@ -67,13 +70,17 @@ public void ConfigureAutoDiscovery(Action configureAutoD /// public void AddResourceGraph(Type dbContextType, Action configureResourceGraph) { - AutoDiscoverResources(_serviceDiscoveryFacade); + _serviceDiscoveryFacade.DiscoverResources(); + if (dbContextType != null) { AddResourcesFromDbContext((DbContext)_intermediateProvider.GetService(dbContextType), _resourceGraphBuilder); } - ExecuteManualConfigurationOfResources(configureResourceGraph, _resourceGraphBuilder); - _services.AddSingleton(_resourceGraphBuilder.Build()); + + configureResourceGraph?.Invoke(_resourceGraphBuilder); + + var resourceGraph = _resourceGraphBuilder.Build(); + _services.AddSingleton(resourceGraph); } /// @@ -216,30 +223,23 @@ private void AddQueryStringParameterServices() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services - .AddScoped(); + _services.AddScoped(); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); + _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => - sp.GetService()); + _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); - _services.AddScoped( - sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); _services.AddScoped(); _services.AddSingleton(); @@ -275,20 +275,6 @@ private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder } } - private void AutoDiscoverResources(ServiceDiscoveryFacade serviceDiscoveryFacade) - { - serviceDiscoveryFacade.DiscoverResources(); - } - - /// - /// Executes the action provided by the user to configure the resources using - /// - private void ExecuteManualConfigurationOfResources(Action configureResources, - ResourceGraphBuilder resourceGraphBuilder) - { - configureResources?.Invoke(resourceGraphBuilder); - } - public void Dispose() { _intermediateProvider.Dispose(); From 948b72abc702a3767d3a2e2b551f85ea61104405 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 10 Sep 2020 12:05:50 +0200 Subject: [PATCH 50/51] chore: casing convention -> naming convention --- docs/usage/options.md | 2 +- docs/usage/resource-graph.md | 4 ++-- docs/usage/resources/attributes.md | 2 +- docs/usage/routing.md | 2 +- src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs | 2 +- src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs | 6 +++--- .../Configuration/ResourceNameFormatter.cs | 2 +- .../Middleware/JsonApiRoutingConvention.cs | 4 ++-- .../Resources/Annotations/ResourceAttribute.cs | 2 +- .../Resources/Annotations/ResourceFieldAttribute.cs | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/usage/options.md b/docs/usage/options.md index 93bcd47c96..6bb09865b7 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -90,7 +90,7 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter()); options.SerializerSettings.Formatting = Formatting.Indented; ``` -The default casing convention (as used in the routes and public resources names) is also determined here, and can be changed (default is camel-case): +The default naming convention (as used in the routes and public resources names) is also determined here, and can be changed (default is camel-case): ```c# options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() }; ``` diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 7e30929b45..d6b1f83d8f 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -95,10 +95,10 @@ services.AddJsonApi(resources: builder => public class MyModel : Identifiable { /* ... */ } ``` -3. The configured casing convention (by default this is camel-case). +3. The configured naming convention (by default this is camel-case). ```c# // this will be registered as "myModels" public class MyModel : Identifiable { /* ... */ } ``` -The default casing convention can be changed in [options](./options.md#custom-serializer-settings). +The default naming convention can be changed in [options](./options.md#custom-serializer-settings). diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index 47b60cfdb4..e9850648cf 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -14,7 +14,7 @@ public class Person : Identifiable There are two ways the public attribute name is determined: -1. Using the configured [casing convention](./options.md#custom-serializer-settings). +1. Using the configured [naming convention](./options.md#custom-serializer-settings). 2. Individually using the attribute's constructor diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 08d25de34e..442bab4d42 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -30,7 +30,7 @@ The public name of the resource ([which can be customized](./resource-graph.md#p ### Non-json:api controllers -If a controller does not inherit from `JsonApiController`, the [configured casing convention](./options.md#custom-serializer-settings) is applied to the name of the controller. +If a controller does not inherit from `JsonApiController`, the [configured naming convention](./options.md#custom-serializer-settings) is applied to the name of the controller. ```c# public class OrderLineController : ControllerBase { /* .... */ } ``` diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 8c2455caf6..fded94294a 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -176,7 +176,7 @@ public interface IJsonApiOptions /// Specifies the settings that are used by the . /// Note that at some places a few settings are ignored, to ensure json:api spec compliance. /// - /// The next example changes the casing convention to kebab casing. + /// The next example changes the naming convention to kebab casing. /// The resource model type. /// /// The name under which the resource is publicly exposed by the API. - /// If nothing is specified, the configured casing convention formatter will be applied. + /// If nothing is specified, the configured naming convention formatter will be applied. /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable => Add(publicName); @@ -64,7 +64,7 @@ public ResourceGraphBuilder Add(string publicName = null) where TReso /// The resource model identifier type. /// /// The name under which the resource is publicly exposed by the API. - /// If nothing is specified, the configured casing convention formatter will be applied. + /// If nothing is specified, the configured naming convention formatter will be applied. /// public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable => Add(typeof(TResource), typeof(TId), publicName); @@ -76,7 +76,7 @@ public ResourceGraphBuilder Add(string publicName = null) where /// The resource model identifier type. /// /// The name under which the resource is publicly exposed by the API. - /// If nothing is specified, the configured casing convention formatter will be applied. + /// If nothing is specified, the configured naming convention formatter will be applied. /// public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) { diff --git a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs index e58816186f..85bf857a9a 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs @@ -16,7 +16,7 @@ public ResourceNameFormatter(IJsonApiOptions options) } /// - /// Gets the publicly visible resource name for the internal type name using the configured casing convention. + /// Gets the publicly visible resource name for the internal type name using the configured naming convention. /// public string FormatResourceName(Type resourceType) { diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 9d78b6a299..81192b1c29 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -14,7 +14,7 @@ namespace JsonApiDotNetCore.Middleware { /// /// The default routing convention registers the name of the resource as the route - /// using the serializer casing convention. The default for this is + /// using the serializer naming convention. The default for this is /// a camel case formatter. If the controller directly inherits from and there is no /// resource directly associated, it uses the name of the controller instead of the name of the type. /// @@ -23,7 +23,7 @@ namespace JsonApiDotNetCore.Middleware /// /// public class RandomNameController : JsonApiController { } // => /someResources/relationship/relatedResource /// - /// // when using kebab-case casing convention: + /// // when using kebab-case naming convention: /// public class SomeResourceController : JsonApiController { } // => /some-resources/relationship/related-resource /// /// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustoms/relationship/relatedResource diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs index 7b8a3a6ad0..f3d92fd2a1 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs @@ -10,7 +10,7 @@ public sealed class ResourceAttribute : Attribute { /// /// The publicly exposed name of this resource type. - /// When not explicitly assigned, the configured casing convention is applied on the pluralized resource class name. + /// When not explicitly assigned, the configured naming convention is applied on the pluralized resource class name. /// public string PublicName { get; } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs index d9688ae52e..2a5bafe980 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs @@ -13,7 +13,7 @@ public abstract class ResourceFieldAttribute : Attribute /// /// The publicly exposed name of this json:api field. - /// When not explicitly assigned, the configured casing convention is applied on the property name. + /// When not explicitly assigned, the configured naming convention is applied on the property name. /// public string PublicName { From 911924c466a40c188a41895b8dbad3327742c53a Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 10 Sep 2020 12:12:07 +0200 Subject: [PATCH 51/51] fix: update example in docs --- docs/usage/extensibility/middleware.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index b168e30e36..2b1c0bce2e 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -20,7 +20,7 @@ services.AddSingleton(); var builder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: builder); -// Ensure this call is placed after the AddJsonApi() call. +// Ensure this call is placed after the AddJsonApi call. builder.AddMvcOptions(mvcOptions => { _postConfigureMvcOptions?.Invoke(mvcOptions); @@ -29,9 +29,12 @@ builder.AddMvcOptions(mvcOptions => /// In Startup.Configure app.UseJsonApi(); +// Ensure this call is placed before the UseEndpoints call. _postConfigureMvcOptions = mvcOptions => { mvcOptions.Filters.Clear(); mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); }; + +app.UseEndpoints(endpoints => endpoints.MapControllers()); ```