Skip to content

Allow for multiple naming conventions (camelCase vs kebab-case) #581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 17, 2019
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCoreExample.Controllers
{
[Route("[controller]")]
[DisableRoutingConvention]
public class CamelCasedModelsController : JsonApiController<CamelCasedModel>
{
public CamelCasedModelsController(
Expand Down
6 changes: 3 additions & 3 deletions src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class ResourceGraphBuilder : IResourceGraphBuilder

public ResourceGraphBuilder(IResourceNameFormatter formatter = null)
{
_resourceNameFormatter = formatter ?? new DefaultResourceNameFormatter();
_resourceNameFormatter = formatter ?? new KebabCaseResourceNameFormatter();
}

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

List<ControllerResourceMap> controllerContexts = new List<ControllerResourceMap>() { };
foreach(var cm in _controllerMapper)
foreach (var cm in _controllerMapper)
{
var model = cm.Key;
foreach (var controller in cm.Value)
Expand Down Expand Up @@ -180,7 +180,7 @@ protected virtual List<RelationshipAttribute> 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)
Expand Down
11 changes: 9 additions & 2 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

namespace JsonApiDotNetCore.Configuration
{

/// <summary>
/// Global options
/// </summary>
Expand All @@ -30,10 +29,18 @@ public class JsonApiOptions : IJsonApiOptions
public Link RelationshipLinks { get; set; } = Link.All;


internal Type ResourceNameFormatterType { get; set; } = typeof(KebabCaseResourceNameFormatter);

//public void UseResourceNameFormatter<TFormatter>() where TFormatter : class, IResourceNameFormatter
//{
// ResourceNameFormatterType = typeof(TFormatter);
//}


/// <summary>
/// Provides an interface for formatting resource names by convention
/// </summary>
public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new DefaultResourceNameFormatter();
public static IResourceNameFormatter ResourceNameFormatter { get; set; } = new KebabCaseResourceNameFormatter();

/// <summary>
/// Provides an interface for formatting relationship id properties given the navigation property name
Expand Down
2 changes: 0 additions & 2 deletions src/JsonApiDotNetCore/Controllers/JsonApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@

namespace JsonApiDotNetCore.Controllers
{


public class JsonApiController<T, TId> : BaseJsonApiController<T, TId> where T : class, IIdentifiable<TId>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool
app.UseMiddleware<CurrentRequestMiddleware>();

if (useMvc)
{
app.UseMvc();
}

using (var scope = app.ApplicationServices.CreateScope())
{
Expand Down
49 changes: 36 additions & 13 deletions src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@
using JsonApiDotNetCore.Serialization.Server.Builders;
using JsonApiDotNetCore.Serialization.Server;
using JsonApiDotNetCore.Serialization.Client;
using JsonApiDotNetCore.Controllers;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace JsonApiDotNetCore.Extensions
{
// ReSharper disable once InconsistentNaming
public static class IServiceCollectionExtensions
{
static private readonly Action<JsonApiOptions> _noopConfig = opt => { };
static private JsonApiOptions _options { get { return new JsonApiOptions(); } }
public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection services,
IMvcCoreBuilder mvcBuilder = null)
where TContext : DbContext
Expand All @@ -52,9 +51,11 @@ public static IServiceCollection AddJsonApi<TContext>(this IServiceCollection se
IMvcCoreBuilder mvcBuilder = null)
where TContext : DbContext
{
var options = _options;
// add basic Mvc functionality

mvcBuilder = mvcBuilder ?? services.AddMvcCore();

var options = new JsonApiOptions();
// add basic Mvc functionality
// set standard options
configureAction(options);

Expand All @@ -67,6 +68,11 @@ public static IServiceCollection AddJsonApi<TContext>(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<IJsonApiRoutingConvention>()));

// register services
AddJsonApiInternals<TContext>(services, options);
return services;
Expand All @@ -83,13 +89,18 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services,
Action<JsonApiOptions> configureOptions,
IMvcCoreBuilder mvcBuilder = null)
{
var options = _options;
var options = new JsonApiOptions();
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<IJsonApiRoutingConvention>()));

// register services
AddJsonApiInternals(services, options);
return services;
Expand All @@ -107,23 +118,28 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services,
Action<ServiceDiscoveryFacade> autoDiscover,
IMvcCoreBuilder mvcBuilder = null)
{
var options = _options;
var options = new JsonApiOptions();
mvcBuilder = mvcBuilder ?? services.AddMvcCore();
configureOptions(options);

// build the resource graph using auto discovery.
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, like routing conventions
AddStartupConfigurationServices(services, options);
var intermediateProvider = services.BuildServiceProvider();
mvcBuilder.AddMvcOptions(opt => opt.Conventions.Insert(0, intermediateProvider.GetRequiredService<IJsonApiRoutingConvention>()));

// register services
AddJsonApiInternals(services, options);

return services;
}


private static void AddMvcOptions(MvcOptions options, JsonApiOptions config)
{
options.Filters.Add(typeof(JsonApiExceptionFilter));
Expand All @@ -143,11 +159,20 @@ public static void AddJsonApiInternals<TContext>(
AddJsonApiInternals(services, jsonApiOptions);
}

/// <summary>
/// Adds services to the container that need to be retrieved with an intermediate provider during Startup.
/// </summary>
private static void AddStartupConfigurationServices(this IServiceCollection services, JsonApiOptions jsonApiOptions)
{
services.AddSingleton<IJsonApiOptions>(jsonApiOptions);
services.TryAddSingleton<IResourceNameFormatter>(new KebabCaseResourceNameFormatter());
services.TryAddSingleton<IJsonApiRoutingConvention, DefaultRoutingConvention>();
}

public static void AddJsonApiInternals(
this IServiceCollection services,
JsonApiOptions jsonApiOptions)
{

var graph = jsonApiOptions.ResourceGraph ?? jsonApiOptions.ResourceGraphBuilder.Build();

if (graph.UsesDbContext == false)
Expand Down Expand Up @@ -183,14 +208,12 @@ public static void AddJsonApiInternals(
services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>));
services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>));

services.AddSingleton<IJsonApiOptions>(jsonApiOptions);
services.AddSingleton<ILinksConfiguration>(jsonApiOptions);
services.AddSingleton(graph);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IContextEntityProvider>(graph);
services.AddScoped<ICurrentRequest, CurrentRequest>();
services.AddScoped<IScopedServiceProvider, RequestScopedServiceProvider>();
services.AddScoped<JsonApiRouteHandler>();
services.AddScoped<IScopedServiceProvider, RequestScopedServiceProvider>();
services.AddScoped<IJsonApiWriter, JsonApiWriter>();
services.AddScoped<IJsonApiReader, JsonApiReader>();
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
Expand Down Expand Up @@ -273,7 +296,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));
}

