Skip to content

Various fixes and improvements #1114

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 29 commits into from
Nov 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
25b2692
Fixed inconsistencies in naming of resource type
Nov 16, 2021
8d17666
Fixed: Query to determine initial state is sent to the read-only data…
Nov 15, 2021
f990010
Make command/query controllers inherit from JsonApiController, passin…
Nov 9, 2021
61aa23e
Added overload on TypeExtensions.IsOrImplementsInterface to take a ty…
Nov 9, 2021
caa131e
Use ResourceType instead of public name in local-id tracker
Nov 17, 2021
0328293
Change IQueryStringParameterReader.AllowEmptyValue into normal interf…
Nov 18, 2021
d343c9d
Simplified startup in example project
Nov 18, 2021
760df70
Removed left-over overload with single type parameter in ResourceGrap…
Nov 18, 2021
93ac5f6
Various corrections in documentation, added #nullable where it makes …
Nov 18, 2021
2e9ca6a
Revert DocFx workaround
Nov 18, 2021
12b23bf
Updated version compatibility table
Nov 18, 2021
4b61bf7
Added release notes and icon to NuGet package
Nov 18, 2021
58ac1ec
Fix nullability warnings produced by .NET 6 with EF Core 6
Nov 18, 2021
6c5214d
Fixed: Error when using EagerLoad on a relationship
Nov 18, 2021
6546063
Use VS2022 image in AppVeyor on Windows
Nov 18, 2021
3fcb6f8
Fixed broken tests on EF Core 6
Nov 18, 2021
0140070
Fixed redacted data when running tests
Nov 18, 2021
2f0687d
Breaking: Removed access-control action filter attributes such as Htt…
Nov 19, 2021
1badb50
Increase version number, use branch name in suffix
Nov 19, 2021
b76629d
Pass the full resource to LinkBuilder, instead of just its ID. This a…
Nov 19, 2021
2802d23
Optimization: Only save when there are changes in a remove-from-to-ma…
Nov 19, 2021
745eacc
Clarifications in doc-comments
Nov 19, 2021
a79f90c
Extract constant
Nov 19, 2021
31f084e
Extract method
Nov 19, 2021
0182ce3
Cleanup SelectClauseBuilder.ToPropertySelectors
Nov 19, 2021
1b8bcb0
Since EF Core 5, SaveChanges automatically creates a savepoint and ro…
Nov 19, 2021
bcf1f18
Improved unittests for populating IJsonApiRequest in middleware
Nov 19, 2021
3fe49f4
Clarify intent in AtomicOperationObjectAdapter
Nov 20, 2021
622fddc
Review feedback: use `ResourceType resourceType` instead of `Resource…
Nov 22, 2021
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 Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ function CreateNuGetPackage {
$versionSuffix = $suffixSegments -join "-"
}
else {
# Get the version suffix from the auto-incrementing build number. Example: "123" => "pre-0123".
# Get the version suffix from the auto-incrementing build number. Example: "123" => "master-0123".
if ($env:APPVEYOR_BUILD_NUMBER) {
$revision = "{0:D4}" -f [convert]::ToInt32($env:APPVEYOR_BUILD_NUMBER, 10)
$versionSuffix = "pre-$revision"
$versionSuffix = "$($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH ?? $env:APPVEYOR_REPO_BRANCH)-$revision"
}
else {
$versionSuffix = "pre-0001"
Expand Down
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,23 @@ See [our documentation](https://www.jsonapi.net/) for detailed usage.
### Models

```c#
public class Article : Identifiable
#nullable enable

public class Article : Identifiable<int>
{
[Attr]
public string Name { get; set; }
public string Name { get; set; } = null!;
}
```

### Controllers

```c#
public class ArticlesController : JsonApiController<Article>
public class ArticlesController : JsonApiController<Article, int>
{
public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
IResourceService<Article> resourceService,)
: base(options, loggerFactory, resourceService)
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IResourceService<Article, int> resourceService)
: base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
Expand Down Expand Up @@ -87,13 +89,16 @@ public class Startup
The following chart should help you pick the best version, based on your environment.
See also our [versioning policy](./VERSIONING_POLICY.md).

| .NET version | Entity Framework Core version | JsonApiDotNetCore version |
| ------------ | ----------------------------- | ------------------------- |
| Core 2.x | 2.x | 3.x |
| Core 3.1 | 3.1 | 4.x |
| Core 3.1 | 5 | 4.x |
| 5 | 5 | 4.x or 5.x |
| 6 | 6 | 5.x |
| JsonApiDotNetCore | .NET | Entity Framework Core | Status |
| ----------------- | -------- | --------------------- | -------------------------- |
| 3.x | Core 2.x | 2.x | Released |
| 4.x | Core 3.1 | 3.1 | Released |
| | Core 3.1 | 5 | |
| | 5 | 5 | |
| | 6 | 5 | |
| v5.x (pending) | 5 | 5 | On AppVeyor, to-be-dropped |
| | 6 | 5 | On AppVeyor, to-be-dropped |
| | 6 | 6 | Requires build from master |

## Contributing

