Skip to content

Routing does not respect custom pluralized resource name #805

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 53 commits into from
Sep 10, 2020
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0800bb2
feat: use resource graph in routing convention rather than formatter
maurei Aug 26, 2020
0d97cba
fix: discovery tests
maurei Aug 26, 2020
10efa97
fix: null ref exception
maurei Aug 26, 2020
6d314aa
fix: client generated id tests
maurei Aug 26, 2020
22d8700
fix: self review 1
maurei Aug 26, 2020
0476228
chore: rename to pluralizedResourceName
maurei Aug 26, 2020
af01214
fix: whitespace
maurei Aug 26, 2020
c2484e8
fix: enumeration exception
maurei Aug 26, 2020
9ac7456
fix: remove whitespace
maurei Aug 26, 2020
787002a
fix: review
maurei Aug 27, 2020
dc0c75a
fix: review
maurei Aug 27, 2020
05f0ad9
test: resource definition discovery test
maurei Aug 27, 2020
28e76fa
docs: update routing related docs
maurei Aug 27, 2020
978e9dd
docs: rephrase
maurei Aug 27, 2020
9d90952
chore: review
maurei Aug 27, 2020
d91bf76
Update routing.md
maurei Aug 27, 2020
08ab1ac
Update routing.md
maurei Aug 27, 2020
6496a7f
chore: trigger build
maurei Aug 27, 2020
aa3c122
fix: reduce amount of intermediate service providers
maurei Aug 28, 2020
681d2cc
feat: configure MvcOptions in phase two, removal of intermediate serv…
maurei Aug 28, 2020
de23e9f
fix: tests
maurei Aug 28, 2020
0b788b5
feat: remove intermediate service providers, consistent middleware co…
maurei Aug 28, 2020
9a7ebe0
fix: remove unnecessary parameter usejsonapi
maurei Aug 31, 2020
6ff5883
fix: review round 31/08
maurei Aug 31, 2020
501e0b7
fix: merge fix attempt 1
maurei Aug 31, 2020
a0d4cfc
fix: docs
maurei Aug 31, 2020
60b9640
fix: merge I
maurei Sep 1, 2020
526b0c6
fix: merge II
maurei Sep 1, 2020
21ed990
fix: docs, publicName
maurei Sep 1, 2020
f519342
fix: whitespace
maurei Sep 1, 2020
c51eea3
chore: review
maurei Sep 2, 2020
234cc86
chore: review 2
maurei Sep 2, 2020
21dfc9b
docs: simplify middleware extensibility docs
maurei Sep 3, 2020
4262410
Changed filters and formatters to async
Sep 3, 2020
e9e24eb
docs: simpify routing docs
maurei Sep 3, 2020
e016d37
Merge branch 'fix/786' of https://github.com/json-api-dotnet/JsonApiD…
maurei Sep 3, 2020
5c05503
chore: improve diff readability
maurei Sep 3, 2020
685eef8
chore: diff readability, fixes
maurei Sep 3, 2020
9f6a4f6
fix: more diff readability
maurei Sep 3, 2020
6696492
fix: nullcheck
maurei Sep 3, 2020
540bc8a
docs: resource graph builder inaccuracy
maurei Sep 3, 2020
4268be1
fix: broken links markdown
maurei Sep 3, 2020
234e0ec
fix: broken markdown links
maurei Sep 3, 2020
39cfdde
fix: restore whitespace
maurei Sep 3, 2020
d60ced3
fix: diff
maurei Sep 3, 2020
1cbd61f
fix: whitespace
maurei Sep 3, 2020
7c65d38
chore: review
maurei Sep 9, 2020
e53fe67
fix: typo in docs
maurei Sep 9, 2020
57eb82e
chore: discovery tests fix
maurei Sep 9, 2020
cd1222a
docs: improvements
maurei Sep 9, 2020
0b623be
chore: review
maurei Sep 10, 2020
948b72a
chore: casing convention -> naming convention
maurei Sep 10, 2020
911924c
fix: update example in docs
maurei Sep 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/DependencyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal static class DependencyFactory
{
public static IResourceGraph CreateResourceGraph(IJsonApiOptions options)
{
IResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
ResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
builder.Add<BenchmarkResource>(BenchmarkResourcePublicNames.Type);
return builder.Build();
}
Expand Down
78 changes: 74 additions & 4 deletions docs/usage/extensibility/middleware.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,84 @@
# Middleware
Add the following to your Startup.ConfigureServices method. Replace AppDbContext with your DbContext.

```c3
The following is the default configuration of JsonApiDotNetCore:
1. Call one of the `AddJsonApi( ... )` overloads in the ` Startup.ConfigureServices` method. In this example uses a `DbContext` to build the resource graph

```c#
services.AddJsonApi<AppDbContext>();
```

Add the middleware to the Startup.Configure method.
2. In the Startup.Configure method, configure your application to use routing, to add the JsonApiMiddleware and to configure endpoint routing.

```c3
```c#
app.UseRouting();
app.UseJsonApi();
app.UseEndpoints(endpoints => endpoints.MapControllers());
```

The following middleware components, in respective order, are registered:

Filters:
- `IJsonApiExceptionFilter`
- `IJsonApiTypeMatchFilter`
- `IQueryStringActionFilter`
- `IConvertEmptyActionResultFilter`

Formatters:
- `IJsonApiInputFormatter`
- `IJsonApiOutputFormatter`

Routing convention:
- `IJsonApiRoutingConvention`

Middleware:
- `JsonApiMiddleware`

All of these components (except for `JsonApiMiddleware`) can be customized by registering your own implementation of these services. For example:

```c#
services.AddSingleton<IJsonApiExceptionFilter, MyCustomExceptionFilter>();
```

It is also possible to directly access the .NET Core `MvcOptions` object and have full control over which components are registered.

## Configuring MvcOptions

JsonApiDotNetCore internally configures `MvcOptions` when calling `AddJsonApi( ... )`. However, it is still possible to register a custom configuration callback. To achieve this it is recommended to register this callback *after* the `AddJsonApi( ... )` call. It is also possible to do it earlier, but your configuration might be overwritten by the JsonApiDotNetCore configuration.

The following example replaces all internally registered filters by retrieving a custom filter from the DI container.
```c#
public class Startup
{
private Action<MvcOptions> _postConfigureMvcOptions;

public void ConfigureServices(IServiceCollection services)
{
...

var builder = services.AddMvcCore();
services.AddJsonApi<AppDbContext>( ... , mvcBuilder: builder);
mvcCoreBuilder.AddMvcOptions(mvcOptions =>
{
// Execute the mvcOptions configuration callback after the JsonApiDotNetCore callback as been executed.
_postConfigureMvcOptions?.Invoke(mvcOptions);
});

...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{

...

// Using a callback, we can defer to later (when service collection has become available).
_postConfigureMvcOptions = mvcOptions =>
{
mvcOptions.Filters.Clear();
mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService<CustomFilter>());
};

...
}
}
```
6 changes: 6 additions & 0 deletions docs/usage/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,14 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter());
options.SerializerSettings.Formatting = Formatting.Indented;
```

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):
```c#
options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() };
```

Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored.


## Enable ModelState Validation

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.
Expand Down
61 changes: 37 additions & 24 deletions docs/usage/resource-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,30 @@ It is built at app startup and available as a singleton through Dependency Injec

There are three ways the resource graph can be created:

1. Auto-discovery
1. Manually specifying each resource
2. Specifying an entire DbContext
3. Manually specifying each resource
3. Auto-discovery

### Auto-Discovery
It is also possible to combine the three of them at once. Be aware that some configuration might overlap,
for example you could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration
is prioritized by the order of the list above.

Auto-discovery refers to the process of reflecting on an assembly and
detecting all of the json:api resources and services.
### Manual Specification

The following command will build the resource graph using all `IIdentifiable`
implementations. It also injects resource definitions and service layer overrides which we will
cover in a later section. You can enable auto-discovery for the
current assembly by adding the following to your `Startup` class.
You can manually construct the graph.

```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddJsonApi(
options => { /* ... */ },
discovery => discovery.AddCurrentAssembly());
services.AddJsonApi(resources: builder =>
{
builder.Add<Person>();
});
}
```

### Entity Framework Core DbContext
### Specifying an Entity Framework Core DbContext

If you are using Entity Framework Core as your ORM, you can add an entire `DbContext` with one line.

Expand All @@ -58,33 +57,47 @@ public void ConfigureServices(IServiceCollection services)
}
```

