From 6f88360b7ba2a9d5ae0ed926b88d12e1a6323ca3 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 14:35:38 +0200 Subject: [PATCH 01/13] feat: draft of DI-registered ApplicationModelConvention --- .../Builders/ResourceGraphBuilder.cs | 6 +- .../Configuration/JsonApiOptions.cs | 11 ++- .../Controllers/JsonApiController.cs | 2 - .../IServiceCollectionExtensions.cs | 33 ++++++--- .../Graph/IResourceNameFormatter.cs | 68 ------------------ .../Graph/KebabResourceNameFormatter.cs | 71 +++++++++++++++++++ .../Internal/CamelizedRoutingConvention.cs | 59 +++++++++++++-- .../Internal/DasherizedRoutingConvention.cs | 4 +- .../Builders/ContextGraphBuilder_Tests.cs | 18 +++-- 9 files changed, 173 insertions(+), 99 deletions(-) create mode 100644 src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index 5422cea8a1..1e11d645e3 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -26,7 +26,7 @@ public class ResourceGraphBuilder : IResourceGraphBuilder public ResourceGraphBuilder(IResourceNameFormatter formatter = null) { - _resourceNameFormatter = formatter ?? new DefaultResourceNameFormatter(); + _resourceNameFormatter = formatter ?? new KebabResourceNameFormatter(); } /// @@ -35,7 +35,7 @@ public IResourceGraph Build() _entities.ForEach(SetResourceLinksOptions); List controllerContexts = new List() { }; - foreach(var cm in _controllerMapper) + foreach (var cm in _controllerMapper) { var model = cm.Key; foreach (var controller in cm.Value) @@ -180,7 +180,7 @@ protected virtual List GetRelationships(Type entityType) // Article → ArticleTag.Tag hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.DependentType) ?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.DependentType}"); - + // ArticleTag.TagId var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name); hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 2b3e9bae21..ef461ba3f4 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -10,7 +10,6 @@ namespace JsonApiDotNetCore.Configuration { - /// /// Global options /// @@ -30,10 +29,18 @@ public class JsonApiOptions : IJsonApiOptions public Link RelationshipLinks { get; set; } = Link.All; + internal Type ResourceNameFormatterType { get; set; } = typeof(KebabResourceNameFormatter); + + //public void UseResourceNameFormatter() where TFormatter : class, IResourceNameFormatter + //{ + // ResourceNameFormatterType = typeof(TFormatter); + //} + + /// /// Provides an interface for formatting resource names by convention /// - public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new DefaultResourceNameFormatter(); + public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabResourceNameFormatter(); /// /// Provides an interface for formatting relationship id properties given the navigation property name diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index e538f1cff9..b960e94a39 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -9,8 +9,6 @@ namespace JsonApiDotNetCore.Controllers { - - public class JsonApiController : BaseJsonApiController where T : class, IIdentifiable { /// diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 4466e1ae30..51ff2a07b2 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -25,6 +25,8 @@ using JsonApiDotNetCore.Serialization.Server; using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCore.Controllers; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace JsonApiDotNetCore.Extensions { @@ -32,7 +34,6 @@ namespace JsonApiDotNetCore.Extensions public static class IServiceCollectionExtensions { static private readonly Action _noopConfig = opt => { }; - static private JsonApiOptions _options { get { return new JsonApiOptions(); } } public static IServiceCollection AddJsonApi(this IServiceCollection services, IMvcCoreBuilder mvcBuilder = null) where TContext : DbContext @@ -52,7 +53,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se IMvcCoreBuilder mvcBuilder = null) where TContext : DbContext { - var options = _options; + var options = new JsonApiOptions(); // add basic Mvc functionality mvcBuilder = mvcBuilder ?? services.AddMvcCore(); // set standard options @@ -83,7 +84,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, Action configureOptions, IMvcCoreBuilder mvcBuilder = null) { - var options = _options; + var options = new JsonApiOptions(); mvcBuilder = mvcBuilder ?? services.AddMvcCore(); configureOptions(options); @@ -107,7 +108,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, Action autoDiscover, IMvcCoreBuilder mvcBuilder = null) { - var options = _options; + var options = new JsonApiOptions(); mvcBuilder = mvcBuilder ?? services.AddMvcCore(); configureOptions(options); @@ -115,11 +116,20 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, var facade = new ServiceDiscoveryFacade(services, options.ResourceGraphBuilder); autoDiscover(facade); - // add JsonApi fitlers and serializer + // add JsonApi filters and serializers mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); + // register services that allow user to override behaviour that is configured on startup + AddStartupConfigurationServices(services, options); + + var intermediateProvider = services.BuildServiceProvider(); + mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); + //intermediateProvider.Dispose(); + // register services AddJsonApiInternals(services, options); + + return services; } @@ -143,11 +153,17 @@ public static void AddJsonApiInternals( AddJsonApiInternals(services, jsonApiOptions); } + private static void AddStartupConfigurationServices(this IServiceCollection services, JsonApiOptions jsonApiOptions) + { + services.AddSingleton(jsonApiOptions); + services.TryAddSingleton(new KebabResourceNameFormatter()); + services.TryAddSingleton(); + } + public static void AddJsonApiInternals( this IServiceCollection services, JsonApiOptions jsonApiOptions) { - var graph = jsonApiOptions.ResourceGraph ?? jsonApiOptions.ResourceGraphBuilder.Build(); if (graph.UsesDbContext == false) @@ -183,14 +199,13 @@ public static void AddJsonApiInternals( services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>)); services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>)); - services.AddSingleton(jsonApiOptions); services.AddSingleton(jsonApiOptions); services.AddSingleton(graph); services.AddSingleton(); services.AddSingleton(graph); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + //services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -273,7 +288,7 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js { options.InputFormatters.Insert(0, new JsonApiInputFormatter()); options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); - options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace)); + //options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace)); } /// diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs index 6feaab949d..9de1a7c6a6 100644 --- a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs @@ -1,10 +1,5 @@ using System; -using System.Linq; using System.Reflection; -using Humanizer; -using JsonApiDotNetCore.Models; -using str = JsonApiDotNetCore.Extensions.StringExtensions; - namespace JsonApiDotNetCore.Graph { @@ -29,67 +24,4 @@ public interface IResourceNameFormatter /// string ApplyCasingConvention(string properName); } - - public class DefaultResourceNameFormatter : IResourceNameFormatter - { - /// - /// Uses the internal type name to determine the external resource name. - /// By default we us Humanizer for pluralization and then we dasherize the name. - /// - /// - /// - /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todo-items" - /// - /// - public string FormatResourceName(Type type) - { - try - { - // check the class definition first - // [Resource("models"] public class Model : Identifiable { /* ... */ } - if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) - return attribute.ResourceName; - - return ApplyCasingConvention(type.Name.Pluralize()); - } - catch (InvalidOperationException e) - { - throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); - } - } - - /// - /// Aoplies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todo-items" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todo-item" - /// - /// - public string ApplyCasingConvention(string properName) => str.Dasherize(properName); - - /// - /// Uses the internal PropertyInfo to determine the external resource name. - /// By default the name will be formatted to kebab-case. - /// - /// - /// Given the following property: - /// - /// public string CompoundProperty { get; set; } - /// - /// The public attribute will be formatted like so: - /// - /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compound-property" - /// - /// - public string FormatPropertyName(PropertyInfo property) => str.Dasherize(property.Name); - } } diff --git a/src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs new file mode 100644 index 0000000000..593daab2bb --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; +using Humanizer; +using JsonApiDotNetCore.Models; +using str = JsonApiDotNetCore.Extensions.StringExtensions; + +namespace JsonApiDotNetCore.Graph +{ + public class KebabResourceNameFormatter : IResourceNameFormatter + { + /// + /// Uses the internal type name to determine the external resource name. + /// By default we us Humanizer for pluralization and then we dasherize the name. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todo-items" + /// + /// + public string FormatResourceName(Type type) + { + try + { + // check the class definition first + // [Resource("models"] public class Model : Identifiable { /* ... */ } + if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + return attribute.ResourceName; + + return ApplyCasingConvention(type.Name.Pluralize()); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); + } + } + + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todo-items" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todo-item" + /// + /// + public string ApplyCasingConvention(string properName) => str.Dasherize(properName); + + /// + /// Uses the internal PropertyInfo to determine the external resource name. + /// By default the name will be formatted to kebab-case. + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compound-property" + /// + /// + public string FormatPropertyName(PropertyInfo property) => str.Dasherize(property.Name); + } +} diff --git a/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs index edb7e2444a..85e73f04d2 100644 --- a/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs @@ -1,28 +1,59 @@ // REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs // REF: https://github.com/aspnet/Mvc/issues/5691 +using System; +using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace JsonApiDotNetCore.Internal { - public class DasherizedRoutingConvention : IApplicationModelConvention + /// + /// The default routing convention registers the name of the resource as the route + /// using the that is registered. The default for this is + /// a kebab-case formatter. + /// + /// + /// public class SomeResourceController{SomeResource} : JsonApiController{SomeResource} { } + /// // => /some-resources/relationship/related-resource + /// + /// public class RandomNameController{SomeResource} : JsonApiController{SomeResource} { } + /// // => /some-resources/relationship/related-resource + /// + /// // when using the camelCase formatter: + /// public class SomeResourceController{SomeResource} : JsonApiController{SomeResource} { } + /// // => /someResources/relationship/relatedResource + /// + public class DefaultRoutingConvention : IJsonApiRoutingConvention { private readonly string _namespace; - public DasherizedRoutingConvention(string nspace) + private readonly IResourceNameFormatter _formatter; + + public DefaultRoutingConvention(IJsonApiOptions options, IResourceNameFormatter formatter) { - _namespace = nspace; + _namespace = options.Namespace; + _formatter = formatter; } public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) { - if (IsDasherizedJsonApiController(controller) == false) + if (RoutingConventionDisabled(controller) == false) continue; - var template = $"{_namespace}/{controller.ControllerName.Dasherize()}"; + // derive the targeted resource by reflecting the controllers generic arguments. + var resourceType = GetResourceTypeFromController(controller.ControllerType); + string endpoint; + if (resourceType != null) + endpoint = _formatter.FormatResourceName(resourceType); + else + endpoint = _formatter.ApplyCasingConvention(controller.ControllerName); + + // apply the registered resource name formatter to the discovered resource type. + var template = $"{_namespace}/{endpoint}"; controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template @@ -30,11 +61,25 @@ public void Apply(ApplicationModel application) } } - private bool IsDasherizedJsonApiController(ControllerModel controller) + private bool RoutingConventionDisabled(ControllerModel controller) { var type = controller.ControllerType; var notDisabled = type.GetCustomAttribute() == null; return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); } + + + public Type GetResourceTypeFromController(Type type) + { + var target = typeof(BaseJsonApiController<,>); + // return all inherited types + var currentBaseType = type.BaseType; + while (!currentBaseType.IsGenericType || currentBaseType.GetGenericTypeDefinition() != target) + { + currentBaseType = currentBaseType.BaseType; + if (currentBaseType == null) break; + } + return currentBaseType?.GetGenericArguments().First(); + } } } diff --git a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs index 21033838d4..3845968b53 100644 --- a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Internal { - public class CamelizedRoutingConvention : IApplicationModelConvention + public class CamelizedRoutingConvention : IJsonApiRoutingConvention { private readonly string _namespace; public CamelizedRoutingConvention(string nspace) @@ -37,4 +37,6 @@ private bool IsCamelizedJsonApiController(ControllerModel controller) return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); } } + + public interface IJsonApiRoutingConvention : IApplicationModelConvention { } } diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index 186530197a..45a515f79d 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -18,15 +18,16 @@ namespace UnitTests { public class ResourceGraphBuilder_Tests { - class NonDbResource : Identifiable {} - class DbResource : Identifiable {} - class TestContext : DbContext { + class NonDbResource : Identifiable { } + class DbResource : Identifiable { } + class TestContext : DbContext + { public DbSet DbResources { get; set; } } public ResourceGraphBuilder_Tests() { - JsonApiOptions.ResourceNameFormatter = new DefaultResourceNameFormatter(); + JsonApiOptions.ResourceNameFormatter = new KebabResourceNameFormatter(); } [Fact] @@ -34,8 +35,10 @@ public void Can_Build_ResourceGraph_Using_Builder() { // arrange var services = new ServiceCollection(); - services.AddJsonApi(opt => { - opt.BuildResourceGraph(b => { + services.AddJsonApi(opt => + { + opt.BuildResourceGraph(b => + { b.AddResource("non-db-resources"); }); }); @@ -128,7 +131,8 @@ public void Relationships_Without_Names_Specified_Will_Use_Default_Formatter() Assert.Equal("related-resources", resource.Relationships.Single(r => r.IsHasMany).PublicRelationshipName); } - public class TestResource : Identifiable { + public class TestResource : Identifiable + { [Attr] public string CompoundAttribute { get; set; } [HasOne] public RelatedResource RelatedResource { get; set; } [HasMany] public List RelatedResources { get; set; } From abcc5c0822b9552681ec23b935e965009f4a3b70 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 15:06:23 +0200 Subject: [PATCH 02/13] feat: injectable IResourceNameFormatter and IJsonApiRoutingCOnvention now working --- .../Builders/ResourceGraphBuilder.cs | 2 +- .../Configuration/JsonApiOptions.cs | 4 +- .../IServiceCollectionExtensions.cs | 2 +- .../Graph/CamelCaseResourceNameFormatter.cs | 71 +++++++++++++++++++ ...r.cs => KebabCaseResourceNameFormatter.cs} | 2 +- .../Internal/DasherizedRoutingConvention.cs | 42 ----------- ...vention.cs => DefaultRoutingConvention.cs} | 42 ++++++----- .../Internal/IJsonApiRoutingConvention.cs | 10 +++ .../Builders/ContextGraphBuilder_Tests.cs | 2 +- 9 files changed, 113 insertions(+), 64 deletions(-) create mode 100644 src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs rename src/JsonApiDotNetCore/Graph/{KebabResourceNameFormatter.cs => KebabCaseResourceNameFormatter.cs} (97%) delete mode 100644 src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs rename src/JsonApiDotNetCore/Internal/{CamelizedRoutingConvention.cs => DefaultRoutingConvention.cs} (68%) create mode 100644 src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index 1e11d645e3..61fed971c9 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -26,7 +26,7 @@ public class ResourceGraphBuilder : IResourceGraphBuilder public ResourceGraphBuilder(IResourceNameFormatter formatter = null) { - _resourceNameFormatter = formatter ?? new KebabResourceNameFormatter(); + _resourceNameFormatter = formatter ?? new KebabCaseResourceNameFormatter(); } /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index ef461ba3f4..996ec796b5 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -29,7 +29,7 @@ public class JsonApiOptions : IJsonApiOptions public Link RelationshipLinks { get; set; } = Link.All; - internal Type ResourceNameFormatterType { get; set; } = typeof(KebabResourceNameFormatter); + internal Type ResourceNameFormatterType { get; set; } = typeof(KebabCaseResourceNameFormatter); //public void UseResourceNameFormatter() where TFormatter : class, IResourceNameFormatter //{ @@ -40,7 +40,7 @@ public class JsonApiOptions : IJsonApiOptions /// /// Provides an interface for formatting resource names by convention /// - public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabResourceNameFormatter(); + public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabCaseResourceNameFormatter(); /// /// Provides an interface for formatting relationship id properties given the navigation property name diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 51ff2a07b2..0272829351 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -156,7 +156,7 @@ public static void AddJsonApiInternals( private static void AddStartupConfigurationServices(this IServiceCollection services, JsonApiOptions jsonApiOptions) { services.AddSingleton(jsonApiOptions); - services.TryAddSingleton(new KebabResourceNameFormatter()); + services.TryAddSingleton(new KebabCaseResourceNameFormatter()); services.TryAddSingleton(); } diff --git a/src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs new file mode 100644 index 0000000000..d36f1fb28f --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs @@ -0,0 +1,71 @@ +using System; +using System.Reflection; +using Humanizer; +using JsonApiDotNetCore.Models; +using str = JsonApiDotNetCore.Extensions.StringExtensions; + +namespace JsonApiDotNetCore.Graph +{ + public class CamelCaseResourceNameFormatter : IResourceNameFormatter + { + /// + /// Uses the internal type name to determine the external resource name. + /// By default we us Humanizer for pluralization and then we dasherize the name. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todo-items" + /// + /// + public string FormatResourceName(Type type) + { + try + { + // check the class definition first + // [Resource("models"] public class Model : Identifiable { /* ... */ } + if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + return attribute.ResourceName; + + return ApplyCasingConvention(type.Name.Pluralize()); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); + } + } + + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todo-items" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todo-item" + /// + /// + public string ApplyCasingConvention(string properName) => str.Camelize(properName); + + /// + /// Uses the internal PropertyInfo to determine the external resource name. + /// By default the name will be formatted to kebab-case. + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compound-property" + /// + /// + public string FormatPropertyName(PropertyInfo property) => str.Camelize(property.Name); + } +} diff --git a/src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs similarity index 97% rename from src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs rename to src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs index 593daab2bb..6660a1054b 100644 --- a/src/JsonApiDotNetCore/Graph/KebabResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Graph { - public class KebabResourceNameFormatter : IResourceNameFormatter + public class KebabCaseResourceNameFormatter : IResourceNameFormatter { /// /// Uses the internal type name to determine the external resource name. diff --git a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs deleted file mode 100644 index 3845968b53..0000000000 --- a/src/JsonApiDotNetCore/Internal/DasherizedRoutingConvention.cs +++ /dev/null @@ -1,42 +0,0 @@ -// REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs -// REF: https://github.com/aspnet/Mvc/issues/5691 -using System.Reflection; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Extensions; -using Microsoft.AspNetCore.Mvc.ApplicationModels; - -namespace JsonApiDotNetCore.Internal -{ - public class CamelizedRoutingConvention : IJsonApiRoutingConvention - { - private readonly string _namespace; - public CamelizedRoutingConvention(string nspace) - { - _namespace = nspace; - } - - public void Apply(ApplicationModel application) - { - foreach (var controller in application.Controllers) - { - if (IsCamelizedJsonApiController(controller) == false) - continue; - - var template = $"{_namespace}/{controller.ControllerName.Camelize()}"; - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel - { - Template = template - }; - } - } - - private bool IsCamelizedJsonApiController(ControllerModel controller) - { - var type = controller.ControllerType; - var notDisabled = type.GetCustomAttribute() == null; - return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); - } - } - - public interface IJsonApiRoutingConvention : IApplicationModelConvention { } -} diff --git a/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs similarity index 68% rename from src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs rename to src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs index 85e73f04d2..e0bb8c93ff 100644 --- a/src/JsonApiDotNetCore/Internal/CamelizedRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs @@ -1,6 +1,7 @@ // REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs // REF: https://github.com/aspnet/Mvc/issues/5691 using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using JsonApiDotNetCore.Configuration; @@ -30,7 +31,7 @@ public class DefaultRoutingConvention : IJsonApiRoutingConvention { private readonly string _namespace; private readonly IResourceNameFormatter _formatter; - + private readonly HashSet _registeredTemplates = new HashSet(); public DefaultRoutingConvention(IJsonApiOptions options, IResourceNameFormatter formatter) { _namespace = options.Namespace; @@ -44,20 +45,11 @@ public void Apply(ApplicationModel application) if (RoutingConventionDisabled(controller) == false) continue; - // derive the targeted resource by reflecting the controllers generic arguments. - var resourceType = GetResourceTypeFromController(controller.ControllerType); - string endpoint; - if (resourceType != null) - endpoint = _formatter.FormatResourceName(resourceType); - else - endpoint = _formatter.ApplyCasingConvention(controller.ControllerName); + var template = TemplateFromResource(controller) ?? TemplateFromController(controller); + if (template == null) + throw new JsonApiSetupException($"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); - // apply the registered resource name formatter to the discovered resource type. - var template = $"{_namespace}/{endpoint}"; - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel - { - Template = template - }; + controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; } } @@ -68,11 +60,29 @@ private bool RoutingConventionDisabled(ControllerModel controller) return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); } + private string TemplateFromResource(ControllerModel model) + { + var resourceType = GetResourceTypeFromController(model.ControllerType); + if (resourceType != null) + { + var template = $"{_namespace}/{_formatter.FormatResourceName(resourceType)}"; + if (_registeredTemplates.Add(template)) + return template; + } + return null; + } + + private string TemplateFromController(ControllerModel model) + { + var template = $"{_namespace}/{_formatter.ApplyCasingConvention(model.ControllerName)}"; + if (_registeredTemplates.Add(template)) + return template; + return null; + } - public Type GetResourceTypeFromController(Type type) + private Type GetResourceTypeFromController(Type type) { var target = typeof(BaseJsonApiController<,>); - // return all inherited types var currentBaseType = type.BaseType; while (!currentBaseType.IsGenericType || currentBaseType.GetGenericTypeDefinition() != target) { diff --git a/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs new file mode 100644 index 0000000000..9cae89fe10 --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs @@ -0,0 +1,10 @@ +// REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs +// REF: https://github.com/aspnet/Mvc/issues/5691 +using Microsoft.AspNetCore.Mvc.ApplicationModels; + +namespace JsonApiDotNetCore.Internal +{ + public interface IJsonApiRoutingConvention : IApplicationModelConvention + { + } +} diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index 45a515f79d..791c495f2c 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -27,7 +27,7 @@ class TestContext : DbContext public ResourceGraphBuilder_Tests() { - JsonApiOptions.ResourceNameFormatter = new KebabResourceNameFormatter(); + JsonApiOptions.ResourceNameFormatter = new KebabCaseResourceNameFormatter(); } [Fact] From 6a2074b765a8b380d429f1eb60de3b380e296db9 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 15:12:29 +0200 Subject: [PATCH 03/13] test: kebab vs camelcase configuration --- .../Controllers/CamelCasedModelsController.cs | 2 -- .../CamelCasedModelsControllerTests.cs | 8 +++--- .../CamelCaseTestStartup.cs | 26 +++++++++++++++++++ .../TestStartup.cs | 2 +- 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs index c46a9aa094..20571166ad 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs @@ -8,8 +8,6 @@ namespace JsonApiDotNetCoreExample.Controllers { - [Route("[controller]")] - [DisableRoutingConvention] public class CamelCasedModelsController : JsonApiController { public CamelCasedModelsController( diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs index 6adb3bce07..8399ff36ed 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/CamelCasedModelsControllerTests.cs @@ -38,7 +38,7 @@ public async Task Can_Get_CamelCasedModels() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = "/camelCasedModels"; + var route = "api/v1/camelCasedModels"; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); @@ -65,7 +65,7 @@ public async Task Can_Get_CamelCasedModels_ById() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = $"/camelCasedModels/{model.Id}"; + var route = $"api/v1/camelCasedModels/{model.Id}"; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); @@ -100,7 +100,7 @@ public async Task Can_Post_CamelCasedModels() } }; var httpMethod = new HttpMethod("POST"); - var route = $"/camelCasedModels"; + var route = $"api/v1/camelCasedModels"; var builder = new WebHostBuilder() .UseStartup(); var server = new TestServer(builder); @@ -144,7 +144,7 @@ public async Task Can_Patch_CamelCasedModels() } }; var httpMethod = new HttpMethod("PATCH"); - var route = $"/camelCasedModels/{model.Id}"; + var route = $"api/v1/camelCasedModels/{model.Id}"; var builder = new WebHostBuilder().UseStartup(); var server = new TestServer(builder); var client = server.CreateClient(); diff --git a/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs b/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs new file mode 100644 index 0000000000..a78bce2d3c --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs @@ -0,0 +1,26 @@ +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Services; +using JsonApiDotNetCoreExample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System; +using UnitTests; + +namespace JsonApiDotNetCoreExampleTests +{ + public class CamelCaseTestStartup : Startup + { + public CamelCaseTestStartup(IHostingEnvironment env) : base(env) + { } + + public override IServiceProvider ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + base.ConfigureServices(services); + services.AddClientSerialization(); + services.AddScoped(); + return services.BuildServiceProvider(); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/TestStartup.cs b/test/JsonApiDotNetCoreExampleTests/TestStartup.cs index 730d5f653b..1ea3c03f55 100644 --- a/test/JsonApiDotNetCoreExampleTests/TestStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/TestStartup.cs @@ -21,4 +21,4 @@ public override IServiceProvider ConfigureServices(IServiceCollection services) return services.BuildServiceProvider(); } } -} +} \ No newline at end of file From 0080a98a27475b75352b6146b51a3e6260904b36 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 15:45:45 +0200 Subject: [PATCH 04/13] feat: kebab vs camel completed --- .../Controllers/CamelCasedModelsController.cs | 1 - .../IApplicationBuilderExtensions.cs | 2 -- .../IServiceCollectionExtensions.cs | 23 ++++++++++++------- .../Server/Builders/LinkBuilder.cs | 4 ++-- .../IServiceCollectionExtensionsTests.cs | 1 - 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs index 20571166ad..c4ce48fc75 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Services; using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCoreExample.Controllers diff --git a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs index 3ebe17467d..e7cbcce43d 100644 --- a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs @@ -24,9 +24,7 @@ public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool app.UseMiddleware(); if (useMvc) - { app.UseMvc(); - } using (var scope = app.ApplicationServices.CreateScope()) { diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 0272829351..97ca6538f1 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -24,12 +24,11 @@ using JsonApiDotNetCore.Serialization.Server.Builders; using JsonApiDotNetCore.Serialization.Server; using JsonApiDotNetCore.Serialization.Client; -using JsonApiDotNetCore.Controllers; -using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.Extensions.DependencyInjection.Extensions; namespace JsonApiDotNetCore.Extensions { + // ReSharper disable once InconsistentNaming public static class IServiceCollectionExtensions { @@ -53,9 +52,11 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se IMvcCoreBuilder mvcBuilder = null) where TContext : DbContext { + + mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + var options = new JsonApiOptions(); // add basic Mvc functionality - mvcBuilder = mvcBuilder ?? services.AddMvcCore(); // set standard options configureAction(options); @@ -68,6 +69,11 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se // add JsonApi fitlers and serializer mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); + // register services that allow user to override behaviour that is configured on startup, like routing conventions + AddStartupConfigurationServices(services, options); + var intermediateProvider = services.BuildServiceProvider(); + mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); + // register services AddJsonApiInternals(services, options); return services; @@ -91,6 +97,11 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, // add JsonApi fitlers and serializer mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); + // register services that allow user to override behaviour that is configured on startup, like routing conventions + AddStartupConfigurationServices(services, options); + var intermediateProvider = services.BuildServiceProvider(); + mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); + // register services AddJsonApiInternals(services, options); return services; @@ -119,21 +130,17 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, // add JsonApi filters and serializers mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); - // register services that allow user to override behaviour that is configured on startup + // register services that allow user to override behaviour that is configured on startup, like routing conventions AddStartupConfigurationServices(services, options); - var intermediateProvider = services.BuildServiceProvider(); mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); - //intermediateProvider.Dispose(); // register services AddJsonApiInternals(services, options); - return services; } - private static void AddMvcOptions(MvcOptions options, JsonApiOptions config) { options.Filters.Add(typeof(JsonApiExceptionFilter)); diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs index 8d271d5327..fe2badc807 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -10,9 +10,9 @@ namespace JsonApiDotNetCore.Serialization.Server.Builders { public class LinkBuilder : ILinkBuilder { - private readonly ICurrentRequest _currentRequest; - private readonly ILinksConfiguration _options; private readonly IContextEntityProvider _provider; + private readonly ILinksConfiguration _options; + private readonly ICurrentRequest _currentRequest; private readonly IPageService _pageService; public LinkBuilder(ILinksConfiguration options, diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index a3d8801293..86f3233901 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -49,7 +49,6 @@ public void AddJsonApiInternals_Adds_All_Required_Services() Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService(typeof(IEntityRepository))); - Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService>()); From de428445b7ab1d3d08fc4d272ffb55ecbce08731 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 15:50:45 +0200 Subject: [PATCH 05/13] chore: spacing --- .../Extensions/IServiceCollectionExtensions.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 97ca6538f1..c239e4e4a2 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -28,7 +28,6 @@ namespace JsonApiDotNetCore.Extensions { - // ReSharper disable once InconsistentNaming public static class IServiceCollectionExtensions { @@ -160,6 +159,9 @@ public static void AddJsonApiInternals( AddJsonApiInternals(services, jsonApiOptions); } + /// + /// Adds services to the container that need to be retrieved with an intermediate provider during Startup. + /// private static void AddStartupConfigurationServices(this IServiceCollection services, JsonApiOptions jsonApiOptions) { services.AddSingleton(jsonApiOptions); @@ -211,8 +213,7 @@ public static void AddJsonApiInternals( services.AddSingleton(); services.AddSingleton(graph); services.AddScoped(); - services.AddScoped(); - //services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); From 521d435615d71d879237a01b98dce005138c223c Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 15:51:53 +0200 Subject: [PATCH 06/13] style: spacing --- src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index c239e4e4a2..4597b3ff40 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -296,7 +296,6 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js { options.InputFormatters.Insert(0, new JsonApiInputFormatter()); options.OutputFormatters.Insert(0, new JsonApiOutputFormatter()); - //options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace)); } /// From 5cd83c43333d4179b6f43532cbe6ba17ea7b2b3e Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 16:02:03 +0200 Subject: [PATCH 07/13] refactor: share logic between resource name formatter implementations --- .../Builders/ResourceGraphBuilder.cs | 2 +- .../Configuration/JsonApiOptions.cs | 11 +-- .../IServiceCollectionExtensions.cs | 2 +- .../Graph/KebabCaseResourceNameFormatter.cs | 71 ------------------- .../CamelCaseFormatter.cs | 24 +++++++ .../IResourceNameFormatter.cs | 0 .../KebabCaseFormatter.cs | 23 ++++++ .../ResourceNameFormatterBase.cs} | 15 ++-- .../JsonApiDotNetCore.csproj | 3 + .../CamelCaseTestStartup.cs | 2 +- .../Builders/ContextGraphBuilder_Tests.cs | 2 +- .../IServiceCollectionExtensionsTests.cs | 1 + 12 files changed, 63 insertions(+), 93 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs create mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs rename src/JsonApiDotNetCore/Graph/{ => ResourceNameFormatters}/IResourceNameFormatter.cs (100%) create mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs rename src/JsonApiDotNetCore/Graph/{CamelCaseResourceNameFormatter.cs => ResourceNameFormatters/ResourceNameFormatterBase.cs} (85%) diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index 61fed971c9..21b3058233 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -26,7 +26,7 @@ public class ResourceGraphBuilder : IResourceGraphBuilder public ResourceGraphBuilder(IResourceNameFormatter formatter = null) { - _resourceNameFormatter = formatter ?? new KebabCaseResourceNameFormatter(); + _resourceNameFormatter = formatter ?? new KebabCaseFormatter(); } /// diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 996ec796b5..f74d81a52c 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -28,19 +28,10 @@ public class JsonApiOptions : IJsonApiOptions /// public Link RelationshipLinks { get; set; } = Link.All; - - internal Type ResourceNameFormatterType { get; set; } = typeof(KebabCaseResourceNameFormatter); - - //public void UseResourceNameFormatter() where TFormatter : class, IResourceNameFormatter - //{ - // ResourceNameFormatterType = typeof(TFormatter); - //} - - /// /// Provides an interface for formatting resource names by convention /// - public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabCaseResourceNameFormatter(); + public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabCaseFormatter(); /// /// Provides an interface for formatting relationship id properties given the navigation property name diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 4597b3ff40..4783452053 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -165,7 +165,7 @@ public static void AddJsonApiInternals( private static void AddStartupConfigurationServices(this IServiceCollection services, JsonApiOptions jsonApiOptions) { services.AddSingleton(jsonApiOptions); - services.TryAddSingleton(new KebabCaseResourceNameFormatter()); + services.TryAddSingleton(new KebabCaseFormatter()); services.TryAddSingleton(); } diff --git a/src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs deleted file mode 100644 index 6660a1054b..0000000000 --- a/src/JsonApiDotNetCore/Graph/KebabCaseResourceNameFormatter.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Reflection; -using Humanizer; -using JsonApiDotNetCore.Models; -using str = JsonApiDotNetCore.Extensions.StringExtensions; - -namespace JsonApiDotNetCore.Graph -{ - public class KebabCaseResourceNameFormatter : IResourceNameFormatter - { - /// - /// Uses the internal type name to determine the external resource name. - /// By default we us Humanizer for pluralization and then we dasherize the name. - /// - /// - /// - /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todo-items" - /// - /// - public string FormatResourceName(Type type) - { - try - { - // check the class definition first - // [Resource("models"] public class Model : Identifiable { /* ... */ } - if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) - return attribute.ResourceName; - - return ApplyCasingConvention(type.Name.Pluralize()); - } - catch (InvalidOperationException e) - { - throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); - } - } - - /// - /// Aoplies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todo-items" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todo-item" - /// - /// - public string ApplyCasingConvention(string properName) => str.Dasherize(properName); - - /// - /// Uses the internal PropertyInfo to determine the external resource name. - /// By default the name will be formatted to kebab-case. - /// - /// - /// Given the following property: - /// - /// public string CompoundProperty { get; set; } - /// - /// The public attribute will be formatted like so: - /// - /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compound-property" - /// - /// - public string FormatPropertyName(PropertyInfo property) => str.Dasherize(property.Name); - } -} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs new file mode 100644 index 0000000000..0b857aff24 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs @@ -0,0 +1,24 @@ +using str = JsonApiDotNetCore.Extensions.StringExtensions; + +namespace JsonApiDotNetCore.Graph +{ + public class CamelCaseFormatter: BaseResourceNameFormatter + { + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todoItems" + ///Came + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todoItem" + /// + /// + public override string ApplyCasingConvention(string properName) => str.Camelize(properName); + } +} + diff --git a/src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs similarity index 100% rename from src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs rename to src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs new file mode 100644 index 0000000000..378b83c99a --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs @@ -0,0 +1,23 @@ +using str = JsonApiDotNetCore.Extensions.StringExtensions; + +namespace JsonApiDotNetCore.Graph +{ + public class KebabCaseFormatter : BaseResourceNameFormatter + { + /// + /// Aoplies the desired casing convention to the internal string. + /// This is generally applied to the type name after pluralization. + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todo-items" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todo-item" + /// + /// + public override string ApplyCasingConvention(string properName) => str.Dasherize(properName); + } +} diff --git a/src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs similarity index 85% rename from src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs rename to src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs index d36f1fb28f..5ab7e04938 100644 --- a/src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs @@ -2,11 +2,10 @@ using System.Reflection; using Humanizer; using JsonApiDotNetCore.Models; -using str = JsonApiDotNetCore.Extensions.StringExtensions; namespace JsonApiDotNetCore.Graph { - public class CamelCaseResourceNameFormatter : IResourceNameFormatter + public abstract class BaseResourceNameFormatter : IResourceNameFormatter { /// /// Uses the internal type name to determine the external resource name. @@ -15,7 +14,7 @@ public class CamelCaseResourceNameFormatter : IResourceNameFormatter /// /// /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todo-items" + /// // > "todoItems" /// /// public string FormatResourceName(Type type) @@ -43,13 +42,13 @@ public string FormatResourceName(Type type) /// /// /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todo-items" + /// // > "todoItems" /// /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todo-item" + /// // > "todoItem" /// /// - public string ApplyCasingConvention(string properName) => str.Camelize(properName); + public abstract string ApplyCasingConvention(string properName); /// /// Uses the internal PropertyInfo to determine the external resource name. @@ -63,9 +62,9 @@ public string FormatResourceName(Type type) /// The public attribute will be formatted like so: /// /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compound-property" + /// // > "compoundProperty" /// /// - public string FormatPropertyName(PropertyInfo property) => str.Camelize(property.Name); + public string FormatPropertyName(PropertyInfo property) => ApplyCasingConvention(property.Name); } } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 1ae5427196..00ecf71759 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -44,4 +44,7 @@ + + + diff --git a/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs b/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs index a78bce2d3c..3a092e5e1a 100644 --- a/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs +++ b/test/JsonApiDotNetCoreExampleTests/CamelCaseTestStartup.cs @@ -16,7 +16,7 @@ public CamelCaseTestStartup(IHostingEnvironment env) : base(env) public override IServiceProvider ConfigureServices(IServiceCollection services) { - services.AddSingleton(); + services.AddSingleton(); base.ConfigureServices(services); services.AddClientSerialization(); services.AddScoped(); diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index 791c495f2c..725b397e27 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -27,7 +27,7 @@ class TestContext : DbContext public ResourceGraphBuilder_Tests() { - JsonApiOptions.ResourceNameFormatter = new KebabCaseResourceNameFormatter(); + JsonApiOptions.ResourceNameFormatter = new KebabCaseFormatter(); } [Fact] diff --git a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 86f3233901..c0bfc9aee0 100644 --- a/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -30,6 +30,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() // arrange var services = new ServiceCollection(); var jsonApiOptions = new JsonApiOptions(); + services.AddSingleton(jsonApiOptions); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb"), ServiceLifetime.Transient); services.AddScoped>(); From 3e3e2594322c92e4302f24069bede70427b6e154 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 16:12:01 +0200 Subject: [PATCH 08/13] docs: resource formatter comments --- .../CamelCaseFormatter.cs | 44 +++++++++++++------ .../KebabCaseFormatter.cs | 44 +++++++++++++------ .../ResourceNameFormatterBase.cs | 28 ------------ 3 files changed, 60 insertions(+), 56 deletions(-) diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs index 0b857aff24..7e9a4d1e27 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs @@ -2,22 +2,38 @@ namespace JsonApiDotNetCore.Graph { + /// + /// Uses kebab-case as formatting options in the route and request/response body. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todoItems" + /// + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compoundProperty" + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todoItems" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todoItem" + /// + /// public class CamelCaseFormatter: BaseResourceNameFormatter { - /// - /// Aoplies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todoItems" - ///Came - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todoItem" - /// - /// + /// public override string ApplyCasingConvention(string properName) => str.Camelize(properName); } } diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs index 378b83c99a..22144a4769 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs @@ -2,22 +2,38 @@ namespace JsonApiDotNetCore.Graph { + /// + /// Uses kebab-case as formatting options in the route and request/response body. + /// + /// + /// + /// _default.FormatResourceName(typeof(TodoItem)).Dump(); + /// // > "todo-items" + /// + /// + /// + /// Given the following property: + /// + /// public string CompoundProperty { get; set; } + /// + /// The public attribute will be formatted like so: + /// + /// _default.FormatPropertyName(compoundProperty).Dump(); + /// // > "compound-property" + /// + /// + /// + /// + /// _default.ApplyCasingConvention("TodoItems"); + /// // > "todo-items" + /// + /// _default.ApplyCasingConvention("TodoItem"); + /// // > "todo-item" + /// + /// public class KebabCaseFormatter : BaseResourceNameFormatter { - /// - /// Aoplies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todo-items" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todo-item" - /// - /// + /// public override string ApplyCasingConvention(string properName) => str.Dasherize(properName); } } diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs index 5ab7e04938..319824041d 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs @@ -9,14 +9,7 @@ public abstract class BaseResourceNameFormatter : IResourceNameFormatter { /// /// Uses the internal type name to determine the external resource name. - /// By default we us Humanizer for pluralization and then we dasherize the name. /// - /// - /// - /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todoItems" - /// - /// public string FormatResourceName(Type type) { try @@ -38,33 +31,12 @@ public string FormatResourceName(Type type) /// Aoplies the desired casing convention to the internal string. /// This is generally applied to the type name after pluralization. /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todoItems" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todoItem" - /// - /// public abstract string ApplyCasingConvention(string properName); /// /// Uses the internal PropertyInfo to determine the external resource name. /// By default the name will be formatted to kebab-case. /// - /// - /// Given the following property: - /// - /// public string CompoundProperty { get; set; } - /// - /// The public attribute will be formatted like so: - /// - /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compoundProperty" - /// - /// public string FormatPropertyName(PropertyInfo property) => ApplyCasingConvention(property.Name); } } From db683044676d6c1a3f1e8ffb96b26ccde17ef7bd Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 16:18:12 +0200 Subject: [PATCH 09/13] docs: comments improved --- .../Internal/DefaultRoutingConvention.cs | 22 +++++++++++++++++-- .../Internal/IJsonApiRoutingConvention.cs | 12 +++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs index e0bb8c93ff..b508bba7c8 100644 --- a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs @@ -14,10 +14,11 @@ namespace JsonApiDotNetCore.Internal /// /// The default routing convention registers the name of the resource as the route /// using the that is registered. The default for this is - /// a kebab-case formatter. + /// a kebab-case formatter. If the controller is directly inherits from JsonApiMixin and there is no + /// resource directly associated, it used the name of the controller instead of the name of the type. /// /// - /// public class SomeResourceController{SomeResource} : JsonApiController{SomeResource} { } + /// public class SomeResourceController: JsonApiController{SomeResource} { } /// // => /some-resources/relationship/related-resource /// /// public class RandomNameController{SomeResource} : JsonApiController{SomeResource} { } @@ -26,6 +27,10 @@ namespace JsonApiDotNetCore.Internal /// // when using the camelCase formatter: /// public class SomeResourceController{SomeResource} : JsonApiController{SomeResource} { } /// // => /someResources/relationship/relatedResource + /// + /// // when inheriting from JsonApiMixin formatter: + /// public class SomeVeryCustomController{SomeResource} : JsonApiMixin { } + /// // => /some-very-customs/relationship/related-resource /// public class DefaultRoutingConvention : IJsonApiRoutingConvention { @@ -38,6 +43,7 @@ public DefaultRoutingConvention(IJsonApiOptions options, IResourceNameFormatter _formatter = formatter; } + /// public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) @@ -53,6 +59,9 @@ public void Apply(ApplicationModel application) } } + /// + /// verifies if routing convention should be enabled for this controller + /// private bool RoutingConventionDisabled(ControllerModel controller) { var type = controller.ControllerType; @@ -60,6 +69,9 @@ private bool RoutingConventionDisabled(ControllerModel controller) return notDisabled && type.IsSubclassOf(typeof(JsonApiControllerMixin)); } + /// + /// Derives a template from the resource type, and checks if this template was already registered. + /// private string TemplateFromResource(ControllerModel model) { var resourceType = GetResourceTypeFromController(model.ControllerType); @@ -72,6 +84,9 @@ private string TemplateFromResource(ControllerModel model) return null; } + /// + /// Derives a template from the controller name, and checks if this template was already registered. + /// private string TemplateFromController(ControllerModel model) { var template = $"{_namespace}/{_formatter.ApplyCasingConvention(model.ControllerName)}"; @@ -80,6 +95,9 @@ private string TemplateFromController(ControllerModel model) return null; } + /// + /// Determines the resource associated to a controller by inspecting generic arguments. + /// private Type GetResourceTypeFromController(Type type) { var target = typeof(BaseJsonApiController<,>); diff --git a/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs index 9cae89fe10..aba03b806b 100644 --- a/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/IJsonApiRoutingConvention.cs @@ -1,10 +1,10 @@ -// REF: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.CustomRoutingConvention/NameSpaceRoutingConvention.cs -// REF: https://github.com/aspnet/Mvc/issues/5691 -using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ApplicationModels; namespace JsonApiDotNetCore.Internal { - public interface IJsonApiRoutingConvention : IApplicationModelConvention - { - } + /// + /// Service for specifying which routing convention to use. This can be overriden to customize + /// the relation between controllers and mapped routes. + /// + public interface IJsonApiRoutingConvention : IApplicationModelConvention { } } From e62604d44110c4542d1514197df7669812a6c1d7 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 16:31:58 +0200 Subject: [PATCH 10/13] chore: remove static reference to ResourceNameFormatter --- src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs | 5 ----- src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs | 2 +- test/UnitTests/Builders/ContextGraphBuilder_Tests.cs | 5 ----- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index f74d81a52c..631f7e2b63 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -28,11 +28,6 @@ public class JsonApiOptions : IJsonApiOptions /// public Link RelationshipLinks { get; set; } = Link.All; - /// - /// Provides an interface for formatting resource names by convention - /// - public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabCaseFormatter(); - /// /// Provides an interface for formatting relationship id properties given the navigation property name /// diff --git a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs index e12d01909b..468537ec1a 100644 --- a/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Graph/ServiceDiscoveryFacade.cs @@ -179,7 +179,7 @@ private void AddResourceToGraph(ResourceDescriptor identifiable) } private string FormatResourceName(Type resourceType) - => JsonApiOptions.ResourceNameFormatter.FormatResourceName(resourceType); + => new KebabCaseFormatter().FormatResourceName(resourceType); /// /// Add implementations to container. diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index 725b397e27..c3a02ec4e4 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -25,11 +25,6 @@ class TestContext : DbContext public DbSet DbResources { get; set; } } - public ResourceGraphBuilder_Tests() - { - JsonApiOptions.ResourceNameFormatter = new KebabCaseFormatter(); - } - [Fact] public void Can_Build_ResourceGraph_Using_Builder() { From 70e1a9e6d353d9388d336d57bf76e52f05d5440c Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 16:59:46 +0200 Subject: [PATCH 11/13] chore: reviewer fixes --- .../IServiceCollectionExtensions.cs | 34 ++++++++----------- .../Internal/DefaultRoutingConvention.cs | 2 +- .../Builders/ContextGraphBuilder_Tests.cs | 2 -- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 4783452053..d1dd11c584 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -53,8 +53,8 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se { mvcBuilder = mvcBuilder ?? services.AddMvcCore(); - var options = new JsonApiOptions(); + // add basic Mvc functionality // set standard options configureAction(options); @@ -65,13 +65,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se // build the resource graph using ef core DbContext options.BuildResourceGraph(builder => builder.AddDbContext()); - // add JsonApi fitlers and serializer - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); - - // register services that allow user to override behaviour that is configured on startup, like routing conventions - AddStartupConfigurationServices(services, options); - var intermediateProvider = services.BuildServiceProvider(); - mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); + ConfigureMvc(services, mvcBuilder, options); // register services AddJsonApiInternals(services, options); @@ -93,13 +87,7 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, mvcBuilder = mvcBuilder ?? services.AddMvcCore(); configureOptions(options); - // add JsonApi fitlers and serializer - mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); - - // register services that allow user to override behaviour that is configured on startup, like routing conventions - AddStartupConfigurationServices(services, options); - var intermediateProvider = services.BuildServiceProvider(); - mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); + ConfigureMvc(services, mvcBuilder, options); // register services AddJsonApiInternals(services, options); @@ -126,6 +114,17 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, var facade = new ServiceDiscoveryFacade(services, options.ResourceGraphBuilder); autoDiscover(facade); + ConfigureMvc(services, mvcBuilder, options); + + // register services + AddJsonApiInternals(services, options); + + return services; + } + + private static void ConfigureMvc(IServiceCollection services, IMvcCoreBuilder mvcBuilder, JsonApiOptions options) + { + // add JsonApi filters and serializers mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); @@ -133,11 +132,6 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, AddStartupConfigurationServices(services, options); var intermediateProvider = services.BuildServiceProvider(); mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService())); - - // register services - AddJsonApiInternals(services, options); - - return services; } private static void AddMvcOptions(MvcOptions options, JsonApiOptions config) diff --git a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs index b508bba7c8..3335ca87aa 100644 --- a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs @@ -14,7 +14,7 @@ namespace JsonApiDotNetCore.Internal /// /// The default routing convention registers the name of the resource as the route /// using the that is registered. The default for this is - /// a kebab-case formatter. If the controller is directly inherits from JsonApiMixin and there is no + /// a kebab-case formatter. If the controller directly inherits from JsonApiMixin and there is no /// resource directly associated, it used the name of the controller instead of the name of the type. /// /// diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index c3a02ec4e4..bb2e2173fc 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -4,10 +4,8 @@ using System.Reflection; using Humanizer; using JsonApiDotNetCore.Builders; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; -using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.EntityFrameworkCore; From e7a731b33d71636b827c559ae4e4be31969dfca6 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 17:06:48 +0200 Subject: [PATCH 12/13] style: removed spacing --- .../IServiceCollectionExtensions.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index d1dd11c584..0d00b74ff6 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -47,26 +47,20 @@ public static IServiceCollection AddJsonApi(this IServiceCollection se /// /// public static IServiceCollection AddJsonApi(this IServiceCollection services, - Action configureAction, + Action configureOptions, IMvcCoreBuilder mvcBuilder = null) where TContext : DbContext { - - mvcBuilder = mvcBuilder ?? services.AddMvcCore(); var options = new JsonApiOptions(); - // add basic Mvc functionality - // set standard options - configureAction(options); - + mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + // configures JsonApiOptions; + configureOptions(options); // ResourceGraphBuilder should not be exposed on JsonApiOptions. // Instead, ResourceGraphBuilder should consume JsonApiOptions - // build the resource graph using ef core DbContext options.BuildResourceGraph(builder => builder.AddDbContext()); - ConfigureMvc(services, mvcBuilder, options); - // register services AddJsonApiInternals(services, options); return services; @@ -84,11 +78,11 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, IMvcCoreBuilder mvcBuilder = null) { var options = new JsonApiOptions(); + // add basic Mvc functionality mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + // configures JsonApiOptions; configureOptions(options); - ConfigureMvc(services, mvcBuilder, options); - // register services AddJsonApiInternals(services, options); return services; @@ -107,18 +101,16 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, IMvcCoreBuilder mvcBuilder = null) { var options = new JsonApiOptions(); + // add basic Mvc functionality mvcBuilder = mvcBuilder ?? services.AddMvcCore(); + // configures JsonApiOptions; configureOptions(options); - // build the resource graph using auto discovery. var facade = new ServiceDiscoveryFacade(services, options.ResourceGraphBuilder); autoDiscover(facade); - ConfigureMvc(services, mvcBuilder, options); - // register services AddJsonApiInternals(services, options); - return services; } From c1e70aff3bd78126f317be9235d83949bbbdcaf1 Mon Sep 17 00:00:00 2001 From: Maurits Moeys Date: Thu, 17 Oct 2019 17:09:12 +0200 Subject: [PATCH 13/13] style: removed spacing --- .../Extensions/IServiceCollectionExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 0d00b74ff6..85d82abb32 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -116,10 +116,8 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, private static void ConfigureMvc(IServiceCollection services, IMvcCoreBuilder mvcBuilder, JsonApiOptions options) { - // add JsonApi filters and serializers mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options)); - // register services that allow user to override behaviour that is configured on startup, like routing conventions AddStartupConfigurationServices(services, options); var intermediateProvider = services.BuildServiceProvider();