Expand Down
8 changes: 3 additions & 5 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
image:
- Ubuntu
- Visual Studio 2019
- Visual Studio 2022

version: '{build}'

Expand Down Expand Up @@ -32,7 +32,7 @@ for:
-
matrix:
only:
- image: Visual Studio 2019
- image: Visual Studio 2022
services:
- postgresql13
# REF: https://github.com/docascode/docfx-seed/blob/master/appveyor.yml
Expand All @@ -42,9 +42,7 @@ for:
# https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html
git checkout $env:APPVEYOR_REPO_BRANCH -q
}
# Pinning to previous version, because zip of v2.58.8 (released 2d ago) is corrupt.
# Tracked at https://github.com/dotnet/docfx/issues/7689
choco install docfx -y --version 2.58.5
choco install docfx -y
if ($lastexitcode -ne 0) {
throw "docfx install failed with exit code $lastexitcode."
}
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/Deserialization/DeserializationBenchmarkBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public abstract class DeserializationBenchmarkBase
protected DeserializationBenchmarkBase()
{
var options = new JsonApiOptions();
IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<IncomingResource>().Build();
IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<IncomingResource, int>().Build();
options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions;

Expand Down
3 changes: 2 additions & 1 deletion benchmarks/QueryString/QueryStringParserBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public QueryStringParserBenchmarks()
EnableLegacyFilterNotation = true
};

IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<QueryableResource>("alt-resource-name").Build();
IResourceGraph resourceGraph =
new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<QueryableResource, int>("alt-resource-name").Build();

var request = new JsonApiRequest
{
Expand Down
6 changes: 3 additions & 3 deletions benchmarks/Serialization/SerializationBenchmarkBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected SerializationBenchmarkBase()
}
};

ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<OutgoingResource>().Build();
ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<OutgoingResource, int>().Build();
SerializerWriteOptions = ((IJsonApiOptions)options).SerializerWriteOptions;

// ReSharper disable VirtualMemberCallInConstructor
Expand Down Expand Up @@ -229,15 +229,15 @@ public TopLevelLinks GetTopLevelLinks()
};
}

public ResourceLinks GetResourceLinks(ResourceType resourceType, string id)
public ResourceLinks GetResourceLinks(ResourceType resourceType, IIdentifiable resource)
{
return new ResourceLinks
{
Self = "Resource:Self"
};
}

public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, string leftId)
public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource)
{
return new RelationshipLinks
{
Expand Down
8 changes: 5 additions & 3 deletions docs/getting-started/step-by-step.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ Define your domain models such that they implement `IIdentifiable<TId>`.
The easiest way to do this is to inherit from `Identifiable<TId>`.

```c#
#nullable enable

public class Person : Identifiable<int>
{
[Attr]
public string Name { get; set; }
public string Name { get; set; } = null!;
}
```

Expand All @@ -52,12 +54,12 @@ Nothing special here, just an ordinary `DbContext`.
```
public class AppDbContext : DbContext
{
public DbSet<Person> People => Set<Person>();

public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}

public DbSet<Person> People { get; set; }
}
```

Expand Down
36 changes: 20 additions & 16 deletions docs/home/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -142,31 +142,35 @@ <h2>Example usage</h2>
<div class="icon"><i class='bx bx-detail'></i></div>
<h4 class="title">Resource</h4>
<pre>
<code>public class Article : Identifiable
<code>#nullable enable

public class Article : Identifiable&lt;long&gt;
{
[Attr]
[Required, MaxLength(30)]
public string Title { get; set; }
[MaxLength(30)]
public string Title { get; set; } = null!;

[Attr(Capabilities = AttrCapabilities.AllowFilter)]
public string Summary { get; set; }
public string? Summary { get; set; }

[Attr(PublicName = "websiteUrl")]
public string Url { get; set; }
public string? Url { get; set; }

[Attr]
[Required]
public int? WordCount { get; set; }

[Attr(Capabilities = AttrCapabilities.AllowView)]
public DateTimeOffset LastModifiedAt { get; set; }

[HasOne]
public Person Author { get; set; }
public Person Author { get; set; } = null!;

[HasMany]
public ICollection&lt;Revision&gt; Revisions { get; set; }
[HasOne]
public Person? Reviewer { get; set; }

[HasManyThrough(nameof(ArticleTags))]
[NotMapped]
public ICollection&lt;Tag&gt; Tags { get; set; }
public ICollection&lt;ArticleTag&gt; ArticleTags { get; set; }
[HasMany]
public ICollection&lt;Tag&gt; Tags { get; set; } = new HashSet&lt;Tag&gt;();
}</code>
</pre>
</div>
Expand All @@ -179,7 +183,7 @@ <h4 class="title">Resource</h4>
<h4 class="title">Request</h4>
<pre>
<code>
GET /articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author HTTP/1.1
GET /articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields[articles]=title,summary&include=author HTTP/1.1
</code>
</pre>
</div>
Expand All @@ -197,9 +201,9 @@ <h4 class="title">Response</h4>
"totalResources": 1
},
"links": {
"self": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author",
"first": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author",
"last": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields=title,summary&include=author"
"self": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author",
"first": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author",
"last": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author"
},
"data": [
{
Expand Down
20 changes: 12 additions & 8 deletions docs/usage/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ From a controller method:
return Conflict(new Error(HttpStatusCode.Conflict)
{
Title = "Target resource was modified by another user.",
Detail = $"User {userName} changed the {resourceField} field on the {resourceName} resource."
Detail = $"User {userName} changed the {resourceField} field on {resourceName} resource."
});
```

Expand All @@ -20,7 +20,7 @@ From other code:
throw new JsonApiException(new Error(HttpStatusCode.Conflict)
{
Title = "Target resource was modified by another user.",
Detail = $"User {userName} changed the {resourceField} field on the {resourceName} resource."
Detail = $"User {userName} changed the {resourceField} field on {resourceName} resource."
});
```

Expand Down Expand Up @@ -69,18 +69,22 @@ public class CustomExceptionHandler : ExceptionHandler
return base.GetLogMessage(exception);
}

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

return base.CreateErrorDocument(exception);
return base.CreateErrorResponse(exception);
}
}

