An implementation of HATEOAS for aspnet core web api projects which gives full control of which links to apply to models returned from your api. In order to communicate varying state to the end-user, this library fully integrates with Authorization, and allows arbitrary conditions to determine whether to show or hide HATEOAS links between api resources.
Install the package from Nuget.org
PM> Install-Package RiskFirst.Hateoas
This will include the dependency RiskFirst.Hateoas.Models which was introduced in version 3.0.0 to remove the AspNetCore dependencies from assemblies referencing the LinkContainer base classes.
Configure the links to include for each of your models.
public class Startup
{
public void ConfigureServices(IServicesCollection services)
{
services.AddLinks(config =>
{
config.AddPolicy<MyModel>(policy => {
policy.RequireSelfLink()
.RequireRoutedLink("all", "GetAllModelsRoute")
.RequireRoutedLink("delete", "DeleteModelRoute", x => new { id = x.Id });
});
});
}
}
Inject ILinksService
into any controller (or other class in your project) to add links to a model.
[Route("api/[controller]")]
public class MyController : Controller
{
private readonly ILinksService linksService;
public MyController(ILinksService linksService)
{
this.linksService = linksService;
}
[HttpGet("{id}",Name = "GetModelRoute")]
public async Task<MyModel> GetMyModel(int id)
{
var model = await myRepository.GetMyModel(id);
await linksService.AddLinksAsync(model);
return model;
}
[HttpGet(Name="GetAllModelsRoute")]
public async Task<IEnumerable<MyModel>> GetAllModels()
{
//... snip .. //
}
[HttpDelete("{id}",Name = "DeleteModelRoute")]
public async Task<MyModel> DeleteMyModel(int id)
{
//... snip .. //
}
}
The above code would produce a response as the example below
{
"id": 1,
"someOtherField": "foo",
"_links": {
"self": {
"rel": "MyController\\GetModelRoute",
"href": "https://api.example.com/my/1",
"method": "GET"
},
"all": {
"rel": "MyController\\GetAllModelsRoute",
"href": "https://api.example.com/my",
"method": "GET"
},
"delete": {
"rel": "MyController\\DeleteModelRoute",
"href": "https://api.example.com/my/1",
"method": "DELETE"
}
}
}
or if you're using XML
<?xml version="1.0"?>
<MyModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<link href="https://api.example.com/my/1" method="GET" rel="self"/>
<link href="https://api.example.com/my" method="GET" rel="all"/>
<link href="https://api.example.com/my/1" method="DELETE" rel="delete"/>
<Id>1</Id>
<SomeOtherField>foo</SomeOtherField>
</MyModel>
It is possible to specify multiple named policies for a model during startup by providing a policy name to AddPolicy
. For example, you could have the default (unnamed) policy give basic links when the model is part of a list, but more detailed information when a model is returned alone.
public class Startup
{
public void ConfigureServices(IServicesCollection services)
{
services.AddLinks(config =>
{
config.AddPolicy<MyModel>(policy => {
policy.RequireRoutedLink("self","GetModelRoute", x => new {id = x.Id })
});
config.AddPolicy<MyModel>("FullInfo",policy => {
policy.RequireSelfLink()
.RequireRoutedLink("all", "GetAllModelsRoute")
.RequireRoutedLink("parentModels", "GetParentModelRoute", x => new { parentId = x.ParentId });
.RequireRoutedLink("subModels", "GetSubModelsRoute", x => new { id = x.Id });
.RequireRoutedLink("delete", "DeleteModelRoute", x => new { id = x.Id });
});
});
}
}
With a named policy, this can be applied at runtime using an overload of AddLinksAsync
which takes a policy name:
await linksService.AddLinksAsync(model,"FullInfo");
You can also markup your controller method with a LinksAttribute
to override the default policy applied. The below code would apply the "FullInfo" profile to the returned model without having to specify the policy name in the call to AddLinksAsync
.
[Route("api/[controller]")]
public class MyController : Controller
{
private readonly ILinksService linksService;
public MyController(ILinksService linksService)
{
this.linksService = linksService;
}
[HttpGet("{id}",Name = "GetModelRoute")]
[Links(Policy = "FullInfo")]
public async Task<MyModel> GetMyModel(int id)
{
var model = await myRepository.GetMyModel(id);
await linksService.AddLinksAsync(model);
return model;
}
}
Another way to achieve the same thing is to mark the actual object with the LinksAttribute
:
[Links(Policy="FullInfo")]
public class MyModel : LinkContainer
{ }
[Route("api/[controller]")]
public class MyController : Controller
{
private readonly ILinksService linksService;
public MyController(ILinksService linksService)
{
this.linksService = linksService;
}
[HttpGet("{id}",Name = "GetModelRoute")]
public async Task<MyModel> GetMyModel(int id)
{
MyModel model = await myRepository.GetMyModel(id);
await linksService.AddLinksAsync(model);
return model;
}
}
There are further overloads of AddLinksAsync
which take an instance of ILinksPolicy
or an array of ILinksRequirement
which will be evaluated at runtime. This should give complete control of which links are applied at any point within your api code.
There should not have much need to change how the Href
is transformed, however one common requirement is to output relative instead of absolute uris. This can be tried in the Basic Sample
services.AddLinks(config =>
{
config.UseRelativeHrefs();
...
});
Both Href and Rel transformations can be fully controlled by supplying a class or Type which implements ILinkTransformation
.
services.AddLinks(config =>
{
// supply a type implementing ILinkTransformation
config.UseHrefTransformation<MyHrefTransformation>();
// or supply an instance
config.UseRelTransformation(new MyRelTransformation());
});
Alternatively, transformations can be configured using a builder syntax
services.AddLinks(config =>
{
// output a uri for the rel values
config.ConfigureRelTransformation(transform => transform.AddProtocol()
.AddHost()
.AddVirtualPath(ctx => $"/rel/{ctx.LinkSpec.ControllerName}/{ctx.LinkSpec.RouteName}");
});
Both ways of customizaing transformations can be seen in the LinkConfigurationSample.
It is likely that you wish to control which links are included with each model, and one common requirement is to only show links for which the current user is authorized. This library fully integrates into the authorization pipeline and will apply any authorization policy you have applied to the linked action.
To enable authorization on a link provide the AuthorizeRoute
condition.
public class Startup
{
public void ConfigureServices(IServicesCollection services)
{
services.AddLinks(config =>
{
config.AddPolicy<MyModel>("FullInfo",policy => {
policy.RequireSelfLink()
.RequireRoutedLink("all", "GetAllModelsRoute")
.RequireRoutedLink("parentModels", "GetParentModelRoute",
x => new { parentId = x.ParentId }, condition => condition.AuthorizeRoute());
.RequireRoutedLink("subModels", "GetSubModelsRoute",
x => new { id = x.Id }, condition => condition.AuthorizeRoute());
.RequireRoutedLink("delete", "DeleteModelRoute",
x => new { id = x.Id }, condition => condition.AuthorizeRoute());
});
});
}
}
In the above example, GetParentModelRoute
, GetSubModelsRoute
& DeleteModelRoute
will not be shown to a user who does not have access to those routes as defined by their authorization policies. See the Microsoft documentation for more information on authrization within an aspnet core webapi project.
As with the above examples, there are further condition methods which allow you to specifiy a policy name, an absolute policy or a set of requirements.
You can also conditionally show a link based on any boolean logic by using the Assert
condition. For example, there is a method which allows you to add common paging links to paged results of objects. You may decide these are not worthwhile if there is a total of only one page of results.
options.AddPolicy<IPageLinkContainer>(policy =>
{
policy.RequireelfLink("all")
.RequirePagingLinks(condition => condition.Assert(x => x.PageCount > 1 ));
});
You are free to add your own requirements using the generic Requires
method on LinksPolicyBuilder
. In addition, you must write an implementation of ILinksHandler
to handle your requirement. For example, you may have a requirement on certain responses to provide a link back to your api root document. Define a simple requirement for this link.
using RiskFirst.Hateoas;
public class ApiRootLinkRequirement : ILinksRequirement
{
public ApiRootLinkRequirement()
{
}
public string Id { get; set; } = "root";
}
Given this requirement, we need a class to handle it, which must implement ILinkHandler
and handle your requirement.
using RiskFirst.Hateoas;
public class ApiRootLinkHandler : LinksHandler<ApiRootLinkRequirement>
{
protected override Task HandleRequirementAsync(LinksHandlerContext context, ApiRootLinkRequirement requirement)
{
var route = context.RouteMap.GetRoute("ApiRoot"); // Assumes your API has a named route "ApiRoot".
context.Links.Add(new LinkSpec(requirement.Id, route));
context.Handled(requirement);
return Task.CompletedTask;
}
}
Finally register your Handler with IServicesCollection
and use the requirement within your link policy
public class Startup
{
public void ConfigureServices(IServicesCollection services)
{
services.AddLinks(config =>
{
config.AddPolicy<MyModel>(policy =>
{
policy.RequireRoutedLink("self","GetModelRoute", x => new {id = x.Id })
.Requires<ApiRootLinkRequirement>();
});
});
services.AddTransient<ILinksHandler,ApiRootLinkHandler>();
}
}
This example is demonstrated in the CustomRequirementSample
There are many additional parts of the framework which can be extended by writing your own implementation of the appropriate interface and registering it with IServicesCollection
for dependency injection. For example, you could change the way that links are evaluated and applied to your link container by implementing your own ILinksEvaluator
using RiskFirst.Hateoas;
public class Startup
{
public void ConfigureServices(IServicesCollection services)
{
services.AddLinks(options => {
...
});
services.AddTransient<ILinksEvaluator, MyLinksEvaluator>();
}
}
The list of interfaces which have a default implementation, but which can be replaced is:
ILinkAuthorizationService
, controls how links are authorized during link condition evaluation.ILinksEvaluator
, controls how links are evaluated and transformed before being written to the returned model.ILinksHandlerContextFactory
, controls how the context is created which is passed through the requirement handlers during processing.ILinksPolicyProvider
, provides lookup forILinkPolicy
instances by resource type and name.ILinksService
, the main entrypoint into the framework, this interface is injected into user code to apply links to api resources.ILinkTransformationContextFactory
, controls how the transformation context is created during transformation for rel & href properies of links.IRouteMap
, controls how your API is indexed to allow links between routes.
The change from version 1.0.x to 1.1.x was mostly non-breaking, however if you have implemented any custom requirement handlers as described in the example above the signature of the base class LinksHandler
changed slightly to remove the duplicate declaration of the generic type TResource
.
In v1.0.x your code may have looked like:
public class MyCustomHandler : ILinksHandler { ... }
It should now inherit from LinksHandler<TRequirement>
making implementation simpler, and giving a type-safe override of HandleRequirementAsync
giving access to your correctly-typed requirement.
public class MyCustomHandler : LinksHandler<MyCustomRequirement>