Skip to content

Commit 54727c9

Browse files
authored
Routing does not respect custom pluralized resource name (#805)
1 parent 88cc6ea commit 54727c9

File tree

58 files changed

+700
-553
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+700
-553
lines changed

benchmarks/DependencyFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class DependencyFactory
1010
{
1111
public static IResourceGraph CreateResourceGraph(IJsonApiOptions options)
1212
{
13-
IResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
13+
ResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
1414
builder.Add<BenchmarkResource>(BenchmarkResourcePublicNames.Type);
1515
return builder.Build();
1616
}
Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
# Middleware
2-
Add the following to your Startup.ConfigureServices method. Replace AppDbContext with your DbContext.
32

4-
```c3
5-
services.AddJsonApi<AppDbContext>();
3+
It is possible to replace JsonApiDotNetCore middleware components by configuring the IoC container and by configuring `MvcOptions`.
4+
5+
## Configuring the IoC container
6+
7+
The following example replaces the internal exception filter with a custom implementation.
8+
```c#
9+
/// In Startup.ConfigureServices
10+
services.AddService<IAsyncJsonApiExceptionFilter, CustomAsyncExceptionFilter>()
611
```
712

8-
Add the middleware to the Startup.Configure method.
13+
## Configuring `MvcOptions`
14+
15+
The following example replaces all internal filters with a custom filter.
16+
```c#
17+
/// In Startup.ConfigureServices
18+
services.AddSingleton<CustomAsyncQueryStringActionFilter>();
19+
20+
var builder = services.AddMvcCore();
21+
services.AddJsonApi<AppDbContext>(mvcBuilder: builder);
922

10-
```c3
11-
app.UseRouting();
23+
// Ensure this call is placed after the AddJsonApi call.
24+
builder.AddMvcOptions(mvcOptions =>
25+
{
26+
_postConfigureMvcOptions?.Invoke(mvcOptions);
27+
});
28+
29+
/// In Startup.Configure
1230
app.UseJsonApi();
31+
32+
// Ensure this call is placed before the UseEndpoints call.
33+
_postConfigureMvcOptions = mvcOptions =>
34+
{
35+
mvcOptions.Filters.Clear();
36+
mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService<CustomAsyncQueryStringActionFilter>());
37+
};
38+
1339
app.UseEndpoints(endpoints => endpoints.MapControllers());
1440
```

docs/usage/options.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,14 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter());
9090
options.SerializerSettings.Formatting = Formatting.Indented;
9191
```
9292

93+
The default naming convention (as used in the routes and public resources names) is also determined here, and can be changed (default is camel-case):
94+
```c#
95+
options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() };
96+
```
97+
9398
Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored.
9499

100+
95101
## Enable ModelState Validation
96102

97103
If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState = true`. By default, no model validation is performed.

docs/usage/resource-graph.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ There are three ways the resource graph can be created:
1414
2. Specifying an entire DbContext
1515
3. Manually specifying each resource
1616

17-
### Auto-Discovery
17+
It is also possible to combine the three of them at once. Be aware that some configuration might overlap,
18+
for example one could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration
19+
is prioritized by the list above in descending order.
20+
21+
### Auto-discovery
1822

1923
Auto-discovery refers to the process of reflecting on an assembly and
20-
detecting all of the json:api resources and services.
24+
detecting all of the json:api resources, resource definitions, resource services and repositories.
2125

2226
The following command will build the resource graph using all `IIdentifiable`
2327
implementations. It also injects resource definitions and service layer overrides which we will
@@ -34,9 +38,9 @@ public void ConfigureServices(IServiceCollection services)
3438
}
3539
```
3640

37-
### Entity Framework Core DbContext
41+
### Specifying an Entity Framework Core DbContext
3842

39-
If you are using Entity Framework Core as your ORM, you can add an entire `DbContext` with one line.
43+
If you are using Entity Framework Core as your ORM, you can add all the models of a `DbContext` to the resource graph.
4044

4145
```c#
4246
// Startup.cs
@@ -46,7 +50,7 @@ public void ConfigureServices(IServiceCollection services)
4650
}
4751
```
4852

49-
Be aware that the previous command does not inject resource definitions and service layer overrides. You can combine it with auto-discovery to register them.
53+
Be aware that this does not register resource definitions, resource services and repositories. You can combine it with auto-discovery to achieve this.
5054

5155
```c#
5256
// Startup.cs
@@ -60,31 +64,41 @@ public void ConfigureServices(IServiceCollection services)
6064

