resourceService)
+ : base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
@@ -114,18 +116,18 @@ public void Configure(IApplicationBuilder app)
One way to seed the database is in your Configure method:
```c#
-public void Configure(IApplicationBuilder app, AppDbContext context)
+public void Configure(IApplicationBuilder app, AppDbContext dbContext)
{
- context.Database.EnsureCreated();
+ dbContext.Database.EnsureCreated();
- if (!context.People.Any())
+ if (!dbContext.People.Any())
{
- context.People.Add(new Person
+ dbContext.People.Add(new Person
{
Name = "John Doe"
});
- context.SaveChanges();
+ dbContext.SaveChanges();
}
app.UseRouting();
diff --git a/docs/home/index.html b/docs/home/index.html
index 661819f3f6..7f01a30e32 100644
--- a/docs/home/index.html
+++ b/docs/home/index.html
@@ -142,31 +142,35 @@ Example usage
Resource
-public class Article : Identifiable
+#nullable enable
+
+public class Article : Identifiable<long>
{
[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<Revision> Revisions { get; set; }
+ [HasOne]
+ public Person? Reviewer { get; set; }
- [HasManyThrough(nameof(ArticleTags))]
- [NotMapped]
- public ICollection<Tag> Tags { get; set; }
- public ICollection<ArticleTag> ArticleTags { get; set; }
+ [HasMany]
+ public ICollection<Tag> Tags { get; set; } = new HashSet<Tag>();
}
@@ -179,7 +183,7 @@ Resource
Request
-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
@@ -197,9 +201,9 @@ Response
"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": [
{
diff --git a/docs/internals/queries.md b/docs/internals/queries.md
index b5e5c2cf19..46005f489c 100644
--- a/docs/internals/queries.md
+++ b/docs/internals/queries.md
@@ -5,7 +5,7 @@ _since v4.0_
The query pipeline roughly looks like this:
```
-HTTP --[ASP.NET Core]--> QueryString --[JADNC:QueryStringParameterReader]--> QueryExpression[] --[JADNC:ResourceService]--> QueryLayer --[JADNC:Repository]--> IQueryable --[EF Core]--> SQL
+HTTP --[ASP.NET]--> QueryString --[JADNC:QueryStringParameterReader]--> QueryExpression[] --[JADNC:ResourceService]--> QueryLayer --[JADNC:Repository]--> IQueryable --[Entity Framework Core]--> SQL
```
Processing a request involves the following steps:
@@ -22,7 +22,7 @@ Processing a request involves the following steps:
- `JsonApiResourceService` contains no more usage of `IQueryable`.
- `EntityFrameworkCoreRepository` delegates to `QueryableBuilder` to transform the `QueryLayer` tree into `IQueryable` expression trees.
`QueryBuilder` depends on `QueryClauseBuilder` implementations that visit the tree nodes, transforming them to `System.Linq.Expression` equivalents.
- The `IQueryable` expression trees are executed by EF Core, which produces SQL statements out of them.
+ The `IQueryable` expression trees are executed by Entity Framework Core, which produces SQL statements out of them.
- `JsonApiWriter` transforms resource objects into json response.
# Example
diff --git a/docs/usage/errors.md b/docs/usage/errors.md
index 96722739b4..3278526e6c 100644
--- a/docs/usage/errors.md
+++ b/docs/usage/errors.md
@@ -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."
});
```
@@ -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."
});
```
@@ -69,18 +69,22 @@ public class CustomExceptionHandler : ExceptionHandler
return base.GetLogMessage(exception);
}
- protected override ErrorDocument CreateErrorDocument(Exception exception)
+ protected override IReadOnlyList 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);
}
}
diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md
index c117642cbc..1993f77841 100644
--- a/docs/usage/extensibility/controllers.md
+++ b/docs/usage/extensibility/controllers.md
@@ -1,112 +1,61 @@
# Controllers
-You need to create controllers that inherit from `JsonApiController`
-
-```c#
-public class ArticlesController : JsonApiController
-{
- public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IResourceService resourceService)
- : base(options, loggerFactory, resourceService)
- {
- }
-}
-```
-
-## 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/service/repository definitions.
+You need to create controllers that inherit from `JsonApiController`
```c#
public class ArticlesController : JsonApiController
-//---------------------------------------------------------- ^^^^
{
- public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IResourceService resourceService)
- //----------------------- ^^^^
- : base(options, loggerFactory, resourceService)
+ public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
+ ILoggerFactory loggerFactory, IResourceService resourceService)
+ : base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
```
+If you want to setup routes yourself, you can instead inherit from `BaseJsonApiController` 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
+public class ReportsController : JsonApiController
{
- public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IResourceService resourceService)
- : base(options, loggerFactory, resourceService)
- {
- }
-
- [HttpGet]
- public override async Task GetAsync(CancellationToken cancellationToken)
+ public ReportsController(IJsonApiOptions options, IResourceGraph resourceGraph,
+ ILoggerFactory loggerFactory, IGetAllService getAllService)
+ : base(options, resourceGraph, loggerFactory, getAll: getAllService)
{
- return await base.GetAsync(cancellationToken);
- }
-
- [HttpGet("{id}")]
- public override async Task GetAsync(int id,
- CancellationToken cancellationToken)
- {
- 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
-{
- public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IResourceService resourceService)
- : base(options, 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
+```json
{
- public ReportsController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IGetAllService getAllService)
- : base(options, loggerFactory, getAllService)
- {
- }
-
- [HttpGet]
- public override async Task 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."
}
+ ]
}
```
diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md
index 623c959510..7d76f2389a 100644
--- a/docs/usage/extensibility/repositories.md
+++ b/docs/usage/extensibility/repositories.md
@@ -8,9 +8,9 @@ The repository should then be registered in Startup.cs.
```c#
public void ConfigureServices(IServiceCollection services)
{
- services.AddScoped, ArticleRepository>();
- services.AddScoped, ArticleRepository>();
- services.AddScoped, ArticleRepository>();
+ services.AddScoped, ArticleRepository>();
+ services.AddScoped, ArticleRepository>();
+ services.AddScoped, ArticleRepository>();
}
```
@@ -34,18 +34,18 @@ 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`, so this is a good method to apply filters such as user or tenant authorization.
```c#
-public class ArticleRepository : EntityFrameworkCoreRepository
+public class ArticleRepository : EntityFrameworkCoreRepository
{
private readonly IAuthenticationService _authenticationService;
public ArticleRepository(IAuthenticationService authenticationService,
- ITargetedFields targetedFields, IDbContextResolver contextResolver,
- IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory,
- IResourceFactory resourceFactory,
+ ITargetedFields targetedFields, IDbContextResolver dbContextResolver,
+ IResourceGraph resourceGraph, IResourceFactory resourceFactory,
IEnumerable constraintProviders,
- ILoggerFactory loggerFactory)
- : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory,
- resourceFactory, constraintProviders, loggerFactory)
+ ILoggerFactory loggerFactory,
+ IResourceDefinitionAccessor resourceDefinitionAccessor)
+ : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory,
+ constraintProviders, loggerFactory, resourceDefinitionAccessor)
{
_authenticationService = authenticationService;
}
@@ -64,18 +64,17 @@ If you need to use multiple Entity Framework Core DbContexts, first create a rep
This example shows a single `DbContextARepository` for all entities that are members of `DbContextA`.
```c#
-public class DbContextARepository : EntityFrameworkCoreRepository
- where TResource : class, IIdentifiable
+public class DbContextARepository : EntityFrameworkCoreRepository
+ where TResource : class, IIdentifiable
{
public DbContextARepository(ITargetedFields targetedFields,
- DbContextResolver contextResolver,
+ DbContextResolver dbContextResolver,
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory,
- IResourceFactory resourceFactory,
+ IResourceGraph resourceGraph, IResourceFactory resourceFactory,
IEnumerable constraintProviders,
- ILoggerFactory loggerFactory)
- : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory,
- resourceFactory, constraintProviders, loggerFactory)
+ ILoggerFactory loggerFactory, IResourceDefinitionAccessor resourceDefinitionAccessor)
+ : base(targetedFields, dbContextResolver, resourceGraph, resourceFactory,
+ constraintProviders, loggerFactory, resourceDefinitionAccessor)
{
}
}
diff --git a/docs/usage/extensibility/resource-definitions.md b/docs/usage/extensibility/resource-definitions.md
index 5f0ca406be..4c9eeeb8a6 100644
--- a/docs/usage/extensibility/resource-definitions.md
+++ b/docs/usage/extensibility/resource-definitions.md
@@ -23,7 +23,7 @@ public class Startup
resource definition on the container yourself:
```c#
-services.AddScoped, ProductResource>();
+services.AddScoped, ProductDefinition>();
```
## Customizing queries
@@ -31,7 +31,7 @@ services.AddScoped, ProductResource>();
_since v4.0_
For various reasons (see examples below) you may need to change parts of the query, depending on resource type.
-`JsonApiResourceDefinition` (which is an empty implementation of `IResourceDefinition`) provides overridable methods that pass you the result of query string parameter parsing.
+`JsonApiResourceDefinition` (which is an empty implementation of `IResourceDefinition`) provides overridable methods that pass you the result of query string parameter parsing.
The value returned by you determines what will be used to execute the query.
An intermediate format (`QueryExpression` and derived types) is used, which enables us to separate JSON:API implementation
@@ -45,7 +45,7 @@ For example, you may accept some sensitive data that should only be exposed to a
**Note:** to exclude attributes unconditionally, use `[Attr(Capabilities = ~AttrCapabilities.AllowView)]` on a resource class property.
```c#
-public class UserDefinition : JsonApiResourceDefinition
+public class UserDefinition : JsonApiResourceDefinition
{
public UserDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
@@ -104,7 +104,7 @@ Content-Type: application/vnd.api+json
You can define the default sort order if no `sort` query string parameter is provided.
```c#
-public class AccountDefinition : JsonApiResourceDefinition
+public class AccountDefinition : JsonApiResourceDefinition
{
public AccountDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
@@ -132,7 +132,7 @@ public class AccountDefinition : JsonApiResourceDefinition
You may want to enforce pagination on large database tables.
```c#
-public class AccessLogDefinition : JsonApiResourceDefinition
+public class AccessLogDefinition : JsonApiResourceDefinition
{
public AccessLogDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
@@ -163,7 +163,7 @@ public class AccessLogDefinition : JsonApiResourceDefinition
The next example filters out `Account` resources that are suspended.
```c#
-public class AccountDefinition : JsonApiResourceDefinition
+public class AccountDefinition : JsonApiResourceDefinition
{
public AccountDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
@@ -172,11 +172,8 @@ public class AccountDefinition : JsonApiResourceDefinition
public override FilterExpression OnApplyFilter(FilterExpression existingFilter)
{
- var resourceContext = ResourceGraph.GetResourceContext();
-
- var isSuspendedAttribute =
- resourceContext.Attributes.Single(account =>
- account.Property.Name == nameof(Account.IsSuspended));
+ var isSuspendedAttribute = ResourceType.Attributes.Single(account =>
+ account.Property.Name == nameof(Account.IsSuspended));
var isNotSuspended = new ComparisonExpression(ComparisonOperator.Equals,
new ResourceFieldChainExpression(isSuspendedAttribute),
@@ -195,7 +192,7 @@ public class AccountDefinition : JsonApiResourceDefinition
In the example below, an error is returned when a user tries to include the manager of an employee.
```c#
-public class EmployeeDefinition : JsonApiResourceDefinition
+public class EmployeeDefinition : JsonApiResourceDefinition
{
public EmployeeDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
@@ -226,11 +223,11 @@ _since v3_
You can define additional query string parameters with the LINQ expression that should be used.
If the key is present in a query string, the supplied LINQ expression will be added to the database query.
-Note this directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of EF Core operators.
+Note this directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of Entity Framework Core operators.
But it only works on primary resource endpoints (for example: /articles, but not on /blogs/1/articles or /blogs?include=articles).
```c#
-public class ItemDefinition : JsonApiResourceDefinition-
+public class ItemDefinition : JsonApiResourceDefinition
-
{
public ItemDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md
index 2c157ae432..77d772435e 100644
--- a/docs/usage/extensibility/services.md
+++ b/docs/usage/extensibility/services.md
@@ -5,13 +5,13 @@ This allows you to customize it however you want. This is also a good place to i
## Supplementing Default Behavior
-If you don't need to alter the underlying mechanisms, you can inherit from `JsonApiResourceService` and override the existing methods.
+If you don't need to alter the underlying mechanisms, you can inherit from `JsonApiResourceService` and override the existing methods.
In simple cases, you can also just wrap the base implementation with your custom logic.
A simple example would be to send notifications when a resource gets created.
```c#
-public class TodoItemService : JsonApiResourceService
+public class TodoItemService : JsonApiResourceService
{
private readonly INotificationService _notificationService;
@@ -19,7 +19,8 @@ public class TodoItemService : JsonApiResourceService
IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext,
IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request,
IResourceChangeTracker resourceChangeTracker,
- IResourceDefinitionAccessor resourceDefinitionAccessor)
+ IResourceDefinitionAccessor resourceDefinitionAccessor,
+ INotificationService notificationService)
: base(repositoryAccessor, queryLayerComposer, paginationContext, options,
loggerFactory, request, resourceChangeTracker, resourceDefinitionAccessor)
{
@@ -43,21 +44,21 @@ public class TodoItemService : JsonApiResourceService
## Not Using Entity Framework Core?
As previously discussed, this library uses Entity Framework Core by default.
-If you'd like to use another ORM that does not provide what JsonApiResourceService depends upon, you can use a custom `IResourceService` implementation.
+If you'd like to use another ORM that does not provide what JsonApiResourceService depends upon, you can use a custom `IResourceService` implementation.
```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// add the service override for Product
- services.AddScoped, ProductService>();
+ services.AddScoped, ProductService>();
// add your own Data Access Object
services.AddScoped();
}
// ProductService.cs
-public class ProductService : IResourceService
+public class ProductService : IResourceService
{
private readonly IProductDao _dao;
@@ -121,7 +122,7 @@ IResourceService
In order to take advantage of these interfaces you first need to register the service for each implemented interface.
```c#
-public class ArticleService : ICreateService, IDeleteService
+public class ArticleService : ICreateService, IDeleteService
{
// ...
}
@@ -130,8 +131,8 @@ public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
- services.AddScoped, ArticleService>();
- services.AddScoped, ArticleService>();
+ services.AddScoped, ArticleService>();
+ services.AddScoped, ArticleService>();
}
}
```
@@ -151,29 +152,16 @@ public class Startup
}
```
-Then in the controller, you should inherit from the base controller and pass the services into the named, optional base parameters:
+Then in the controller, you should inherit from the JSON:API controller and pass the services into the named, optional base parameters:
```c#
-public class ArticlesController : BaseJsonApiController
+public class ArticlesController : JsonApiController
{
- public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- ICreateService create, IDeleteService delete)
- : base(options, loggerFactory, create: create, delete: delete)
+ public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
+ ILoggerFactory loggerFactory, ICreateService create,
+ IDeleteService delete)
+ : base(options, resourceGraph, loggerFactory, create: create, delete: delete)
{
}
-
- [HttpPost]
- public override async Task PostAsync([FromBody] Article resource,
- CancellationToken cancellationToken)
- {
- return await base.PostAsync(resource, cancellationToken);
- }
-
- [HttpDelete("{id}")]
- public override async TaskDeleteAsync(int id,
- CancellationToken cancellationToken)
- {
- return await base.DeleteAsync(id, cancellationToken);
- }
}
```
diff --git a/docs/usage/meta.md b/docs/usage/meta.md
index 6f052103e4..29c074b8b6 100644
--- a/docs/usage/meta.md
+++ b/docs/usage/meta.md
@@ -8,14 +8,16 @@ Global metadata can be added to the root of the response document by registering
This is useful if you need access to other registered services to build the meta object.
```c#
+#nullable enable
+
// In Startup.ConfigureServices
services.AddSingleton();
public sealed class CopyrightResponseMeta : IResponseMeta
{
- public IReadOnlyDictionary GetMeta()
+ public IReadOnlyDictionary GetMeta()
{
- return new Dictionary
+ return new Dictionary
{
["copyright"] = "Copyright (C) 2002 Umbrella Corporation.",
["authors"] = new[] { "Alice", "Red Queen" }
@@ -39,24 +41,26 @@ public sealed class CopyrightResponseMeta : IResponseMeta
## Resource Meta
-Resource-specific metadata can be added by implementing `IResourceDefinition.GetMeta` (or overriding it on `JsonApiResourceDefinition`):
+Resource-specific metadata can be added by implementing `IResourceDefinition.GetMeta` (or overriding it on `JsonApiResourceDefinition`):
```c#
-public class PersonDefinition : JsonApiResourceDefinition
+#nullable enable
+
+public class PersonDefinition : JsonApiResourceDefinition
{
public PersonDefinition(IResourceGraph resourceGraph)
: base(resourceGraph)
{
}
- public override IReadOnlyDictionary GetMeta(Person person)
+ public override IReadOnlyDictionary? GetMeta(Person person)
{
if (person.IsEmployee)
{
- return new Dictionary
+ return new Dictionary
{
["notice"] = "Check our intranet at http://www.example.com/employees/" +
- person.StringId + " for personal details."
+ $"{person.StringId} for personal details."
};
}
diff --git a/docs/usage/options.md b/docs/usage/options.md
index e2e099e31e..2f350b8bf9 100644
--- a/docs/usage/options.md
+++ b/docs/usage/options.md
@@ -39,6 +39,9 @@ options.MaximumPageNumber = new PageNumber(50);
options.IncludeTotalResourceCount = true;
```
+To retrieve the total number of resources on secondary and relationship endpoints, the reverse of the relationship must to be available. For example, in `GET /customers/1/orders`, both the relationships `[HasMany] Customer.Orders` and `[HasOne] Order.Customer` must be defined.
+If `IncludeTotalResourceCount` is set to `false` (or the inverse relationship is unavailable on a non-primary endpoint), best-effort paging links are returned instead. This means no `last` link and the `next` link only occurs when the current page is full.
+
## Relative Links
All links are absolute by default. However, you can configure relative links.
@@ -100,20 +103,31 @@ options.SerializerOptions.DictionaryKeyPolicy = null;
Because we copy resource properties into an intermediate object before serialization, JSON annotations such as `[JsonPropertyName]` and `[JsonIgnore]` on `[Attr]` properties are ignored.
-## Enable ModelState Validation
+## ModelState Validation
+
+[ASP.NET ModelState validation](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) can be used to validate incoming request bodies when creating and updating resources. Since v5.0, this is enabled by default.
+When `ValidateModelState` is set to `false`, no model validation is performed.
-If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState` to `true`. By default, no model validation is performed.
+How nullability affects ModelState validation is described [here](~/usage/resources/nullability.md).
```c#
options.ValidateModelState = true;
```
```c#
-public class Person : Identifiable
+#nullable enable
+
+public class Person : Identifiable
{
[Attr]
- [Required]
[MinLength(3)]
- public string FirstName { get; set; }
+ public string FirstName { get; set; } = null!;
+
+ [Attr]
+ [Required]
+ public int? Age { get; set; }
+
+ [HasOne]
+ public LoginAccount Account { get; set; } = null!;
}
```
diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md
index 36e424d6e0..beb20d2d92 100644
--- a/docs/usage/resource-graph.md
+++ b/docs/usage/resource-graph.md
@@ -65,7 +65,7 @@ public void ConfigureServices(IServiceCollection services)
{
services.AddJsonApi(resources: builder =>
{
- builder.Add();
+ builder.Add();
});
}
```
@@ -78,14 +78,14 @@ The public resource name is exposed through the `type` member in the JSON:API pa
```c#
services.AddJsonApi(resources: builder =>
{
- builder.Add(publicName: "people");
+ builder.Add(publicName: "people");
});
```
2. The model is decorated with a `ResourceAttribute`
```c#
[Resource("myResources")]
-public class MyModel : Identifiable
+public class MyModel : Identifiable
{
}
```
@@ -93,7 +93,7 @@ public class MyModel : Identifiable
3. The configured naming convention (by default this is camel-case).
```c#
// this will be registered as "myModels"
-public class MyModel : Identifiable
+public class MyModel : Identifiable
{
}
```
diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md
index 6a42bae7e0..669dba0892 100644
--- a/docs/usage/resources/attributes.md
+++ b/docs/usage/resources/attributes.md
@@ -3,10 +3,15 @@
If you want an attribute on your model to be publicly available, add the `AttrAttribute`.
```c#
-public class Person : Identifiable
+#nullable enable
+
+public class Person : Identifiable
{
[Attr]
- public string FirstName { get; set; }
+ public string? FirstName { get; set; }
+
+ [Attr]
+ public string LastName { get; set; } = null!;
}
```
@@ -18,10 +23,11 @@ There are two ways the exposed attribute name is determined:
2. Individually using the attribute's constructor.
```c#
-public class Person : Identifiable
+#nullable enable
+public class Person : Identifiable
{
[Attr(PublicName = "first-name")]
- public string FirstName { get; set; }
+ public string? FirstName { get; set; }
}
```
@@ -42,10 +48,12 @@ This can be overridden per attribute.
Attributes can be marked to allow returning their value in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response.
```c#
-public class User : Identifiable
+#nullable enable
+
+public class User : Identifiable
{
[Attr(Capabilities = ~AttrCapabilities.AllowView)]
- public string Password { get; set; }
+ public string Password { get; set; } = null!;
}
```
@@ -54,10 +62,12 @@ public class User : Identifiable
Attributes can be marked as creatable, which will allow `POST` requests to assign a value to them. When sent but not allowed, an HTTP 422 response is returned.
```c#
-public class Person : Identifiable
+#nullable enable
+
+public class Person : Identifiable
{
[Attr(Capabilities = AttrCapabilities.AllowCreate)]
- public string CreatorName { get; set; }
+ public string? CreatorName { get; set; }
}
```
@@ -66,10 +76,12 @@ public class Person : Identifiable
Attributes can be marked as changeable, which will allow `PATCH` requests to update them. When sent but not allowed, an HTTP 422 response is returned.
```c#
-public class Person : Identifiable
+#nullable enable
+
+public class Person : Identifiable
{
[Attr(Capabilities = AttrCapabilities.AllowChange)]
- public string FirstName { get; set; }
+ public string? FirstName { get; set; };
}
```
@@ -78,10 +90,12 @@ public class Person : Identifiable
Attributes can be marked to allow filtering and/or sorting. When not allowed, it results in an HTTP 400 response.
```c#
-public class Person : Identifiable
+#nullable enable
+
+public class Person : Identifiable
{
[Attr(Capabilities = AttrCapabilities.AllowSort | AttrCapabilities.AllowFilter)]
- public string FirstName { get; set; }
+ public string? FirstName { get; set; }
}
```
@@ -93,17 +107,19 @@ so you should use their APIs to specify serialization format.
You can also use [global options](~/usage/options.md#customize-serializer-options) to control the `JsonSerializer` behavior.
```c#
-public class Foo : Identifiable
+#nullable enable
+
+public class Foo : Identifiable
{
[Attr]
- public Bar Bar { get; set; }
+ public Bar? Bar { get; set; }
}
public class Bar
{
[JsonPropertyName("compound-member")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- public string CompoundMember { get; set; }
+ public string? CompoundMember { get; set; }
}
```
@@ -113,12 +129,15 @@ The first member is the concrete type that you will directly interact with in yo
and retrieval.
```c#
-public class Foo : Identifiable
+#nullable enable
+
+public class Foo : Identifiable
{
- [Attr, NotMapped]
- public Bar Bar { get; set; }
+ [Attr]
+ [NotMapped]
+ public Bar? Bar { get; set; }
- public string BarJson
+ public string? BarJson
{
get
{
diff --git a/docs/usage/resources/index.md b/docs/usage/resources/index.md
index 29f510e543..552b3886fa 100644
--- a/docs/usage/resources/index.md
+++ b/docs/usage/resources/index.md
@@ -8,25 +8,12 @@ public class Person : Identifiable
}
```
-You can use the non-generic `Identifiable` if your primary key is an integer.
+**Note:** Earlier versions of JsonApiDotNetCore allowed a short-hand notation when `TId` is of type `int`. This was removed in v5.
-```c#
-public class Person : Identifiable
-{
-}
-
-// is the same as:
-
-public class Person : Identifiable
-{
-}
-```
-
-If you need to attach annotations or attributes on the `Id` property,
-you can override the virtual property.
+If you need to attach annotations or attributes on the `Id` property, you can override the virtual property.
```c#
-public class Person : Identifiable
+public class Person : Identifiable
{
[Key]
[Column("PersonID")]
diff --git a/docs/usage/resources/nullability.md b/docs/usage/resources/nullability.md
new file mode 100644
index 0000000000..24b15572fc
--- /dev/null
+++ b/docs/usage/resources/nullability.md
@@ -0,0 +1,89 @@
+# Nullability in resources
+
+Properties on a resource class can be declared as nullable or non-nullable. This affects both ASP.NET ModelState validation and the way Entity Framework Core generates database columns.
+
+ModelState validation is enabled by default since v5.0. In earlier versions, it can be enabled in [options](~/usage/options.md#modelstate-validation).
+
+# Value types
+
+When ModelState validation is enabled, non-nullable value types will **not** trigger a validation error when omitted in the request body.
+To make JsonApiDotNetCore return an error when such a property is missing on resource creation, declare it as nullable and annotate it with `[Required]`.
+
+Example:
+
+```c#
+public sealed class User : Identifiable
+{
+ [Attr]
+ [Required]
+ public bool? IsAdministrator { get; set; }
+}
+```
+
+This makes Entity Framework Core generate non-nullable columns. And model errors are returned when nullable fields are omitted.
+
+# Reference types
+
+When the [nullable reference types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) (NRT) compiler feature is enabled, it affects both ASP.NET ModelState validation and Entity Framework Core.
+
+## NRT turned off
+
+When NRT is turned off, use `[Required]` on required attributes and relationships. This makes Entity Framework Core generate non-nullable columns. And model errors are returned when required fields are omitted.
+
+Example:
+
+```c#
+#nullable disable
+
+public sealed class Label : Identifiable
+{
+ [Attr]
+ [Required]
+ public string Name { get; set; }
+
+ [Attr]
+ public string RgbColor { get; set; }
+
+ [HasOne]
+ [Required]
+ public Person Creator { get; set; }
+
+ [HasOne]
+ public Label Parent { get; set; }
+
+ [HasMany]
+ public ISet TodoItems { get; set; }
+}
+```
+
+## NRT turned on
+
+When NRT is turned on, use nullability annotations (?) on attributes and relationships. This makes Entity Framework Core generate non-nullable columns. And model errors are returned when non-nullable fields are omitted.
+
+The [Entity Framework Core guide on NRT](https://docs.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types) recommends to use constructor binding to initialize non-nullable properties, but JsonApiDotNetCore does not support that. For required navigation properties, it suggests to use a non-nullable property with a nullable backing field. JsonApiDotNetCore does not support that either. In both cases, just use the null-forgiving operator (!).
+
+When ModelState validation is turned on, to-many relationships must be assigned an empty collection. Otherwise an error is returned when they don't occur in the request body.
+
+Example:
+
+```c#
+#nullable enable
+
+public sealed class Label : Identifiable
+{
+ [Attr]
+ public string Name { get; set; } = null!;
+
+ [Attr]
+ public string? RgbColor { get; set; }
+
+ [HasOne]
+ public Person Creator { get; set; } = null!;
+
+ [HasOne]
+ public Label? Parent { get; set; }
+
+ [HasMany]
+ public ISet TodoItems { get; set; } = new HashSet();
+}
+```
diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md
index 2495419a6a..8776041e98 100644
--- a/docs/usage/resources/relationships.md
+++ b/docs/usage/resources/relationships.md
@@ -11,24 +11,106 @@ The left side of a relationship is where the relationship is declared, the right
This exposes a to-one relationship.
```c#
-public class TodoItem : Identifiable
+#nullable enable
+
+public class TodoItem : Identifiable
{
[HasOne]
- public Person Owner { get; set; }
+ public Person? Owner { get; set; }
}
```
The left side of this relationship is of type `TodoItem` (public name: "todoItems") and the right side is of type `Person` (public name: "persons").
+### Required one-to-one relationships in Entity Framework Core
+
+By default, Entity Framework Core generates an identifying foreign key for a required 1-to-1 relationship.
+This means no foreign key column is generated, instead the primary keys point to each other directly.
+
+The next example defines that each car requires an engine, while an engine is optionally linked to a car.
+
+```c#
+#nullable enable
+
+public sealed class Car : Identifiable
+{
+ [HasOne]
+ public Engine Engine { get; set; } = null!;
+}
+
+public sealed class Engine : Identifiable
+{
+ [HasOne]
+ public Car? Car { get; set; }
+}
+
+public sealed class AppDbContext : DbContext
+{
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ builder.Entity()
+ .HasOne(car => car.Engine)
+ .WithOne(engine => engine.Car)
+ .HasForeignKey()
+ .IsRequired();
+ }
+}
+```
+
+Which results in Entity Framework Core generating the next database objects:
+```sql
+CREATE TABLE "Engine" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ CONSTRAINT "PK_Engine" PRIMARY KEY ("Id")
+);
+CREATE TABLE "Cars" (
+ "Id" integer NOT NULL,
+ CONSTRAINT "PK_Cars" PRIMARY KEY ("Id"),
+ CONSTRAINT "FK_Cars_Engine_Id" FOREIGN KEY ("Id") REFERENCES "Engine" ("Id")
+ ON DELETE CASCADE
+);
+```
+
+That mechanism does not make sense for JSON:API, because patching a relationship would result in also
+changing the identity of a resource. Naming the foreign key explicitly fixes the problem by forcing to
+create a foreign key column.
+
+```c#
+protected override void OnModelCreating(ModelBuilder builder)
+{
+ builder.Entity()
+ .HasOne(car => car.Engine)
+ .WithOne(engine => engine.Car)
+ .HasForeignKey("EngineId") // Explicit foreign key name added
+ .IsRequired();
+}
+```
+
+Which generates the correct database objects:
+```sql
+CREATE TABLE "Engine" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ CONSTRAINT "PK_Engine" PRIMARY KEY ("Id")
+);
+CREATE TABLE "Cars" (
+ "Id" integer GENERATED BY DEFAULT AS IDENTITY,
+ "EngineId" integer NOT NULL,
+ CONSTRAINT "PK_Cars" PRIMARY KEY ("Id"),
+ CONSTRAINT "FK_Cars_Engine_EngineId" FOREIGN KEY ("EngineId") REFERENCES "Engine" ("Id")
+ ON DELETE CASCADE
+);
+CREATE UNIQUE INDEX "IX_Cars_EngineId" ON "Cars" ("EngineId");
+```
+
## HasMany
This exposes a to-many relationship.
```c#
-public class Person : Identifiable
+public class Person : Identifiable
{
[HasMany]
- public ICollection TodoItems { get; set; }
+ public ICollection TodoItems { get; set; } = new HashSet();
}
```
@@ -44,7 +126,9 @@ which would expose the relationship to the client the same way as any other `Has
However, under the covers it would use the join type and Entity Framework Core's APIs to get and set the relationship.
```c#
-public class Article : Identifiable
+#nullable disable
+
+public class Article : Identifiable
{
// tells Entity Framework Core to ignore this property
[NotMapped]
@@ -68,10 +152,11 @@ There are two ways the exposed relationship name is determined:
2. Individually using the attribute's constructor.
```c#
-public class TodoItem : Identifiable
+#nullable enable
+public class TodoItem : Identifiable
{
[HasOne(PublicName = "item-owner")]
- public Person Owner { get; set; }
+ public Person Owner { get; set; } = null!;
}
```
@@ -80,10 +165,12 @@ public class TodoItem : Identifiable
Relationships can be marked to disallow including them using the `?include=` query string parameter. When not allowed, it results in an HTTP 400 response.
```c#
-public class TodoItem : Identifiable
+#nullable enable
+
+public class TodoItem : Identifiable
{
[HasOne(CanInclude: false)]
- public Person Owner { get; set; }
+ public Person? Owner { get; set; }
}
```
@@ -95,25 +182,24 @@ Your resource may expose a calculated property, whose value depends on a related
So for the calculated property to be evaluated correctly, the related entity must always be retrieved. You can achieve that using `EagerLoad`, for example:
```c#
-public class ShippingAddress : Identifiable
+#nullable enable
+
+public class ShippingAddress : Identifiable
{
[Attr]
- public string Street { get; set; }
+ public string Street { get; set; } = null!;
[Attr]
- public string CountryName
- {
- get { return Country.DisplayName; }
- }
+ public string? CountryName => Country?.DisplayName;
// not exposed as resource, but adds .Include("Country") to the query
[EagerLoad]
- public Country Country { get; set; }
+ public Country? Country { get; set; }
}
public class Country
{
- public string IsoCode { get; set; }
- public string DisplayName { get; set; }
+ public string IsoCode { get; set; } = null!;
+ public string DisplayName { get; set; } = null!;
}
```
diff --git a/docs/usage/routing.md b/docs/usage/routing.md
index 0a10831d9b..c68914a04a 100644
--- a/docs/usage/routing.md
+++ b/docs/usage/routing.md
@@ -23,15 +23,15 @@ Which results in URLs like: https://yourdomain.com/api/v1/people
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.
```c#
-public class OrderLine : Identifiable
+public class OrderLine : Identifiable
{
}
-public class OrderLineController : JsonApiController
+public class OrderLineController : JsonApiController
{
- public OrderLineController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IResourceService resourceService)
- : base(options, loggerFactory, resourceService)
+ public OrderLineController(IJsonApiOptions options, IResourceGraph resourceGraph,
+ ILoggerFactory loggerFactory, IResourceService resourceService)
+ : base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
@@ -45,7 +45,7 @@ The exposed name of the resource ([which can be customized](~/usage/resource-gra
### Non-JSON:API controllers
-If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#customize-serializer-options) is applied to the name of the controller.
+If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#customize-serializer-options) is applied to the name of the controller.
```c#
public class OrderLineController : ControllerBase
@@ -63,11 +63,11 @@ It is possible to bypass the default routing convention for a controller.
```c#
[Route("v1/custom/route/lines-in-order"), DisableRoutingConvention]
-public class OrderLineController : JsonApiController
+public class OrderLineController : JsonApiController
{
- public OrderLineController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IResourceService resourceService)
- : base(options, loggerFactory, resourceService)
+ public OrderLineController(IJsonApiOptions options, IResourceGraph resourceGraph,
+ ILoggerFactory loggerFactory, IResourceService resourceService)
+ : base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
diff --git a/docs/usage/toc.md b/docs/usage/toc.md
index 10fee6bc72..fabef61b68 100644
--- a/docs/usage/toc.md
+++ b/docs/usage/toc.md
@@ -1,6 +1,7 @@
# [Resources](resources/index.md)
## [Attributes](resources/attributes.md)
## [Relationships](resources/relationships.md)
+## [Nullability](resources/nullability.md)
# Reading data
## [Filtering](reading/filtering.md)
diff --git a/docs/usage/writing/bulk-batch-operations.md b/docs/usage/writing/bulk-batch-operations.md
index 549ff68025..21fe04b636 100644
--- a/docs/usage/writing/bulk-batch-operations.md
+++ b/docs/usage/writing/bulk-batch-operations.md
@@ -17,10 +17,10 @@ To enable operations, add a controller to your project that inherits from `JsonA
```c#
public sealed class OperationsController : JsonApiOperationsController
{
- public OperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory,
- IOperationsProcessor processor, IJsonApiRequest request,
- ITargetedFields targetedFields)
- : base(options, loggerFactory, processor, request, targetedFields)
+ public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph,
+ ILoggerFactory loggerFactory, IOperationsProcessor processor,
+ IJsonApiRequest request, ITargetedFields targetedFields)
+ : base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
{
}
}
diff --git a/inspectcode.ps1 b/inspectcode.ps1
index ab4b9c95dd..16dccfd373 100644
--- a/inspectcode.ps1
+++ b/inspectcode.ps1
@@ -8,15 +8,9 @@ if ($LASTEXITCODE -ne 0) {
throw "Tool restore failed with exit code $LASTEXITCODE"
}
-dotnet build -c Release
-
-if ($LASTEXITCODE -ne 0) {
- throw "Build failed with exit code $LASTEXITCODE"
-}
-
$outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml')
$resultPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.html')
-dotnet jb inspectcode JsonApiDotNetCore.sln --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal
+dotnet jb inspectcode JsonApiDotNetCore.sln --build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal
if ($LASTEXITCODE -ne 0) {
throw "Code inspection failed with exit code $LASTEXITCODE"
diff --git a/src/Examples/GettingStarted/Controllers/BooksController.cs b/src/Examples/GettingStarted/Controllers/BooksController.cs
index 17e1c1417d..3f049429cd 100644
--- a/src/Examples/GettingStarted/Controllers/BooksController.cs
+++ b/src/Examples/GettingStarted/Controllers/BooksController.cs
@@ -6,10 +6,10 @@
namespace GettingStarted.Controllers
{
- public sealed class BooksController : JsonApiController
+ public sealed class BooksController : JsonApiController
{
- public BooksController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService)
- : base(options, loggerFactory, resourceService)
+ public BooksController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService resourceService)
+ : base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs
index c7600be15a..e7a5537f14 100644
--- a/src/Examples/GettingStarted/Controllers/PeopleController.cs
+++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs
@@ -6,10 +6,11 @@
namespace GettingStarted.Controllers
{
- public sealed class PeopleController : JsonApiController
+ public sealed class PeopleController : JsonApiController
{
- public PeopleController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService)
- : base(options, loggerFactory, resourceService)
+ public PeopleController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
diff --git a/src/Examples/GettingStarted/Data/SampleDbContext.cs b/src/Examples/GettingStarted/Data/SampleDbContext.cs
index b54011ff14..c5460db810 100644
--- a/src/Examples/GettingStarted/Data/SampleDbContext.cs
+++ b/src/Examples/GettingStarted/Data/SampleDbContext.cs
@@ -7,16 +7,11 @@ namespace GettingStarted.Data
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public class SampleDbContext : DbContext
{
- public DbSet Books { get; set; }
+ public DbSet Books => Set();
public SampleDbContext(DbContextOptions