Skip to content

Commit a94f318

Browse files
authored
Allow for multiple naming conventions (camelCase vs kebab-case) (#581)
* feat: draft of DI-registered ApplicationModelConvention * feat: injectable IResourceNameFormatter and IJsonApiRoutingCOnvention now working * test: kebab vs camelcase configuration * feat: kebab vs camel completed * chore: spacing * style: spacing * refactor: share logic between resource name formatter implementations * docs: resource formatter comments * docs: comments improved * chore: remove static reference to ResourceNameFormatter * chore: reviewer fixes * style: removed spacing * style: removed spacing
1 parent 4c067db commit a94f318

23 files changed

+356
-242
lines changed

src/Examples/JsonApiDotNetCoreExample/Controllers/CamelCasedModelsController.cs

-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
using JsonApiDotNetCore.Internal.Contracts;
44
using JsonApiDotNetCore.Services;
55
using JsonApiDotNetCoreExample.Models;
6-
using Microsoft.AspNetCore.Mvc;
76
using Microsoft.Extensions.Logging;
87

98
namespace JsonApiDotNetCoreExample.Controllers
109
{
11-
[Route("[controller]")]
12-
[DisableRoutingConvention]
1310
public class CamelCasedModelsController : JsonApiController<CamelCasedModel>
1411
{
1512
public CamelCasedModelsController(

src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class ResourceGraphBuilder : IResourceGraphBuilder
2626

2727
public ResourceGraphBuilder(IResourceNameFormatter formatter = null)
2828
{
29-
_resourceNameFormatter = formatter ?? new DefaultResourceNameFormatter();
29+
_resourceNameFormatter = formatter ?? new KebabCaseFormatter();
3030
}
3131

3232
/// <inheritdoc />
@@ -35,7 +35,7 @@ public IResourceGraph Build()
3535
_entities.ForEach(SetResourceLinksOptions);
3636

3737
List<ControllerResourceMap> controllerContexts = new List<ControllerResourceMap>() { };
38-
foreach(var cm in _controllerMapper)
38+
foreach (var cm in _controllerMapper)
3939
{
4040
var model = cm.Key;
4141
foreach (var controller in cm.Value)
@@ -180,7 +180,7 @@ protected virtual List<RelationshipAttribute> GetRelationships(Type entityType)
180180
// Article → ArticleTag.Tag
181181
hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.DependentType)
182182
?? throw new JsonApiSetupException($"{hasManyThroughAttribute.ThroughType} does not contain a navigation property to type {hasManyThroughAttribute.DependentType}");
183-
183+
184184
// ArticleTag.TagId
185185
var rightIdPropertyName = JsonApiOptions.RelatedIdMapper.GetRelatedIdPropertyName(hasManyThroughAttribute.RightProperty.Name);
186186
hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName)

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
namespace JsonApiDotNetCore.Configuration
1212
{
13-
1413
/// <summary>
1514
/// Global options
1615
/// </summary>
@@ -29,12 +28,6 @@ public class JsonApiOptions : IJsonApiOptions
2928
/// <inheritdoc/>
3029
public Link RelationshipLinks { get; set; } = Link.All;
3130

32-
33-
/// <summary>
34-
/// Provides an interface for formatting resource names by convention
35-
/// </summary>
36-
public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new DefaultResourceNameFormatter();
37-
3831
/// <summary>
3932
/// Provides an interface for formatting relationship id properties given the navigation property name
4033
/// </summary>

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99

1010
namespace JsonApiDotNetCore.Controllers
1111
{
12-
13-
1412
public class JsonApiController<T, TId> : BaseJsonApiController<T, TId> where T : class, IIdentifiable<TId>
1513
{
1614
/// <summary>

src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs

-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool
2424
app.UseMiddleware<CurrentRequestMiddleware>();
2525

2626
if (useMvc)
27-
{
2827
app.UseMvc();
29-
}
3028

3129
using (var scope = app.ApplicationServices.CreateScope())
3230
{

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

+34-28
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@
2424
using JsonApiDotNetCore.Serialization.Server.Builders;
2525
using JsonApiDotNetCore.Serialization.Server;
2626
using JsonApiDotNetCore.Serialization.Client;
27-
using JsonApiDotNetCore.Controllers;
27+
using Microsoft.Extensions.DependencyInjection.Extensions;
2828

2929
namespace JsonApiDotNetCore.Extensions
3030
{
3131
// ReSharper disable once InconsistentNaming
3232
public static class IServiceCollectionExtensions
3333
{
3434
static private readonly Action<JsonApiOptions> _noopConfig = opt => { };
35-
static private JsonApiOptions _options { get { return new JsonApiOptions(); } }
3635
public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection services,
3736
IMvcCoreBuilder mvcBuilder = null)
3837
where TContext : DbContext
@@ -48,25 +47,20 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
4847
/// <param name="configureAction"></param>
4948
/// <returns></returns>
5049
public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection services,
51-
Action<JsonApiOptions> configureAction,
50+
Action<JsonApiOptions> configureOptions,
5251
IMvcCoreBuilder mvcBuilder = null)
5352
where TContext : DbContext
5453
{
55-
var options = _options;
54+
var options = new JsonApiOptions();
5655
// add basic Mvc functionality
5756
mvcBuilder = mvcBuilder ?? services.AddMvcCore();
58-
// set standard options
59-
configureAction(options);
60-
57+
// configures JsonApiOptions;
58+
configureOptions(options);
6159
// ResourceGraphBuilder should not be exposed on JsonApiOptions.
6260
// Instead, ResourceGraphBuilder should consume JsonApiOptions
63-
6461
// build the resource graph using ef core DbContext
6562
options.BuildResourceGraph(builder => builder.AddDbContext<TContext>());
66-
67-
// add JsonApi fitlers and serializer
68-
mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options));
69-
63+
ConfigureMvc(services, mvcBuilder, options);
7064
// register services
7165
AddJsonApiInternals<TContext>(services, options);
7266
return services;
@@ -83,13 +77,12 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services,
8377
Action<JsonApiOptions> configureOptions,
8478
IMvcCoreBuilder mvcBuilder = null)
8579
{
86-
var options = _options;
80+
var options = new JsonApiOptions();
81+
// add basic Mvc functionality
8782
mvcBuilder = mvcBuilder ?? services.AddMvcCore();
83+
// configures JsonApiOptions;
8884
configureOptions(options);
89-
90-
// add JsonApi fitlers and serializer
91-
mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options));
92-
85+
ConfigureMvc(services, mvcBuilder, options);
9386
// register services
9487
AddJsonApiInternals(services, options);
9588
return services;
@@ -107,22 +100,29 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services,
107100
Action<ServiceDiscoveryFacade> autoDiscover,
108101
IMvcCoreBuilder mvcBuilder = null)
109102
{
110-
var options = _options;
103+
var options = new JsonApiOptions();
104+
// add basic Mvc functionality
111105
mvcBuilder = mvcBuilder ?? services.AddMvcCore();
106+
// configures JsonApiOptions;
112107
configureOptions(options);
113-
114108
// build the resource graph using auto discovery.
115109
var facade = new ServiceDiscoveryFacade(services, options.ResourceGraphBuilder);
116110
autoDiscover(facade);
117-
118-
// add JsonApi fitlers and serializer
119-
mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options));
120-
111+
ConfigureMvc(services, mvcBuilder, options);
121112
// register services
122113
AddJsonApiInternals(services, options);
123114
return services;
124115
}
125116

117+
private static void ConfigureMvc(IServiceCollection services, IMvcCoreBuilder mvcBuilder, JsonApiOptions options)
118+
{
119+
// add JsonApi filters and serializers
120+
mvcBuilder.AddMvcOptions(opt => AddMvcOptions(opt, options));
121+
// register services that allow user to override behaviour that is configured on startup, like routing conventions
122+
AddStartupConfigurationServices(services, options);
123+
var intermediateProvider = services.BuildServiceProvider();
124+
mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService<IJsonApiRoutingConvention>()));
125+
}
126126

