Table of Contents

Common Pitfalls

This section lists various problems we've seen users run into over the years when using JsonApiDotNetCore. See also Frequently Asked Questions.

JSON:API resources are not DTOs or ViewModels

This is a common misconception. Similar to a database model, which consists of tables and foreign keys, JSON:API defines resources that are connected via relationships. You're opening up a can of worms when trying to model a single table to multiple JSON:API resources.

This is best clarified using an example. Let's assume we're building a public website and an admin portal, both using the same API. The API uses the database tables "Customers" and "LoginAccounts", having a one-to-one relationship between them.

Now let's try to define the resource classes:

[Table("Customers")]
public sealed class WebCustomer : Identifiable<long>
{
    [Attr]
    public string Name { get; set; } = null!;

    [HasOne]
    public LoginAccount? Account { get; set; }
}

[Table("Customers")]
public sealed class AdminCustomer : Identifiable<long>
{
    [Attr]
    public string Name { get; set; } = null!;

    [Attr]
    public string? CreditRating { get; set; }

    [HasOne]
    public LoginAccount? Account { get; set; }
}

[Table("LoginAccounts")]
public sealed class LoginAccount : Identifiable<long>
{
    [Attr]
    public string EmailAddress { get; set; } = null!;

    [HasOne]
    public ??? Customer { get; set; }
}

Did you notice the missing type of the LoginAccount.Customer property? We must choose between WebCustomer or AdminCustomer, but neither is correct. This is only one of the issues you'll run into. Just don't go there.

The right way to model this is by having only Customer instead of WebCustomer and AdminCustomer. And then:

  • Hide the CreditRating property for web users using this approach.
  • Block web users from setting the CreditRating property from POST/PATCH resource endpoints by either:
    • Detecting if the CreditRating property has changed, such as done here.
    • Injecting ITargetedFields, throwing an error when it contains the CreditRating property.

JSON:API resources are not DDD domain entities

In Domain-driven design, it's considered best practice to implement business rules inside entities, with changes being controlled through an aggregate root. This paradigm doesn't work well with JSON:API, because each resource can be changed in isolation. So if your API needs to guard invariants such as "the sum of all orders must never exceed 500 dollars", then you're better off with an RPC-style API instead of the REST paradigm that JSON:API follows.

Adding constructors to resource classes that validate incoming parameters before assigning them to properties does not work. Entity Framework Core supports that, but does so via internal implementation details that are inaccessible by JsonApiDotNetCore.

In JsonApiDotNetCore, resources are what DDD calls anemic models. Validation and business rules are typically implemented in Resource Definitions.

Model relationships instead of foreign key attributes

It may be tempting to expose numeric resource attributes such as customerId, orderId, etc. You're better off using relationships instead, because they give you the richness of JSON:API. For example, it enables users to include related resources in a single request, apply filters over related resources and use dedicated endpoints for updating relationships. As an API developer, you'll benefit from rich input validation and fine-grained control for setting what's permitted when users access relationships.

Model relationships instead of complex (JSON) attributes

Similar to the above, returning a complex object takes away all the relationship features of JSON:API. Users can't filter inside a complex object. Or update a nested value, without risking accidentally overwriting another unrelated nested value from a concurrent request. Basically, there's no partial PATCH to prevent that.

Stay away from stored procedures

There are many reasons to not use stored procedures. But with JSON:API, there's an additional concern. Due to its dynamic nature of filtering, sorting, pagination, sparse fieldsets, and including related resources, the number of required stored procedures to support all that either explodes, or you'll end up with one extremely complex stored proceduce to handle it all. With stored procedures, you're either going to have a lot of work to do, or you'll end up with an API that has very limited capabilities. Neither sounds very compelling. If stored procedures is what you need, you're better off creating an RPC-style API that doesn't use JsonApiDotNetCore.

Do not use [ApiController] on JSON:API controllers

Although recommended by Microsoft for hard-written controllers, the opinionated behavior of [ApiController] violates the JSON:API specification. Despite JsonApiDotNetCore trying its best to deal with it, the experience won't be as good as leaving it out.

Register/override injectable services

Register your JSON:API resource services, resource definitions and repositories with services.AddResourceService/AddResourceDefinition/AddResourceRepository() instead of services.AddScoped(). When using Auto-discovery, you don't need to register these at all.

Note

In older versions of JsonApiDotNetCore, registering your own services in the IoC container afterwards increased the chances that your replacements would take effect.

Never use the Entity Framework Core In-Memory Database Provider

When using this provider, many invalid mappings go unnoticed, leading to strange errors or wrong behavior. A real SQL engine fails to create the schema when mappings are invalid. If you're in need of a quick setup, use SQLite. After adding its NuGet package, it's as simple as:

// Program.cs
builder.Services.AddSqlite<AppDbContext>("Data Source=temp.db");

Which creates temp.db on disk. Simply deleting the file gives you a clean slate. This is a lot more convenient compared to using SqlLocalDB, which runs a background service that breaks if you delete its underlying storage files.

However, even SQLite does not support all queries produced by Entity Framework Core. You'll get the best (and fastest) experience with PostgreSQL in a docker container.

One-to-one relationships require custom Entity Framework Core mappings

Entity Framework Core has great conventions and sane mapping defaults. But two of them are problematic for JSON:API: identifying foreign keys and default delete behavior. See here for how to get it right.

Prefer model attributes over fluent mappings

Validation attributes such as [Required] are detected by ASP.NET ModelState validation, Entity Framework Core, OpenAPI, and JsonApiDotNetCore. When using a Fluent API instead, the other frameworks cannot know about it, resulting in a less streamlined experience.

Validation of [Required] value types doesn't work

This is a limitation of ASP.NET ModelState validation. For example:

[Required] public int Age { get; set; }

won't cause a validation error when sending 0 or omitting it entirely in the request body. This limitation does not apply to reference types. The workaround is to make it nullable:

[Required] public int? Age { get; set; }

Entity Framework Core recognizes this and generates a non-nullable column.

Don't change resource property values from POST/PATCH controller methods

It simply won't work. Without going into details, this has to do with JSON:API partial POST/PATCH. Use Resource Definition callback methods to apply such changes from code.

You can't mix up pipeline methods

For example, you can't call service.UpdateAsync() from controller.GetAsync(), or call service.SetRelationshipAsync() from controller.PatchAsync(). The reason is that various ambient injectable objects are in play, used to track what's going on during the request pipeline internally. And they won't match up with the current endpoint when switching to a different pipeline halfway during a request.

If you need such side effects, it's easiest to inject your DbContext in the controller, directly apply the changes on it and save. A better way is to inject your DbContext in a Resource Definition and apply the changes there.

Concurrency tokens (timestamp/rowversion/xmin) won't work

While we'd love to support such tokens for optimistic concurrency, it turns out that the implementation is far from trivial. We've come a long way, but aren't sure how it should work when relationship endpoints and atomic operations are involved. If you're interested, we welcome your feedback at https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1119.