Skip to content
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

Comment thread
maurei marked this conversation as resolved.
Outdated
mvcBuilder = mvcBuilder ?? services.AddMvcCore();

var options = new JsonApiOptions();
// add basic Mvc functionality
Comment thread
maurei marked this conversation as resolved.
// 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>();
Comment thread
maurei marked this conversation as resolved.
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