Skip to content

Documentation cleanup, corrections, additions etc #854

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 5 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<PropertyGroup>
<XUnitVersion>2.4.1</XUnitVersion>
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
<BogusVersion>29.0.1</BogusVersion>
<MoqVersion>4.13.1</MoqVersion>
<BogusVersion>31.0.3</BogusVersion>
<MoqVersion>4.14.6</MoqVersion>
</PropertyGroup>
</Project>
2 changes: 1 addition & 1 deletion benchmarks/Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion docs/getting-started/step-by-step.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class AppDbContext : DbContext
### Define Controllers

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

```c#
public class PeopleController : JsonApiController<Person>
Expand Down
2 changes: 1 addition & 1 deletion docs/internals/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Processing a request involves the following steps:
- These validated expressions contain direct references to attributes and relationships.
- The readers also implement `IQueryConstraintProvider`, which exposes expressions through `ExpressionInScope` objects.
- `QueryLayerComposer` (used from `JsonApiResourceService`) collects all query constraints.
- It combines them with default options and `ResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
- It combines them with default options and `IResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
- It lifts the tree for nested endpoints like /blogs/1/articles and rewrites includes.
- `JsonApiResourceService` contains no more usage of `IQueryable`.
- `EntityFrameworkCoreRepository` delegates to `QueryableBuilder` to transform the `QueryLayer` tree into `IQueryable` expression trees.
Expand Down
67 changes: 67 additions & 0 deletions docs/usage/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,70 @@ throw new JsonApiException(new Error(HttpStatusCode.Conflict)
```

In both cases, the middleware will properly serialize it and return it as a json:api error.

# Exception handling

The translation of user-defined exceptions to error responses can be customized by registering your own handler.
This handler is also the place to choose the log level and message, based on the exception type.

```c#
public class ProductOutOfStockException : Exception
{
public int ProductId { get; }

public ProductOutOfStockException(int productId)
{
ProductId = productId;
}
}

public class CustomExceptionHandler : ExceptionHandler
{
public CustomExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options)
: base(loggerFactory, options)
{
}

protected override LogLevel GetLogLevel(Exception exception)
{
if (exception is ProductOutOfStockException)
{
return LogLevel.Information;
}

return base.GetLogLevel(exception);
}

protected override string GetLogMessage(Exception exception)
{
if (exception is ProductOutOfStockException productOutOfStock)
{
return $"Product {productOutOfStock.ProductId} is currently unavailable.";
}

return base.GetLogMessage(exception);
}

protected override ErrorDocument CreateErrorDocument(Exception exception)
{
if (exception is ProductOutOfStockException productOutOfStock)
{
return new ErrorDocument(new Error(HttpStatusCode.Conflict)
{
Title = "Product is temporarily available.",
Detail = $"Product {productOutOfStock.ProductId} cannot be ordered at the moment."
});
}

return base.CreateErrorDocument(exception);
}
}

public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IExceptionHandler, CustomExceptionHandler>();
}
}
```
4 changes: 2 additions & 2 deletions docs/usage/extensibility/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class ArticlesController : JsonApiController<Article>

## Non-Integer Type Keys

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.
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.

```c#
public class ArticlesController : JsonApiController<Article, Guid>
Expand All @@ -34,7 +34,7 @@ public class ArticlesController : JsonApiController<Article, Guid>

## Resource Access Control

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.
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.

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.

Expand Down
31 changes: 5 additions & 26 deletions docs/usage/extensibility/layer-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ JsonApiController (required)
+-- EntityFrameworkCoreRepository : IResourceRepository
```

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.
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.
You can use the following as a general rule of thumb for where to put business logic:

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

## Replacing Services

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

Replacing services is done on a per-resource basis and can be done through simple DI in your Startup.cs file.

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

```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// custom service
services.AddResourceService<FooService>();

// custom repository
services.AddScoped<FooRepository>();
}
```

Prior to v3.0.0 you could do it like so:

```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// custom service
services.AddScoped<IResourceService<Foo>, FooService>();

// custom repository
services.AddScoped<IResourceRepository<Foo>, FooRepository>();
services.AddScoped<ProductService, IResourceService<Product>();
services.AddScoped<ProductRepository, IResourceRepository<Product>>();
}
```
54 changes: 32 additions & 22 deletions docs/usage/extensibility/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,37 @@ services.AddService<IAsyncJsonApiExceptionFilter, CustomAsyncExceptionFilter>()