127127
private static void AddMvcOptions(MvcOptions options, JsonApiOptions config)
128128
{
@@ -143,11 +143,20 @@ public static void AddJsonApiInternals<TContext>(
143143
AddJsonApiInternals(services, jsonApiOptions);
144144
}
145145

146+
/// <summary>
147+
/// Adds services to the container that need to be retrieved with an intermediate provider during Startup.
148+
/// </summary>
149+
private static void AddStartupConfigurationServices(this IServiceCollection services, JsonApiOptions jsonApiOptions)
150+
{
151+
services.AddSingleton<IJsonApiOptions>(jsonApiOptions);
152+
services.TryAddSingleton<IResourceNameFormatter>(new KebabCaseFormatter());
153+
services.TryAddSingleton<IJsonApiRoutingConvention, DefaultRoutingConvention>();
154+
}
155+
146156
public static void AddJsonApiInternals(
147157
this IServiceCollection services,
148158
JsonApiOptions jsonApiOptions)
149159
{
150-
151160
var graph = jsonApiOptions.ResourceGraph ?? jsonApiOptions.ResourceGraphBuilder.Build();
152161

153162
if (graph.UsesDbContext == false)
@@ -183,14 +192,12 @@ public static void AddJsonApiInternals(
183192
services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>));
184193
services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>));
185194

