Skip to content

Commit 7da2019

Browse files
author
Bart Koelman
authored
Documentation cleanup, corrections, additions etc (#854)
* Documentation cleanup, corrections, additions etc * Removed dead code that breaks upgrade to EF Core 5 * Fixed placeholder * Package updates * Fix for failing tests in EF Core 5 RC1
1 parent a824b4a commit 7da2019

24 files changed

+257
-272
lines changed

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<PropertyGroup>
1717
<XUnitVersion>2.4.1</XUnitVersion>
1818
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
19-
<BogusVersion>29.0.1</BogusVersion>
20-
<MoqVersion>4.13.1</MoqVersion>
19+
<BogusVersion>31.0.3</BogusVersion>
20+
<MoqVersion>4.14.6</MoqVersion>
2121
</PropertyGroup>
2222
</Project>

benchmarks/Benchmarks.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</ItemGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
12+
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
1313
<PackageReference Include="Moq" Version="$(MoqVersion)" />
1414
</ItemGroup>
1515
</Project>

docs/getting-started/step-by-step.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class AppDbContext : DbContext
6262
### Define Controllers
6363

6464
You need to create controllers that inherit from `JsonApiController<TResource>` or `JsonApiController<TResource, TId>`
65-
where `T` is the model that inherits from `Identifiable<TId>`
65+
where `TResource` is the model that inherits from `Identifiable<TId>`
6666

6767
```c#
6868
public class PeopleController : JsonApiController<Person>

docs/internals/queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Processing a request involves the following steps:
1717
- These validated expressions contain direct references to attributes and relationships.
1818
- The readers also implement `IQueryConstraintProvider`, which exposes expressions through `ExpressionInScope` objects.
1919
- `QueryLayerComposer` (used from `JsonApiResourceService`) collects all query constraints.
20-
- It combines them with default options and `ResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
20+
- It combines them with default options and `IResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
2121
- It lifts the tree for nested endpoints like /blogs/1/articles and rewrites includes.
2222
- `JsonApiResourceService` contains no more usage of `IQueryable`.
2323
- `EntityFrameworkCoreRepository` delegates to `QueryableBuilder` to transform the `QueryLayer` tree into `IQueryable` expression trees.

docs/usage/errors.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,70 @@ throw new JsonApiException(new Error(HttpStatusCode.Conflict)
2323
```
2424

2525
In both cases, the middleware will properly serialize it and return it as a json:api error.
26+
27+
# Exception handling
28+
29+
The translation of user-defined exceptions to error responses can be customized by registering your own handler.
30+
This handler is also the place to choose the log level and message, based on the exception type.
31+
32+
```c#
33+
public class ProductOutOfStockException : Exception
34+
{
35+
public int ProductId { get; }
36+
37+
public ProductOutOfStockException(int productId)
38+
{
39+
ProductId = productId;
40+
}
41+
}
42+
43+
public class CustomExceptionHandler : ExceptionHandler
44+
{
45+
public CustomExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options)
46+
: base(loggerFactory, options)
47+
{
48+
}
49+
50+
protected override LogLevel GetLogLevel(Exception exception)
51+
{
52+
if (exception is ProductOutOfStockException)
53+
{
54+
return LogLevel.Information;
55+
}
56+
57+
return base.GetLogLevel(exception);
58+
}
59+
60+
protected override string GetLogMessage(Exception exception)
61+
{
62+
if (exception is ProductOutOfStockException productOutOfStock)
63+
{
64+
return $"Product {productOutOfStock.ProductId} is currently unavailable.";
65+
}
66+
67+
return base.GetLogMessage(exception);
68+
}
69+
70+
protected override ErrorDocument CreateErrorDocument(Exception exception)
71+
{
72+
if (exception is ProductOutOfStockException productOutOfStock)
73+
{
74+
return new ErrorDocument(new Error(HttpStatusCode.Conflict)
75+
{
76+
Title = "Product is temporarily available.",
77+
Detail = $"Product {productOutOfStock.ProductId} cannot be ordered at the moment."
78+
});
79+
}
80+
81+
return base.CreateErrorDocument(exception);
82+
}
83+
}
84+
85+
public class Startup
86+
{
87+
public void ConfigureServices(IServiceCollection services)
88+
{
89+
services.AddScoped<IExceptionHandler, CustomExceptionHandler>();
90+
}
91+
}
92+
```

docs/usage/extensibility/controllers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class ArticlesController : JsonApiController<Article>
1616

1717
## Non-Integer Type Keys
1818

19-
If your model is using a type other than int for the primary key, you must explicitly declare it in the controller and service generic type definitions.
19+
If your model is using a type other than `int` for the primary key, you must explicitly declare it in the controller/service/repository definitions.
2020

2121
```c#
2222
public class ArticlesController : JsonApiController<Article, Guid>
@@ -34,7 +34,7 @@ public class ArticlesController : JsonApiController<Article, Guid>
3434

3535
## Resource Access Control
3636

37-
It is often desirable to limit what methods are exposed on your controller. The first way, you can do this is to simply inherit from `BaseJsonApiController` and explicitly declare what methods are available.
37+
It is often desirable to limit what methods are exposed on your controller. The first way you can do this, is to simply inherit from `BaseJsonApiController` and explicitly declare what methods are available.
3838

3939
In this example, if a client attempts to do anything other than GET a resource, an HTTP 404 Not Found response will be returned since no other methods are exposed.
4040

docs/usage/extensibility/layer-overview.md

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ JsonApiController (required)
1010
+-- EntityFrameworkCoreRepository : IResourceRepository
1111
```
1212

13-
Customization can be done at any of these layers. However, it is recommended that you make your customizations at the service or the repository layer when possible to keep the controllers free of unnecessary logic.
13+
Customization can be done at any of these layers. However, it is recommended that you make your customizations at the service or the repository layer when possible, to keep the controllers free of unnecessary logic.
1414
You can use the following as a general rule of thumb for where to put business logic:
1515

1616
- `Controller`: simple validation logic that should result in the return of specific HTTP status codes, such as model validation
@@ -19,36 +19,15 @@ You can use the following as a general rule of thumb for where to put business l
1919

2020
## Replacing Services
2121

22-
**Note:** If you are using auto-discovery, services will be automatically registered for you.
22+
**Note:** If you are using auto-discovery, resource services and repositories will be automatically registered for you.
2323

24-
Replacing services is done on a per-resource basis and can be done through simple DI in your Startup.cs file.
25-
26-
In v3.0.0 we introduced an extenion method that you should use to
27-
register services. This method handles some of the common issues
28-
users have had with service registration.
24+
Replacing services and repositories is done on a per-resource basis and can be done through dependency injection in your Startup.cs file.
2925

3026
```c#
3127
// Startup.cs
3228
public void ConfigureServices(IServiceCollection services)
3329
{
34-
// custom service
35-
services.AddResourceService<FooService>();
36-
37-
// custom repository
38-
services.AddScoped<FooRepository>();
39-
}
40-
```
41-
42-
Prior to v3.0.0 you could do it like so:
43-
44-
```c#
45-
// Startup.cs
46-
public void ConfigureServices(IServiceCollection services)
47-
{
48-
// custom service
49-
services.AddScoped<IResourceService<Foo>, FooService>();
50-
51-
// custom repository
52-
services.AddScoped<IResourceRepository<Foo>, FooRepository>();
30+
services.AddScoped<ProductService, IResourceService<Product>();
31+
services.AddScoped<ProductRepository, IResourceRepository<Product>>();
5332
}
5433
```

docs/usage/extensibility/middleware.md

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,37 @@ services.AddService<IAsyncJsonApiExceptionFilter, CustomAsyncExceptionFilter>()
1414

1515
The following example replaces all internal filters with a custom filter.
1616
```c#
17-
/// In Startup.ConfigureServices
18-
services.AddSingleton<CustomAsyncQueryStringActionFilter>();
19-
20-
var builder = services.AddMvcCore();
21-
services.AddJsonApi<AppDbContext>(mvcBuilder: builder);
22-
23-
// Ensure this call is placed after the AddJsonApi call.
24-
builder.AddMvcOptions(mvcOptions =>
17+
public class Startup
2518
{
26-
_postConfigureMvcOptions?.Invoke(mvcOptions);
27-
});
28-
29-
/// In Startup.Configure
30-
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-
39-
app.UseEndpoints(endpoints => endpoints.MapControllers());
19+
private Action<MvcOptions> _postConfigureMvcOptions;
20+
21+
public void ConfigureServices(IServiceCollection services)
22+
{
23+
services.AddSingleton<CustomAsyncQueryStringActionFilter>();
24+
25+
var builder = services.AddMvcCore();
26+
services.AddJsonApi<AppDbContext>(mvcBuilder: builder);
27+
28+
// Ensure this call is placed after the AddJsonApi call.
29+
builder.AddMvcOptions(mvcOptions =>
30+
{
31+
_postConfigureMvcOptions.Invoke(mvcOptions);
32+
});
33+
}
34+
35+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
36+
{
37+
// Ensure this call is placed before the UseEndpoints call.
38+
_postConfigureMvcOptions = mvcOptions =>
39+
{
40+
mvcOptions.Filters.Clear();
41+
mvcOptions.Filters.Insert(0,
42+
app.ApplicationServices.GetService<CustomAsyncQueryStringActionFilter>());
43+
};
44+
45+
app.UseRouting();
46+
app.UseJsonApi();
47+
app.UseEndpoints(endpoints => endpoints.MapControllers());
48+
}
49+
}
4050
```

docs/usage/extensibility/repositories.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
# Resource Repositories
22

3-
If you want to use a data access technology other than Entity Framework Core, you can create an implementation of IResourceRepository<TResource, TId>.
4-
If you only need minor changes you can override the methods defined in EntityFrameworkCoreRepository<TResource, TId>.
3+
If you want to use a data access technology other than Entity Framework Core, you can create an implementation of `IResourceRepository<TResource, TId>`.
4+
If you only need minor changes you can override the methods defined in `EntityFrameworkCoreRepository<TResource, TId>`.
55

66
The repository should then be added to the service collection in Startup.cs.
77

88
```c#
99
public void ConfigureServices(IServiceCollection services)
1010
{
1111
services.AddScoped<IResourceRepository<Article>, ArticleRepository>();
12-
// ...
1312
}
1413
```
1514

1615
A sample implementation that performs authorization might look like this.
1716

18-
All of the methods in EntityFrameworkCoreRepository will use the GetAll() method to get the DbSet<TResource>, so this is a good method to apply filters such as user or tenant authorization.
17+
All of the methods in EntityFrameworkCoreRepository will use the `GetAll()` method to get the `DbSet<TResource>`, so this is a good method to apply filters such as user or tenant authorization.
1918

2019
```c#
2120
public class ArticleRepository : EntityFrameworkCoreRepository<Article>
@@ -31,7 +30,8 @@ public class ArticleRepository : EntityFrameworkCoreRepository<Article>
3130
IResourceFactory resourceFactory,
3231
IEnumerable<IQueryConstraintProvider> constraintProviders,
3332
ILoggerFactory loggerFactory)
34-
: base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory)
33+
: base(targetedFields, contextResolver, resourceGraph, genericServiceFactory,
34+
resourceFactory, constraintProviders, loggerFactory)
3535
{
3636
_authenticationService = authenticationService;
3737
}

docs/usage/extensibility/services.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ public class TodoItemService : JsonApiResourceService<TodoItem>
1616
private readonly INotificationService _notificationService;
1717

1818
public TodoItemService(
19-
IResourceRepository<TResource, int> repository,
19+
IResourceRepository<TodoItem> repository,
2020
IQueryLayerComposer queryLayerComposer,
2121
IPaginationContext paginationContext,
2222
IJsonApiOptions options,
2323
ILoggerFactory loggerFactory,
2424
IJsonApiRequest request,
25-
IResourceChangeTracker<TResource> resourceChangeTracker,
25+
IResourceChangeTracker<TodoItem> resourceChangeTracker,
2626
IResourceFactory resourceFactory,
2727
IResourceHookExecutor hookExecutor = null)
28-
: base(repository, queryLayerComposer, paginationContext, options, loggerFactory, request,
29-
resourceChangeTracker, resourceFactory, hookExecutor)
28+
: base(repository, queryLayerComposer, paginationContext, options, loggerFactory,
29+
request, resourceChangeTracker, resourceFactory, hookExecutor)
3030
{
3131
_notificationService = notificationService;
3232
}
@@ -53,30 +53,27 @@ If you'd like to use another ORM that does not provide what JsonApiResourceServi
5353
// Startup.cs
5454
public void ConfigureServices(IServiceCollection services)
5555
{
56-
// add the service override for MyModel
57-
services.AddScoped<IResourceService<MyModel>, MyModelService>();
56+
// add the service override for Product
57+
services.AddScoped<IResourceService<Product>, ProductService>();
5858

5959
// add your own Data Access Object
60-
services.AddScoped<IMyModelDao, MyModelDao>();
61-
// ...
60+
services.AddScoped<IProductDao, ProductDao>();
6261
}
6362

64-
// MyModelService.cs
65-
public class MyModelService : IResourceService<MyModel>
63+
// ProductService.cs
64+
public class ProductService : IResourceService<Product>
6665
{
67-
private readonly IMyModelDao _dao;
66+
private readonly IProductDao _dao;
6867

69-
public MyModelService(IMyModelDao dao)
68+
public ProductService(IProductDao dao)
7069
{
7170
_dao = dao;
7271
}
7372

74-
public Task<IEnumerable<MyModel>> GetAsync()
73+
public Task<IEnumerable<Product>> GetAsync()
7574
{
76-
return await _dao.GetModelAsync();
75+
return await _dao.GetProductsAsync();
7776
}
78-
79-
// ...
8077
}
8178
```
8279

@@ -136,10 +133,17 @@ public class Startup
136133
}
137134
```
138135

139-
Other dependency injection frameworks such as Autofac can be used to simplify this syntax.
136+
In v3.0 we introduced an extension method that you can use to register a resource service on all of its JsonApiDotNetCore interfaces.
137+
This is helpful when you implement a subset of the resource interfaces and want to register them all in one go.
140138

141139
```c#
142-
builder.RegisterType<ArticleService>().AsImplementedInterfaces();
140+
public class Startup
141+
{
142+
public void ConfigureServices(IServiceCollection services)
143+
{
144+
services.AddResourceService<ArticleService>();
145+
}
146+
}
143147
```
144148

145149
Then in the controller, you should inherit from the base controller and pass the services into the named, optional base parameters:

docs/usage/filtering.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ GET /articles?filter[caption]=tech&filter=expr:equals(caption,'cooking')) HTTP/1
116116

117117
There are multiple ways you can add custom filters:
118118

119-
1. Creating a `ResourceDefinition` using `OnApplyFilter` (see [here](~/usage/resources/resource-definitions.md#exclude-soft-deleted-resources)) and inject `IRequestQueryStringAccessor`, which works at all depths, but filter operations are constrained to what `FilterExpression` provides
120-
2. Creating a `ResourceDefinition` using `OnRegisterQueryableHandlersForQueryStringParameters` as [described previously](~/usage/resources/resource-definitions.md#custom-query-string-parameters), which enables the full range of `IQueryable<T>` functionality, but only works on primary endpoints
119+
1. Implementing `IResourceDefinition.OnApplyFilter` (see [here](~/usage/resources/resource-definitions.md#exclude-soft-deleted-resources)) and inject `IRequestQueryStringAccessor`, which works at all depths, but filter operations are constrained to what `FilterExpression` provides
120+
2. Implementing `IResourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters` as [described previously](~/usage/resources/resource-definitions.md#custom-query-string-parameters), which enables the full range of `IQueryable<T>` functionality, but only works on primary endpoints
121121
3. Add an implementation of `IQueryConstraintProvider` to supply additional `FilterExpression`s, which are combined with existing filters using AND operator
122122
4. Override `EntityFrameworkCoreRepository.ApplyQueryLayer` to adapt the `IQueryable<T>` expression just before execution
123123
5. Take a deep dive and plug into reader/parser/tokenizer/visitor/builder for adding additional general-purpose filter operators

0 commit comments

Comments
 (0)