JSONAPI.NET is no longer actively maintained. It is not recommended for use in new projects. It does NOT work with .NET Core. Please consider one of the other .NET server implementations instead.
JSONAPI.NET is a set of utility classes that aim to make it possible to implement JSON API spec compliant RESTful web services quickly and easily using ASP.NET MVC WebAPI. In practice, the primary target is to build backend web services that work with the Ember Data library and Ember.js. Since the JSON API spec project originated from the Ember Data and Active Model Serializers project, Ember Data's included RESTAdapter is very close to JSON API spec compliant--but not quite. You will need to use the separately available JSON API Adapter to connect with Ember Data, and it is currently a primary goal of this project to work well with this adapter (coverage of the whole JSON API spec is far from complete).
At its most basic level, JSONAPI includes a Json.NET JsonMediaTypeFormatter
, which can be used to transform WebAPI JSON output into JSON API spec compliant messages. You can use the media formatter by adding an instance of it to the Json.NET Formatters collection, probably in your WebApiConfig class, like this:
public static void Register(HttpConfiguration config)
{
//...
JsonApiFormatter formatter = new JSONAPI.Json.JsonApiFormatter();
formatter.PluralizationService = new JSONAPI.Core.PluralizationService();
GlobalConfiguration.Configuration.Formatters.Add(formatter);
//...
}
In connection with the media formatter, a PluralizationService
is required to translate between singular and plural object names, since the spec (usually?) uses plural class/type names, and your class names are likely singular. The JSONAPI.Core.PluralizationService
is provided as a horribly naive sample implementation (it simply adds or strips "s" from the end of the class name), but at least lets you manually add explicit mappings with the AddMapping(string singular, string plural)
method.
ℹ️ HINT: the
JSONAPI.EntityFramework
library includes aPluralizationService
that uses EntityFramework's much more sophisticated inflector!
That will configure a working formatter. Note that the formatter (by default) will only handle requests having an Accept
or Content-type
header of application/vnd.api+json
, the media type defined for the JSON API spec.
Essentially, that's it! At this point your WebAPI methods should be able to produce and consume JSON that conforms to (a subset of) the JSON API spec! There is, of course, much more that you can configure.
One of the benefits of the JSON API spec is that it provides several ways to serialize relationships. This gives you a lot of flexibility to improve your API performance for both the server and client: If related objects are expensive to retrieve or compute, you can represent the relationship as a URL to avoid that cost if the client never accesses it. If it is not expensive, or you know the client will need them, you can bundle them in the same response to reduce the number of network requests. JSONAPI.NET provides you three attributes you can use to decorate your models to specify how a relationship property will be serialized.
SerializeAs
: Decorate a property with this attribute to specify that the related objects should be serialized as ids, a link, or with the serialized object(s) embedded within the parent object (embedding is NOT part of the spec, but is supported by Ember Data!)IncludeInPayload
: Decorate a property with this attribute to cause the related objects to be included with the parent object in the same response message. This is called a Compound Document in the spec. This only makes sense to use with[SerializeAs(SerializeAsOptions.Ids)]
.LinkTemplate
: If you use[SerializeAs(SerializeAsOptions.Link)]
, you must also use this attribute to specify the format of the link sent to the client. Two template parameters are supported,{0}
will be replaced with the Id(s) of the related object, and{1}
will be replaced with the Id of the parent object.
ℹ️ As a side note, the RelationAggregator class handles the work of pulling any included related objects into the document--which may be a recursive process: for example, if an object has a relationship of its own type and that relationship property is annotated to be included, then it will recursively include all related objects of the same type! However, those related objects will all be included in a flat array for that type, according to the spec.
JSONAPI defines the URL query parameter for filtering is filter
and should be combined with the associations but there is not much more how the syntax should be. So the JSONAPI.Net framework provides the following filter syntax.
If you want to get a Resource by Id you provide the Id in the URL as part of the path e.g:
/posts/1
With this call you get the post with Id 1 or a status code 404 if no post with the Id 1 exists.
If you would filter all related comments on post with the Id 1 you append the name of relationship e.g:
/posts/1/comments
In both above cases you can add the filter
query parameter to filter the result on non Id properties. The property to filter on is specified in squared brackets like below.
The value(s) after the equal sing we would call filter value.
/posts/1/comments?filter[autor]=Bob
This will only return objects where "Bob" is the value of the author property.
/posts/1/comments?filter[category]=3
This will only return objects where 3 is the value of the category property.
If you want to filter by multiple values you can concatenate the values separated by comma. In case of strings you need to quote the strings to provide multiple values.
/posts?filter[title]=Post one, which is awesome
=> this returns all posts with title "Post one, which is awesome"
/posts?filter[title]="Post one","which is awesome"
=> this returns all posts with title "Post one" OR "which is awesome"
If the field is numeric or DateTime you can concatenate values with comma.
/posts?filter[category]=1,2,3
=> this returns all posts with category 1 OR 2 OR 3
/posts?filter[date-created]=2016-09-01,2016-09-02
=> this returns all posts with date-created 2016-09-01 OR 2016-09-02
In case of string properties you can provide a percent sign (%) at the beginning or end of the filter value. This will advice to not compare with equal but with contains.
/posts?filter[title]="%one","%awesome%"
=> this returns all posts with title ending on "one" OR containing the word "awesome"
ℹ️ HINT: the comparison with wildcards is made case insensitive.
ℹ️ HINT: If there is a comma inside of the quoted filter value the term gets not split.
ℹ️ HINT: The percent sign is used to start an encoded character in the URL so the filter values must unconditionally be encoded before put in an URL. The above example should look like this when sent to server:
filter%5Btitle%5D=%22%25one%22%2C%22%25awesome%25%22
DateTime filters with equal can be a pain. If you store DateTime with full resolution (milliseconds) you must provide the full resolution to make the filter value equal the stored value.
To avoid this problem JSONAPI.Net is automatically filtering by a DateTime range. If you provide a "day" (YYYY-MM-DD) as filter value the filter will be this: BETWEEN day 00:00:00.000 AND day 23:59:59.999
.
Now if the property is DateTime or DateTimeOffset you can provide the following types of filter values:
Part | Format | Filter |
---|---|---|
year | YYYY | YYYY-01-01 00:00:00.000 - YYYY-12-31 23:59:59.999 |
month* | YYYY-MM | YYYY-MM-01 00:00:00.000 - YYYY-MM-31 23:59:59.999 |
day | YYYY-MM-DD | YYYY-MM-DD 00:00:00.000 - YYYY-MM-DD 23:59:59.999 |
hour | YYYY-MM-DD HH | YYYY-MM-DD HH:00:00.000 - YYYY-MM-DD HH:59:59.999 |
minute | YYYY-MM-DD HH:mm | YYYY-MM-DD HH:mm:00.000 - YYYY-MM-DD HH:mm:59.999 |
second | YYYY-MM-DD HH:mm:ss | YYYY-MM-DD HH:mm:ss.000 - YYYY-MM-DD HH:mm:ss.999 |
*) assuming this month has 31 days. JSONAPI.Net automatically determines the last day of month by adding one month to the given date. This respects months with less than 31 days.
If you want to filter all posts created in month May of year 2016 you must provide the format for month filled by your needed date:
/posts?filter[date-created]=2016-05
=> this returns all posts with date-created in may 2016
There is no implementation on filtering number or date ranges. If you need things like that you can open an issue or even better provide a pull request.
Filters can be combined for multiple fields. You can filter posts created in May 2016 and containing awesome in title:
/posts?filter[date-created]=2016-05&filter[title]="%awesome%"
=> this returns all posts with date-created in may 2016 with title containing "awesome"
So we have the following standard behavior to concatenate multiple filters together:
- multiple values on the same property are concatenated with OR
- multiple filters on multiple properties are always concatenated with AND
There is no implementation on filtering with a tree of logical AND and OR operations other than described above. If you need things like that you can open an issue or even better provide a pull request.
JSONAPI.Net intends to be so much more than just a media formatter. It provides subclasses of WebAPI's ApiController
that automate some of the boilerplate that goes into building a web service, especially one that can comply with the JSON API spec. While JSONAPI.ApiController
doesn't do much, it can be subclassed to make default actions for the HTTP methods that WebAPI supports.
A JSONAPI.IMaterializer
object can be added to that ApiController
to broker between the WebAPI layer and your persistence layer. If you create an implementation of IMaterializer
, you can begin to use the base ApiController
's handler methods, such as the Get() handler--which already handles the JSON API spec for requesting multiple objects with a list of comma-separated Ids!
The classes in the JSONAPI.EntityFramework
namespace take great advantage of the patterns set out in the JSONAPI
namespace. The EntityFrameworkMaterializer
is an IMaterializer
that can operate with your own DbContext
class to retrieve objects by Id/Primary Key, and can retrieve and update existing objects from your context in a way that Entity Framework expects for change tracking…that means, in theory, you can use the provided JSONAPI.EntityFramework.ApiController
base class to handle GET, PUT, POST, and DELETE without writing any additional code! You will still almost certainly subclass ApiController
to implement your business logic, but that means you only have to worry about your business logic--not implementing the JSON API spec or messing with your persistence layer.
- Add some hints about the configuration of JSONAPI.EntityFramework
To change your entities before they get persisted you can extend the EntityFrameworkDocumentMaterializer<T>
class. You need to register your custom DocumentMaterializer in your JsonApiConfiguration
like that:
configuration.RegisterEntityFrameworkResourceType<MyEntityType>(c =>c.UseDocumentMaterializer<CustomDocumentMaterializer>());
Afterwards you can override the OnCreate
, OnUpdate
or OnDelete
methods in your CustomDocumentMaterializer
.
protected override async Task OnCreate(Task<MyEntityType> record)
{
await base.OnUpdate(record);
var entity = await record;
entity.CreatedOn = DateTime.Now;
entity.CreatedBy = Principal?.Identity;
}
ℹ️ HINT: To get the
Principal
you can add the following part into yourStartup.cs
which registers thePrincipal
in Autofac and define a constructor Parameter on yourCustomDocumentMaterializer
of typeIPrincipal
.
configurator.OnApplicationLifetimeScopeCreating(builder =>
{
// ...
builder.Register(ctx => HttpContext.Current.GetOwinContext()).As<IOwinContext>();
builder.Register((c, p) =>
{
var owin = c.Resolve<IOwinContext>();
return owin.Authentication.User;
})
.As<IPrincipal>()
.InstancePerRequest();
}
Per default the routes created for the registered models from EntityFramework will appear in root folder. This can conflict with folders of the file system or other routes you may want to serve from the same project.
To solve the issue we can create an instance of the BaseUrlService
and put it in the configuration.
The BaseUrlService
can be created with the context path parameter which will be used to register the routes and put into responses.
var configuration = new JsonApiConfiguration();
configuration.RegisterEntityFrameworkResourceType<Post>();
// ... registration stuff you need
// this makes JSONAPI.NET create the route 'api/posts'
configuration.CustomBaseUrlService = new BaseUrlService("api");
Since JSONAPI.NET returns urls it could result wrong links if JSONAPI runs behind a reverse proxy. Configure the public origin address as follows:
var configuration = new JsonApiConfiguration();
configuration.RegisterEntityFrameworkResourceType<Post>();
// ... registration stuff you need
// this makes JSONAPI.NET to set the urls in responses to https://api.example.com/posts
// Important: don't leave the second string parameter! see below.
configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.example.com/"), "");
// this can also be combined with the context paht for routing like that:
// this makes JSONAPI.NET create the route 'api/posts' and response urls to be https://api.example.com:9443/api/posts
configuration.CustomBaseUrlService = new BaseUrlService(new Uri("https://api.example.com:9443/"), "api");
When using Entity Framework you can register type EntityFrameworkQueryableResourceCollectionDocumentBuilder
to enable the total-pages
and total-count
meta properties.
When pagination is used the total-pages
and total-count
meta properties are provided to indicate the number of pages and records to the client.