6165
### Manual Specification
6266

63-
You can also manually construct the graph.
67+
You can manually construct the graph.
6468

6569
```c#
6670
// Startup.cs
6771
public void ConfigureServices(IServiceCollection services)
6872
{
6973
services.AddJsonApi(resources: builder =>
7074
{
71-
builder.AddResource<Person>();
75+
builder.Add<Person>();
7276
});
7377
}
7478
```
7579

76-
### Public Resource Type Name
80+
## Public Resource Name
81+
82+
The public resource name is exposed through the `type` member in the json:api payload. This can be configured by the following approaches (in order of priority):
7783

78-
The public resource type name is determined by the following criteria (in order of priority):
84+
1. The `publicName` parameter when manually adding a resource to the graph
85+
```c#
86+
services.AddJsonApi(resources: builder =>
87+
{
88+
builder.Add<Person>(publicName: "people");
89+
});
90+
```
7991

80-
1. The model is decorated with a `ResourceAttribute`
92+
2. The model is decorated with a `ResourceAttribute`
8193
```c#
82-
[Resource("my-models")]
94+
[Resource("myResources")]
8395
public class MyModel : Identifiable { /* ... */ }
8496
```
8597

86-
2. The configured naming convention (by default this is camel-case).
98+
3. The configured naming convention (by default this is camel-case).
8799
```c#
88100
// this will be registered as "myModels"
89101
public class MyModel : Identifiable { /* ... */ }
90102
```
103+
104+
The default naming convention can be changed in [options](./options.md#custom-serializer-settings).

docs/usage/resources/attributes.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,8 @@ public class Person : Identifiable
1414

1515
There are two ways the public attribute name is determined:
1616

17-
1. By convention, specified in @JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_SerializerSettings
18-
```c#
19-
options.SerializerSettings.ContractResolver = new DefaultContractResolver
20-
{
21-
NamingStrategy = new KebabCaseNamingStrategy() // default: CamelCaseNamingStrategy
22-
};
23-
```
17+
1. Using the configured [naming convention](./options.md#custom-serializer-settings).
18+
2419

2520
2. Individually using the attribute's constructor
2621
```c#

docs/usage/routing.md

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
# Routing
22

3-
By default the library will configure routes for each controller.
4-
Based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased.
5-
6-
```http
7-
GET /api/compoundModels HTTP/1.1
8-
```
9-
103
## Namespacing and Versioning URLs
11-
12-
You can add a namespace to all URLs by specifying it in ConfigureServices
4+
You can add a namespace to all URLs by specifying it in ConfigureServices.
135

146
```c#
157
public void ConfigureServices(IServiceCollection services)
@@ -20,40 +12,47 @@ public void ConfigureServices(IServiceCollection services)
2012
```
2113
Which results in URLs like: https://yourdomain.com/api/v1/people
2214

23-
## Disable Convention
15+
## Default Routing Convention
2416

25-
You can disable the default casing convention and specify your own template by using the `DisableRoutingConvention` attribute.
17+
The library will configure routes for all controllers in your project. By default, routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec.
2618

2719
```c#
28-
[Route("[controller]")]
29-
[DisableRoutingConvention]
30-
public class CamelCasedModelsController : JsonApiController<CamelCasedModel>
31-
{
32-
public CamelCasedModelsController(
33-
IJsonApiOptions options,
34-
ILoggerFactory loggerFactory,
35-
IResourceService<CamelCasedModel> resourceService)
36-
: base(options, loggerFactory, resourceService)
37-
{ }
38-
}
20+
public class OrderLine : Identifiable { }
21+
22+
public class OrderLineController : JsonApiController<OrderLine> { /* .... */ }
23+
```
24+
25+
```http
26+
GET /orderLines HTTP/1.1
3927
```
4028

41-
It is important to note that your routes must still end with the model name in the same format as the resource name. This is so that we can build accurate resource links in the json:api document. For example, if you define a resource as MyModels, the controller route must match.
29+
The public name of the resource ([which can be customized](./resource-graph.md#public-resource-name)) is used for the route, instead of the controller name.
4230

31+
### Non-json:api controllers
32+
33+
If a controller does not inherit from `JsonApiController<TResource>`, the [configured naming convention](./options.md#custom-serializer-settings) is applied to the name of the controller.
4334
```c#
44-
public void ConfigureServices(IServiceCollection services)
45-
{
46-
services.AddJsonApi(resources: builder =>
47-
builder.AddResource<TodoItem>("my-models")); // kebab-cased
48-
}
35+
public class OrderLineController : ControllerBase { /* .... */ }
36+
```
37+
```http
38+
GET /orderLines HTTP/1.1
39+
```
4940

50-
// controller definition
51-
[Route("api/my-models"), DisableRoutingConvention]
52-
public class MyModelsController : JsonApiController<TodoItem>
53-
{
54-
//...
55-
}
41+
## Disabling the Default Routing Convention
42+
43+
It is possible to bypass the default routing convention for a controller.
44+
```c#
45+
[Route("v1/custom/route/orderLines"), DisableRoutingConvention]
46+
public class OrderLineController : JsonApiController<OrderLine> { /* ... */ }
5647
```
48+
It is required to match your custom url with the public name of the associated resource.
49+
50+
## Advanced Usage: Custom Routing Convention
5751

58-
See [this](~/usage/resource-graph.md#public-resource-type-name) for
59-
more information on how the resource name is determined.
52+
It is possible to replace the built-in routing convention with a [custom routing convention]](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`.
53+
```c#
54+
public void ConfigureServices(IServiceCollection services)
55+
{
56+
services.AddSingleton<IJsonApiRoutingConvention, CustomRoutingConvention>();
57+
}
58+
```

src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public override void ConfigureServices(IServiceCollection services)
3838
}, ServiceLifetime.Transient);
3939

