JsonApiDotnetCore provides a framework for building json:api compliant web servers. Unlike other .Net implementations, this library provides all the required middleware to build a complete server. All you need to focus on is defining the resources. However, the library is also fully extensible so you can customize the implementation to meet your specific needs.
- Comprehensive Demo
- Installation
- Generators
- Usage
- Tests
The following is a WIP demo showing how to create a web application using this library, EmberJS and PostgreSQL. If there are specific topics you'd like to see in future videos, comment on the playlist.
- Visual Studio
Install-Package JsonApiDotnetCore
- project.json
"JsonApiDotNetCore": "1.3.0"
- *.csproj
<ItemGroup>
<!-- ... -->
<PackageReference Include="JsonApiDotNetCore" Version="1.3.0" />
</ItemGroup>
Click here for the latest NuGet version.
For pre-releases, add the MyGet package feed (https://www.myget.org/F/research-institute/api/v3/index.json) to your nuget configuration.
You can install the Yeoman generators to make building applications much easier.
You need to do 3 things:
- Add Middleware and Services
- Define Models
- Define Controllers
I recommend reading the details below, but once you're familiar with the setup, you can use the Yeoman generator to generate the required classes.
Add the following to your Startup.ConfigureServices
method.
Replace AppDbContext
with your DbContext.
services.AddJsonApi<AppDbContext>();
Add the middleware to the Startup.Configure
method.
Note that under the hood, this will call app.UseMvc()
so there is no need to add that as well.
app.UseJsonApi();
Your models should inherit Identifiable<TId>
where TId
is the type of the primary key, like so:
public class Person : Identifiable<Guid>
{ }
You can use the non-generic Identifiable
if your primary key is an integer:
public class Person : Identifiable
{ }
If you need to hang annotations or attributes on the Id
property, you can override the virtual member:
public class Person : Identifiable
{
[Key]
[Column("person_id")]
public override int Id { get; set; }
}
If you want an attribute on your model to be publicly available,
add the AttrAttribute
and provide the outbound name.
public class Person : Identifiable<int>
{
[Attr("first-name")]
public string FirstName { get; set; }
}
In order for navigation properties to be identified in the model,
they should be labeled with the appropriate attribute (either HasOne
or HasMany
).
public class Person : Identifiable<int>
{
[Attr("first-name")]
public string FirstName { get; set; }
[HasMany("todo-items")]
public virtual List<TodoItem> TodoItems { get; set; }
}
Dependent relationships should contain a property in the form {RelationshipName}Id
.
For example, a TodoItem
may have an Owner
and so the Id attribute should be OwnerId
like so:
public class TodoItem : Identifiable<int>
{
[Attr("description")]
public string Description { get; set; }
public int OwnerId { get; set; }
[HasOne("owner")]
public virtual Person Owner { get; set; }
}
If a DbContext is specified when adding the services, the context will be used to define the resources and their names.
public DbSet<MyModel> SomeModels { get; set; } // this will be translated into "some-models"
However, you can specify a custom name like so:
[Resource("some-models")]
public DbSet<MyModel> MyModels { get; set; } // this will be translated into "some-models"
For further resource customizations, please see the section on Defining Custom Data Access Methods.
You need to create controllers that inherit from JsonApiController<TEntity>
or JsonApiController<TEntity, TId>
where TEntity
is the model that inherits from Identifiable<TId>
.
[Route("api/[controller]")]
public class ThingsController : JsonApiController<Thing>
{
public ThingsController(
IJsonApiContext jsonApiContext,
IResourceService<Thing> resourceService,
ILoggerFactory loggerFactory)
: base(jsonApiContext, resourceService, loggerFactory)
{ }
}
If your model is using a type other than int
for the primary key,
you should explicitly declare it in the controller
and repository generic type definitions:
[Route("api/[controller]")]
public class ThingsController : JsonApiController<Thing, Guid>
{
public ThingsController(
IJsonApiContext jsonApiContext,
IResourceService<Thing, Guid> resourceService,
ILoggerFactory loggerFactory)
: base(jsonApiContext, resourceService, loggerFactory)
{ }
}
By default the library will configure routes for each controller. Based on the recommendations outlined in the JSONAPI spec, routes are hyphenated. For example:
/todo-items --> TodoItemsController
NOT /todoItems
You can add a namespace to the URL by specifying it in ConfigureServices
:
services.AddJsonApi<AppDbContext>(
opt => opt.Namespace = "api/v1");
You can disable the dasherized convention and specify your own template
by using the DisableRoutingConvention
Attribute.
[Route("[controller]")]
[DisableRoutingConvention]
public class CamelCasedModelsController : JsonApiController<CamelCasedModel>
{
public CamelCasedModelsController(
IJsonApiContext jsonApiContext,
IResourceService<CamelCasedModel> resourceService,
ILoggerFactory loggerFactory)
: base(jsonApiContext, resourceService, loggerFactory)
{ }
}
It is important to note that your routes must still end with the model name in the same format
as the resource name. This is so that we can build accurrate resource links in the json:api document.
For example, if you define a resource as MyModels
the controller route must match:
// resource definition
builder.AddResource<TodoItem>("myModels");
// controller definition
[Route("api/myModels")]
[DisableRoutingConvention]
public class TodoItemsController : JsonApiController<TodoItem>
{ //...
}
By default, data retrieval is distributed across 3 layers:
JsonApiController
EntityResourceService
DefaultEntityRepository
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.
Out of the box, the library uses your DbContext
to create a "ContextGraph" or map of all your models and their relationships. If, however, you have models that are not members of a DbContext
, you can manually create this graph like so:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
var mvcBuilder = services.AddMvc();
services.AddJsonApi(options => {
options.Namespace = "api/v1";
options.BuildContextGraph((builder) => {
builder.AddResource<MyModel>("my-models");1
});
}, mvcBuilder);
// ...
}
By default, this library uses Entity Framework. If you'd like to use another ORM that does not implement IQueryable
, you can inject a custom service like so:
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IResourceService<MyModel>, MyModelService>();
// ...
}
// MyModelService.cs
public class MyModelService : IResourceService<MyModel>
{
private readonly IMyModelDAL _dal;
public MyModelService(IMyModelDAL dal)
{
_dal = dal;
}
public Task<IEnumerable<MyModel>> GetAsync()
{
return await _dal.GetModelAsync();
}
}
If you want to use EF, but need additional data access logic (such as authorization), you can implement custom methods for accessing the data by creating an implementation of
IEntityRepository<TEntity, TId>
. If you only need minor changes you can override the
methods defined in DefaultEntityRepository<TEntity, TId>
. The repository should then be
add to the service collection in Startup.ConfigureServices
like so:
services.AddScoped<IEntityRepository<MyEntity,Guid>, MyAuthorizedEntityRepository>();
A sample implementation might look like:
public class MyAuthorizedEntityRepository : DefaultEntityRepository<MyEntity>
{
private readonly ILogger _logger;
private readonly AppDbContext _context;
private readonly IAuthenticationService _authenticationService;
public MyAuthorizedEntityRepository(AppDbContext context,
ILoggerFactory loggerFactory,
IJsonApiContext jsonApiContext,
IAuthenticationService authenticationService)
: base(context, loggerFactory, jsonApiContext)
{
_context = context;
_logger = loggerFactory.CreateLogger<MyEntityRepository>();
_authenticationService = authenticationService;
}
public override IQueryable<MyEntity> Get()
{
return base.Get().Where(e => e.UserId == _authenticationService.UserId);
}
}
For more examples, take a look at the customization tests
in ./test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility
.
Resources can be paginated. The following query would set the page size to 10 and get page 2.
?page[size]=10&page[number]=2
If you would like pagination implemented by default, you can specify the page size when setting up the services:
services.AddJsonApi<AppDbContext>(
opt => opt.DefaultPageSize = 10);
Total Record Count
The total number of records can be added to the document meta by setting it in the options:
services.AddJsonApi<AppDbContext>(opt =>
{
opt.DefaultPageSize = 5;
opt.IncludeTotalRecordCount = true;
});
You can filter resources by attributes using the filter
query parameter.
By default, all attributes are filterable.
The filtering strategy we have selected, uses the following form:
?filter[attribute]=value
For operations other than equality, the query can be prefixed with an operation identifier):
?filter[attribute]=eq:value
?filter[attribute]=lt:value
?filter[attribute]=gt:value
?filter[attribute]=le:value
?filter[attribute]=ge:value
?filter[attribute]=like:value
You can customize the filter implementation by overriding the method in the DefaultEntityRepository
like so:
public class MyEntityRepository : DefaultEntityRepository<MyEntity>
{
public MyEntityRepository(
AppDbContext context,
ILoggerFactory loggerFactory,
IJsonApiContext jsonApiContext)
: base(context, loggerFactory, jsonApiContext)
{ }
public override IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery)
{
// use the base filtering method
entities = base.Filter(entities, filterQuery);
// implement custom method
return ApplyMyCustomFilter(entities, filterQuery);
}
}
Resources can be sorted by an attribute:
?sort=attribute // ascending
?sort=-attribute // descending
Meta objects can be assigned in two ways:
- Resource meta
- Request Meta
Resource meta can be defined by implementing IHasMeta
on the model class:
public class Person : Identifiable<int>, IHasMeta
{
// ...
public Dictionary<string, object> GetMeta(IJsonApiContext context)
{
return new Dictionary<string, object> {
{ "copyright", "Copyright 2015 Example Corp." },
{ "authors", new string[] { "Jared Nance" } }
};
}
}
Request Meta can be added by injecting a service that implements IRequestMeta
.
In the event of a key collision, the Request Meta will take precendence.
By default, the server will respond with a 403 Forbidden
HTTP Status Code if a POST
request is
received with a client generated id. However, this can be allowed by setting the AllowClientGeneratedIds
flag in the options:
services.AddJsonApi<AppDbContext>(opt =>
{
opt.AllowClientGeneratedIds = true;
// ..
});
By default, errors will only contain the properties defined by the internal Error class. However, you can create your own by inheriting from Error
and either throwing it in a JsonApiException
or returning the error from your controller.
// custom error definition
public class CustomError : Error {
public CustomError(string status, string title, string detail, string myProp)
: base(status, title, detail)
{
MyCustomProperty = myProp;
}
public string MyCustomProperty { get; set; }
}
// throwing a custom error
public void MyMethod() {
var error = new CustomError("507", "title", "detail", "custom");
throw new JsonApiException(error);
}
// returning from controller
[HttpPost]
public override async Task<IActionResult> PostAsync([FromBody] MyEntity entity)
{
if(_db.IsFull)
return new ObjectResult(new CustomError("507", "Database is full.", "Theres no more room.", "Sorry."));
// ...
}
We currently support top-level field selection.
What this means is you can restrict which fields are returned by a query using the fields
query parameter, but this does not yet apply to included relationships.
- Currently valid:
GET /articles?fields[articles]=title,body HTTP/1.1
Accept: application/vnd.api+json
- Not yet supported:
GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1
Accept: application/vnd.api+json
I am using DotNetCoreDocs to generate sample requests and documentation.
- To run the tests, start a postgres server and verify the connection properties define in
/test/JsonApiDotNetCoreExampleTests/appsettings.json
cd ./test/JsonApiDotNetCoreExampleTests
dotnet test
cd ./src/JsonApiDotNetCoreExample
dotnet run
open http://localhost:5000/docs