/// <summary>
Expand Down
71 changes: 71 additions & 0 deletions src/JsonApiDotNetCore/Graph/CamelCaseResourceNameFormatter.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Uses the internal type name to determine the external resource name.
/// By default we us Humanizer for pluralization and then we dasherize the name.
/// </summary>
/// <example>
/// <code>
/// _default.FormatResourceName(typeof(TodoItem)).Dump();
/// // > "todo-items"
/// </code>
/// </example>
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);
}
}

/// <summary>
/// Aoplies the desired casing convention to the internal string.
/// This is generally applied to the type name after pluralization.
/// </summary>
///
/// <example>
/// <code>
/// _default.ApplyCasingConvention("TodoItems");
/// // > "todo-items"
///
/// _default.ApplyCasingConvention("TodoItem");
/// // > "todo-item"
/// </code>
/// </example>
public string ApplyCasingConvention(string properName) => str.Camelize(properName);

/// <summary>
/// Uses the internal PropertyInfo to determine the external resource name.
/// By default the name will be formatted to kebab-case.
/// </summary>
/// <example>
/// Given the following property:
/// <code>
/// public string CompoundProperty { get; set; }
/// </code>
/// The public attribute will be formatted like so:
/// <code>
/// _default.FormatPropertyName(compoundProperty).Dump();
/// // > "compound-property"
/// </code>
/// </example>
public string FormatPropertyName(PropertyInfo property) => str.Camelize(property.Name);
}
}
68 changes: 0 additions & 68 deletions src/JsonApiDotNetCore/Graph/IResourceNameFormatter.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -29,67 +24,4 @@ public interface IResourceNameFormatter
/// </summary>
string ApplyCasingConvention(string properName);
}

public class DefaultResourceNameFormatter : IResourceNameFormatter
{
/// <summary>
/// Uses the internal type name to determine the external resource name.
/// By default we us Humanizer for pluralization and then we dasherize the name.
/// </summary>
/// <example>
/// <code>
/// _default.FormatResourceName(typeof(TodoItem)).Dump();
/// // > "todo-items"
/// </code>
/// </example>
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);
}
}

/// <summary>
/// Aoplies the desired casing convention to the internal string.
/// This is generally applied to the type name after pluralization.
/// </summary>
///
/// <example>
/// <code>
/// _default.ApplyCasingConvention("TodoItems");
/// // > "todo-items"
///
/// _default.ApplyCasingConvention("TodoItem");
/// // > "todo-item"
/// </code>
/// </example>
public string ApplyCasingConvention(string properName) => str.Dasherize(properName);

/// <summary>
/// Uses the internal PropertyInfo to determine the external resource name.
/// By default the name will be formatted to kebab-case.
/// </summary>
/// <example>
/// Given the following property:
/// <code>
/// public string CompoundProperty { get; set; }
/// </code>
/// The public attribute will be formatted like so:
/// <code>
/// _default.FormatPropertyName(compoundProperty).Dump();
/// // > "compound-property"
/// </code>
/// </example>
public string FormatPropertyName(PropertyInfo property) => str.Dasherize(property.Name);
}
}
Loading