### Manual Specification
### Auto-discovery

You can also manually construct the graph.
Auto-discovery refers to the process of reflecting on an assembly and
detecting all of the json:api resources, resource definitions, resource services and repositories.

The following command will build the resource graph using all `IIdentifiable`
implementations. It also injects resource definitions and service layer overrides which we will
cover in a later section. You can enable auto-discovery for the
current assembly by adding the following to your `Startup` class.

```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddJsonApi(resources: builder =>
{
builder.AddResource<Person>();
});
services.AddJsonApi(
options => { /* ... */ },
discovery => discovery.AddCurrentAssembly());
}
```

### Public Resource Type Name
### Public Resource Name

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):

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

1. The model is decorated with a `ResourceAttribute`
2. The model is decorated with a `ResourceAttribute`
```c#
[Resource("my-models")]
[Resource("myResources")]
public class MyModel : Identifiable { /* ... */ }
```

2. The configured naming convention (by default this is camel-case).
3. The configured naming convention (by default this is camel-case).
```c#
// this will be registered as "myModels"
public class MyModel : Identifiable { /* ... */ }
```
This convention can be changed by setting the `SerializerSettings.ContractResolver` property on `IJsonApiOptions`. See the [options documentation](./options.md#custom-serializer-settings).
64 changes: 31 additions & 33 deletions docs/usage/routing.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
# Routing

By default the library will configure routes for each controller.
Based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased.

```http
GET /api/compoundModels HTTP/1.1
```

## Namespacing and Versioning URLs

You can add a namespace to all URLs by specifying it in ConfigureServices

```c#
Expand All @@ -20,40 +12,46 @@ public void ConfigureServices(IServiceCollection services)
```
Which results in URLs like: https://yourdomain.com/api/v1/people

## Disable Convention
## Customizing Routes

The library will configure routes for each controller. By default, based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec, routes are camel-cased.

```http
GET /api/compoundModels HTTP/1.1
```

You can disable the default casing convention and specify your own template by using the `DisableRoutingConvention` attribute.
1. **Using the public name of the resource associated to a controller**.

```c#
[Route("[controller]")]
[DisableRoutingConvention]
public class CamelCasedModelsController : JsonApiController<CamelCasedModel>
{
public CamelCasedModelsController(
IJsonApiOptions options,
ILoggerFactory loggerFactory,
IResourceService<CamelCasedModel> resourceService)
: base(options, loggerFactory, resourceService)
{ }
}
public class MyResourceController : JsonApiController<MyApiResource> { /* .... */ }
```
Note that the route
- is `/myApiResources`, which matches the public resouce name in the json:api payload (`{ "type": "myApiResources", ... }`)
- can be configured by configuring the public resource name. See [this section](~/usage/resource-graph.md#public-resource-name) on how to do that.

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.

2. **Using the controller name**.
If a controller does not have an associated resource, the name of the controller will be used following the configured naming strategy.
```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddJsonApi(resources: builder =>
builder.AddResource<TodoItem>("my-models")); // kebab-cased
}
public class MyResourceController : ControllerBase { /* .... */ }
```
Note that the route is `myResources` can be changed by renaming the controller. This approach is the default .NET Core MVC routing approach.

// controller definition
[Route("api/my-models"), DisableRoutingConvention]
public class MyModelsController : JsonApiController<TodoItem>
## Customizing the Routing Convention
It is possible to fully customize routing behavior by registering a `IJsonApiRoutingConvention` implementation.
```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddSingleton<IJsonApiConvention, CustomRoutingConvention>();
}
```

See [this](~/usage/resource-graph.md#public-resource-type-name) for
more information on how the resource name is determined.
## Disabling the Default Routing Convention
It is possible to completely bypass the default routing convention for a particular controller and specify a custom routing template by using the `DisableRoutingConvention` attribute.
In the following example, the `CamelCasedModel` resource can be accessed on `/my-custom-route`.

```c#
[Route("my-custom-route"), DisableRoutingConvention]
public class MyCustomResourceController : JsonApiController<CamelCasedModel> { /* ... */ }
```
12 changes: 3 additions & 9 deletions src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,10 @@ namespace JsonApiDotNetCoreExample
/// </summary>
public abstract class EmptyStartup
{
protected EmptyStartup(IConfiguration configuration)
{
}
protected EmptyStartup(IConfiguration configuration) { }

public virtual void ConfigureServices(IServiceCollection services)
{
}
public virtual void ConfigureServices(IServiceCollection services) { }

public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
}
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { }
}
}
2 changes: 1 addition & 1 deletion src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public override void ConfigureServices(IServiceCollection services)
}, ServiceLifetime.Transient);