186-
services.AddSingleton<IJsonApiOptions>(jsonApiOptions);
187195
services.AddSingleton<ILinksConfiguration>(jsonApiOptions);
188196
services.AddSingleton(graph);
189197
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
190198
services.AddSingleton<IContextEntityProvider>(graph);
191199
services.AddScoped<ICurrentRequest, CurrentRequest>();
192-
services.AddScoped<IScopedServiceProvider, RequestScopedServiceProvider>();
193-
services.AddScoped<JsonApiRouteHandler>();
200+
services.AddScoped<IScopedServiceProvider, RequestScopedServiceProvider>();
194201
services.AddScoped<IJsonApiWriter, JsonApiWriter>();
195202
services.AddScoped<IJsonApiReader, JsonApiReader>();
196203
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
@@ -273,7 +280,6 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js
273280
{
274281
options.InputFormatters.Insert(0, new JsonApiInputFormatter());
275282
options.OutputFormatters.Insert(0, new JsonApiOutputFormatter());
276-
options.Conventions.Insert(0, new DasherizedRoutingConvention(jsonApiOptions.Namespace));
277283
}
278284

279285
/// <summary>

src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs

-95
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using str = JsonApiDotNetCore.Extensions.StringExtensions;
2+
3+
namespace JsonApiDotNetCore.Graph
4+
{
5+
/// <summary>
6+
/// Uses kebab-case as formatting options in the route and request/response body.
7+
/// </summary>
8+
/// <example>
9+
/// <code>
10+
/// _default.FormatResourceName(typeof(TodoItem)).Dump();
11+
/// // > "todoItems"
12+
/// </code>
13+
/// </example>
14+
/// <example>
15+
/// Given the following property:
16+
/// <code>
17+
/// public string CompoundProperty { get; set; }
18+
/// </code>
19+
/// The public attribute will be formatted like so:
20+
/// <code>
21+
/// _default.FormatPropertyName(compoundProperty).Dump();
22+
/// // > "compoundProperty"
23+
/// </code>
24+
/// </example>
25+
/// <example>
26+
/// <code>
27+
/// _default.ApplyCasingConvention("TodoItems");
28+
/// // > "todoItems"
29+
///
30+
/// _default.ApplyCasingConvention("TodoItem");
31+
/// // > "todoItem"
32+
/// </code>
33+
/// </example>
34+
public class CamelCaseFormatter: BaseResourceNameFormatter
35+
{
36+
/// <inheritdoc/>
37+
public override string ApplyCasingConvention(string properName) => str.Camelize(properName);
38+
}
39+
}
40+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace JsonApiDotNetCore.Graph
5+
{
6+
/// <summary>
7+
/// Provides an interface for formatting resource names by convention
8+
/// </summary>
9+
public interface IResourceNameFormatter
10+
{
11+
/// <summary>
12+
/// Get the publicly visible resource name from the internal type name
13+
/// </summary>
14+
string FormatResourceName(Type resourceType);
15+
16+
/// <summary>
17+
/// Get the publicly visible name for the given property
18+
/// </summary>
19+
string FormatPropertyName(PropertyInfo property);
20+
21+
/// <summary>
22+
/// Aoplies the desired casing convention to the internal string.
23+
/// This is generally applied to the type name after pluralization.
24+
/// </summary>
25+
string ApplyCasingConvention(string properName);
26+
}
27+
}

0 commit comments

Comments
 (0)