Expand Down
84 changes: 25 additions & 59 deletions docs/usage/extensibility/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,83 +13,49 @@ public class ArticlesController : JsonApiController<Article, Guid>
}
```

If you want to setup routes yourself, you can instead inherit from `BaseJsonApiController<TResource, TId>` and override its methods with your own `[HttpGet]`, `[HttpHead]`, `[HttpPost]`, `[HttpPatch]` and `[HttpDelete]` attributes added on them. Don't forget to add `[FromBody]` on parameters where needed.

## 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 which routes are exposed on your controller.

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.
To provide read-only access, inherit from `JsonApiQueryController` instead, which blocks all POST, PATCH and DELETE requests.
Likewise, to provide write-only access, inherit from `JsonApiCommandController`, which blocks all GET and HEAD requests.

This approach is ok, but introduces some boilerplate that can easily be avoided.
You can even make your own mix of allowed routes by calling the alternate constructor of `JsonApiController` and injecting the set of service implementations available.
In some cases, resources may be an aggregation of entities or a view on top of the underlying entities. In these cases, there may not be a writable `IResourceService` implementation, so simply inject the implementation that is available.

```c#
public class ArticlesController : BaseJsonApiController<Article, int>
public class ReportsController : JsonApiController<Report, int>
{
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IResourceService<Article, int> resourceService)
: base(options, resourceGraph, loggerFactory, resourceService)
{
}

[HttpGet]
public override async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
{
return await base.GetAsync(cancellationToken);
}

[HttpGet("{id}")]
public override async Task<IActionResult> GetAsync(int id,
CancellationToken cancellationToken)
public ReportsController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IGetAllService<Report, int> getAllService)
: base(options, resourceGraph, loggerFactory, getAll: getAllService)
{
return await base.GetAsync(id, cancellationToken);
}
}
```

## Using ActionFilterAttributes

The next option is to use the ActionFilter attributes that ship with the library. The available attributes are:

- `NoHttpPost`: disallow POST requests
- `NoHttpPatch`: disallow PATCH requests
- `NoHttpDelete`: disallow DELETE requests
- `HttpReadOnly`: all of the above
For more information about resource service injection, see [Replacing injected services](~/usage/extensibility/layer-overview.md#replacing-injected-services) and [Resource Services](~/usage/extensibility/services.md).

Not only does this reduce boilerplate, but it also provides a more meaningful HTTP response code.
An attempt to use one of the blacklisted methods will result in a HTTP 405 Method Not Allowed response.
When a route is blocked, an HTTP 403 Forbidden response is returned.

```c#
[HttpReadOnly]
public class ArticlesController : BaseJsonApiController<Article, int>
{
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IResourceService<Article, int> resourceService)
: base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
```http
DELETE http://localhost:14140/people/1 HTTP/1.1
```

## Implicit Access By Service Injection

Finally, you can control the allowed methods by supplying only the available service implementations. In some cases, resources may be an aggregation of entities or a view on top of the underlying entities. In these cases, there may not be a writable `IResourceService` implementation, so simply inject the implementation that is available.

As with the ActionFilter attributes, if a service implementation is not available to service a request, HTTP 405 Method Not Allowed will be returned.

For more information about resource service injection, see [Replacing injected services](~/usage/extensibility/layer-overview.md#replacing-injected-services) and [Resource Services](~/usage/extensibility/services.md).

```c#
public class ReportsController : BaseJsonApiController<Report, int>
```json
{
public ReportsController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IGetAllService<Report, int> getAllService)
: base(options, resourceGraph, loggerFactory, getAllService)
{
}

[HttpGet]
public override async Task<IActionResult> GetAsync(CancellationToken cancellationToken)
"links": {
"self": "/api/v1/people"
},
"errors": [
{
return await base.GetAsync(cancellationToken);
"id": "dde7f219-2274-4473-97ef-baac3e7c1487",
"status": "403",
"title": "The requested endpoint is not accessible.",
"detail": "Endpoint '/people/1' is not accessible for DELETE requests."
}
]
}
```
Loading