The following example replaces all internal filters with a custom filter.
```c#
/// In Startup.ConfigureServices
services.AddSingleton<CustomAsyncQueryStringActionFilter>();

var builder = services.AddMvcCore();
services.AddJsonApi<AppDbContext>(mvcBuilder: builder);

// Ensure this call is placed after the AddJsonApi call.
builder.AddMvcOptions(mvcOptions =>
public class Startup
{
_postConfigureMvcOptions?.Invoke(mvcOptions);
});

/// In Startup.Configure
app.UseJsonApi();

// Ensure this call is placed before the UseEndpoints call.
_postConfigureMvcOptions = mvcOptions =>
{
mvcOptions.Filters.Clear();
mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService<CustomAsyncQueryStringActionFilter>());
};

app.UseEndpoints(endpoints => endpoints.MapControllers());
private Action<MvcOptions> _postConfigureMvcOptions;

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<CustomAsyncQueryStringActionFilter>();

var builder = services.AddMvcCore();
services.AddJsonApi<AppDbContext>(mvcBuilder: builder);

// Ensure this call is placed after the AddJsonApi call.
builder.AddMvcOptions(mvcOptions =>
{
_postConfigureMvcOptions.Invoke(mvcOptions);
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Ensure this call is placed before the UseEndpoints call.
_postConfigureMvcOptions = mvcOptions =>
{
mvcOptions.Filters.Clear();
mvcOptions.Filters.Insert(0,
app.ApplicationServices.GetService<CustomAsyncQueryStringActionFilter>());
};

app.UseRouting();
app.UseJsonApi();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
```
10 changes: 5 additions & 5 deletions docs/usage/extensibility/repositories.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
# Resource Repositories

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

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

```c#
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IResourceRepository<Article>, ArticleRepository>();
// ...
}
```

A sample implementation that performs authorization might look like this.

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.
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.

```c#
public class ArticleRepository : EntityFrameworkCoreRepository<Article>
Expand All @@ -31,7 +30,8 @@ public class ArticleRepository : EntityFrameworkCoreRepository<Article>
IResourceFactory resourceFactory,
IEnumerable<IQueryConstraintProvider> constraintProviders,
ILoggerFactory loggerFactory)
: base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory)
: base(targetedFields, contextResolver, resourceGraph, genericServiceFactory,
resourceFactory, constraintProviders, loggerFactory)
{
_authenticationService = authenticationService;
}
Expand Down
40 changes: 22 additions & 18 deletions docs/usage/extensibility/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ public class TodoItemService : JsonApiResourceService<TodoItem>
private readonly INotificationService _notificationService;

public TodoItemService(
IResourceRepository<TResource, int> repository,
IResourceRepository<TodoItem> repository,
IQueryLayerComposer queryLayerComposer,
IPaginationContext paginationContext,
IJsonApiOptions options,
ILoggerFactory loggerFactory,
IJsonApiRequest request,
IResourceChangeTracker<TResource> resourceChangeTracker,
IResourceChangeTracker<TodoItem> resourceChangeTracker,
IResourceFactory resourceFactory,
IResourceHookExecutor hookExecutor = null)
: base(repository, queryLayerComposer, paginationContext, options, loggerFactory, request,
resourceChangeTracker, resourceFactory, hookExecutor)
: base(repository, queryLayerComposer, paginationContext, options, loggerFactory,
request, resourceChangeTracker, resourceFactory, hookExecutor)
{
_notificationService = notificationService;
}
Expand All @@ -53,30 +53,27 @@ If you'd like to use another ORM that does not provide what JsonApiResourceServi
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// add the service override for MyModel
services.AddScoped<IResourceService<MyModel>, MyModelService>();
// add the service override for Product
services.AddScoped<IResourceService<Product>, ProductService>();

// add your own Data Access Object
services.AddScoped<IMyModelDao, MyModelDao>();
// ...
services.AddScoped<IProductDao, ProductDao>();
}

// MyModelService.cs
public class MyModelService : IResourceService<MyModel>
// ProductService.cs
public class ProductService : IResourceService<Product>
{
private readonly IMyModelDao _dao;
private readonly IProductDao _dao;

public MyModelService(IMyModelDao dao)
public ProductService(IProductDao dao)
{
_dao = dao;
}

public Task<IEnumerable<MyModel>> GetAsync()
public Task<IEnumerable<Product>> GetAsync()
{
return await _dao.GetModelAsync();
return await _dao.GetProductsAsync();
}

// ...
}
```

Expand Down Expand Up @@ -136,10 +133,17 @@ public class Startup
}
```

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

```c#
builder.RegisterType<ArticleService>().AsImplementedInterfaces();
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddResourceService<ArticleService>();
}
}
```

Then in the controller, you should inherit from the base controller and pass the services into the named, optional base parameters:
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ GET /articles?filter[caption]=tech&filter=expr:equals(caption,'cooking')) HTTP/1

There are multiple ways you can add custom filters:

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
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
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
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
3. Add an implementation of `IQueryConstraintProvider` to supply additional `FilterExpression`s, which are combined with existing filters using AND operator
4. Override `EntityFrameworkCoreRepository.ApplyQueryLayer` to adapt the `IQueryable<T>` expression just before execution
5. Take a deep dive and plug into reader/parser/tokenizer/visitor/builder for adding additional general-purpose filter operators
Loading