4040
services.AddJsonApi<AppDbContext>(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly());
41-
41+
4242
// once all tests have been moved to WebApplicationFactory format we can get rid of this line below
4343
services.AddClientSerialization();
4444
}

src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using JsonApiDotNetCore.Middleware;
33
using Microsoft.AspNetCore.Builder;
4+
using Microsoft.Extensions.DependencyInjection;
45

56
namespace JsonApiDotNetCore.Configuration
67
{
@@ -22,6 +23,23 @@ public static class ApplicationBuilderExtensions
2223
public static void UseJsonApi(this IApplicationBuilder builder)
2324
{
2425
if (builder == null) throw new ArgumentNullException(nameof(builder));
26+
27+
using var scope = builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
28+
var inverseRelationshipResolver = scope.ServiceProvider.GetRequiredService<IInverseRelationships>();
29+
inverseRelationshipResolver.Resolve();
30+
31+
var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService<IJsonApiApplicationBuilder>();
32+
jsonApiApplicationBuilder.ConfigureMvcOptions = options =>
33+
{
34+
var inputFormatter = builder.ApplicationServices.GetRequiredService<IJsonApiInputFormatter>();
35+
options.InputFormatters.Insert(0, inputFormatter);
36+
37+
var outputFormatter = builder.ApplicationServices.GetRequiredService<IJsonApiOutputFormatter>();
38+
options.OutputFormatters.Insert(0, outputFormatter);
39+
40+
var routingConvention = builder.ApplicationServices.GetRequiredService<IJsonApiRoutingConvention>();
41+
options.Conventions.Insert(0, routingConvention);
42+
};
2543

2644
builder.UseMiddleware<JsonApiMiddleware>();
2745
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace JsonApiDotNetCore.Configuration
5+
{
6+
internal interface IJsonApiApplicationBuilder
7+
{
8+
public Action<MvcOptions> ConfigureMvcOptions { set; }
9+
}
10+
}

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public interface IJsonApiOptions
176176
/// Specifies the settings that are used by the <see cref="JsonSerializer"/>.
177177
/// Note that at some places a few settings are ignored, to ensure json:api spec compliance.
178178
/// <example>
179-
/// The next example changes the casing convention to kebab casing.
179+
/// The next example changes the naming convention to kebab casing.
180180
/// <code><![CDATA[
181181
/// options.SerializerSettings.ContractResolver = new DefaultContractResolver
182182
/// {

src/JsonApiDotNetCore/Configuration/IResourceGraphBuilder.cs

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/JsonApiDotNetCore/Configuration/IServiceDiscoveryFacade.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)