services.AddJsonApi<AppDbContext>(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly());

// once all tests have been moved to WebApplicationFactory format we can get rid of this line below
services.AddClientSerialization();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ namespace JsonApiDotNetCoreExample
{
public class TestStartup : Startup
{
public TestStartup(IConfiguration configuration) : base(configuration)
{
}
public TestStartup(IConfiguration configuration) : base(configuration) { }

protected override void ConfigureClock(IServiceCollection services)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace JsonApiDotNetCore.Configuration
{
Expand All @@ -21,7 +21,22 @@ public static class ApplicationBuilderExtensions
/// </example>
public static void UseJsonApi(this IApplicationBuilder builder)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
using var scope = builder.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope();
var inverseRelationshipResolver = scope.ServiceProvider.GetRequiredService<IInverseRelationships>();
inverseRelationshipResolver.Resolve();

var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService<IJsonApiApplicationBuilder>();
jsonApiApplicationBuilder.ConfigureMvcOptions = options =>
{
var inputFormatter = builder.ApplicationServices.GetRequiredService<IJsonApiInputFormatter>();
options.InputFormatters.Insert(0, inputFormatter);

var outputFormatter = builder.ApplicationServices.GetRequiredService<IJsonApiOutputFormatter>();
options.OutputFormatters.Insert(0, outputFormatter);

var routingConvention = builder.ApplicationServices.GetRequiredService<IJsonApiRoutingConvention>();
options.Conventions.Insert(0, routingConvention);
};

builder.UseMiddleware<JsonApiMiddleware>();
}
Expand Down
10 changes: 10 additions & 0 deletions src/JsonApiDotNetCore/Configuration/IJsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using Microsoft.AspNetCore.Mvc;

namespace JsonApiDotNetCore.Configuration
{
internal interface IJsonApiApplicationBuilder
{
public Action<MvcOptions> ConfigureMvcOptions { set; }
}
}
Loading