MakingSense AspNet HypermediaApi Helpers
This library pretends to expose some models and utilities useful to create an Hypermedia API using ASP.NET 5 Framework.
The idea is not to create a framework, only a set of utilities that can be used together or not.
Basic Configuration
Example:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.OutputFormatters.Clear();
options.OutputFormatters.Add(new HypermediaApiJsonOutputFormatter());
options.InputFormatters.Clear();
options.InputFormatters.Add(new HypermediaApiJsonInputFormatter());
options.Filters.Add(new PayloadValidationFilter());
options.Filters.Add(new RequiredPayloadFilter());
});
services.AddApiMappers();
services.AddSuitableValidators();
services.AddLogging();
services.AddLinkHelper<CustomLinkHelper>();
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
app.UseApiErrorHandler();
app.UseMvc();
app.UseNotFoundHandler();
loggerfactory.AddProvider(new CustomLoggerProvider());
}
}
Hypermedia API library architecture
Abstractions
-
Relations
A Relation represents a behavior in the system, see Hypermedia and H Factors. Each link has a
rel
property with one or more relations separated by spaces.A relation defines how a link works. There are standard relations, for example
self
,next
, etc. And custom relations defined by implementer and are expressed as URLs to HTML documents with the behavior details. In general, custom relations are Action Relations. -
Action Relations
Controller actions are decorated with
ActionRelation
attributes that indicate: HTTP method, Input model, Output model, a description and a link to related HTML document.These attributes also allow to define the MVC route, and some other metadata:
NotImplemented
- The action is not implemented yet.IsExperimental
- The behavior is not confirmed and could change in the near future.Description
- Default description of links to this action.SuitableValidators
- See related section in under MVC Filters.
example:
public class SubscribersController : Controller { // . . . [ImportSubscribersRelation( Template = "/accounts/{accountName}/lists/{listId}/subscribers/import", SuitableValidators = new[] { typeof(ImportSubscribersSuitableValidator) })] public async Task<CreationResult> ImportSubscribersToList(string accountName, int listId, [FromBody] [NotNull] ImportSubscribersPayload subscribers) { // . . .
-
Models
All API input and output types should be represented as Model classes.
The convention for these classes is to have camel case properties and an special property called
_links
where controllers will load related links based on the state of the application.See API Mappers section for information about a provided utility to map domain objects to API Models.
-
Problems
Problems are an special kind of models used to represent errors. ApiErrorHandler Middleware is in charge of render them.
Example:
if (Exists(list.name)) { throw new ApiException(new DuplicatedListNameProblem(list.name)); }
Linking
Making Sense Hypermedia API provides a LinkHelper
that allows to use strongly typed code to generate links to actions, including metadata information and processing Suitable Validators (See related section in under MVC Filters).
Example:
public class SubscribersController : Controller
{
// . . .
[GetSubscriberCollectionRelation(Template = "/accounts/{accountName}/lists/{listId}/subscribers", Description = "Get subscribers of a list")]
public async Task<SubscriberCollectionPage> GetSubscribersOfList(string accountName, int listId, int? page = null, int? per_page = null)
{
var pagination = new PaginationParameters(page, per_page);
SubscriberCollectionPage result = await _service.GetSubscribersOfList(accountName, userId, listId);
return result
// Adds links to output model (a page of subscribers)
.AddLinks(
_link.ToHomeAccount(),
_link.ToAction<ListsController>(x => x.GetList(accountName, listId)),
_link.ToAction<SubscribersController>(x => x.ImportSubscribersToList(accountName, listId, null)),
_link.ToAction<SubscribersController>(x => x.SubscribeToList(accountName, listId, null)),
_link.ToAction<SubscribersController>(c => c.Unsubscribe(accountName, null)))
// Adds page links to output model (first, prev, next, etc)
.AddPageLinks(
(p, pp) => _link.ToAction<SubscribersController>(x => x.GetSubscribersOfList(accountName, listId, p, pp)))
// Adds links to each child item (each subscriber in the page)
.AddItemsLinks(item => new[]
{
_link.ToAction<SubscribersController>(c => c.GetSubscriber(accountName, item.emailMd5)),
_link.ToAction<SubscribersController>(c => c.EditSubscriber(accountName, item.emailMd5, null))
})
.Cast<SubscriberCollectionPage>();
}
// . . .
ASP.NET Middlewares
ApiErrorHandler Middleware
This middleware render all exceptions as Problem Details (See Problem Details for HTTP APIs)
It catches:
Response.StatusCode == 401 (Unauthorized)
and render anMakingSense.AspNet.HypermediaApi.Problems.UnauthorizedProblem
Response.StatusCode == 403 (Forbidden)
and render aMakingSense.AspNet.HypermediaApi.Problems.ForbiddenProblem
Response.StatusCode == 404 (Not Found)
and render aMakingSense.AspNet.HypermediaApi.Problems.RouteNotFoundProblem
- Any
MakingSense.AspNet.Authentication.Abstractions.AuthenticationException
and render them asMakingSense.AspNet.HypermediaApi.Problems.AuthenticationErrorProblem
- Any
MakingSense.AspNet.HypermediaApi.ExceptionHandling.ApiException
and render the inner problem (Problem
property) - Any unhandled exception and render them as
MakingSense.AspNet.HypermediaApi.Problems.UnexpectedProblem
Basic Configuration example:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// If you want to automatically render links to self and home resources,
// it is required to register a LinkHelper.
services.AddScoped<ILinkHelper, CustomLinkHelper>();
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerfactory)
{
app.UseApiErrorHandler();
app.UseMvc();
}
}
NotFoundHandler Middleware
It is a terminal middleware, if a request comes to here it throws an ApiException
with a RouteNotFoundProblem
.
MVC
Suitable validators filters
Suitable validators are used to validate if given a set of parameters it is possible to execute an action depending on the state of the system.
They are useful to implement HATEOAS because are invoked during link generation and also before execute a task (after parameters validation).
Initialization:
public void ConfigureServices(IServiceCollection services)
{
// . . .
services.AddSuitableValidators(typeof(ACustomSuitableValidatorClass).GetAssembly);
// . . .
Assigning to a controller action:
public class ListsController
{
// . . .
[CreateListRelation(
Template = "/accounts/{accountName}/lists",
SuitableValidators = new[] { typeof(CreateListSuitableValidator) })]
public async Task<CreationResult> CreateList(string accountName, [FromBody] ListModel list)
{
// . . .
Example implementation for CreateList
method (previous example):
public class CreateListSuitableValidator : ISuitableValidator
{
readonly ICustomService _customService;
public static readonly string arg_accountName = "accountName";
public CreateListSuitableValidator([NotNull] customService)
{
_customService = customService;
}
public async Task<Problem> ValidateAsync(IDictionary<string, TemplateParameter> values)
{
if (values[arg_accountName].ForceValue)
{
if (await _customService.MaxListsReached((string)values[arg_accountName].ForcedValue))
{
return new MaximumNumberOfListsReachedProblem();
}
}
return null;
}
}
}
Validation Filters
Validation Filters are standard MVC filters with common behavior that can be reused in your Hypermedia API project. They are executed after standard MVC validation.
Setup:
public void ConfigureServices(IServiceCollection services)
{
// . . .
services.AddMvc(options => {
options.Filters.Add(new PayloadValidationFilter());
options.Filters.Add(new RequiredPayloadFilter());
});
// . . .
PayloadValidationFilter
If there was validation errors (!ModelState.IsValid
), it generates and throws the ValidationProblem
with information about validation errors.
RequiredPayloadFilter
It checks if expected complex types are included in the request, based in the follow criteria:
-
NotNull Attributes
RequiredPayloadFilter verifies if parameters marked as
[NotNull]
are effectively not null. If not, it throws aValidationProblem
.public async Task<CreationResult> CreateList(string accountName, [NotNull] [FromBody] ListModel list)
-
Relation Attribute Input
RequiredPayloadFilter verifies if the argument associated to relation input parameter has value.
public class CreateListRelation : ActionRelationAttribute { public override HttpMethod? Method => HttpMethod.POST; public override Type InputModel => typeof(ListModel); // . . .
[CreateListRelation(Template = "/accounts/{accountName}/lists")] public async Task<CreationResult> CreateList(string accountName, [FromBody] ListModel list)
Formatters
HypermediaApiJsonInputFormatter
HypermediaApiJsonInputFormatter ignores request content-type
header assuming that the content type is application/json
.
HypermediaApiJsonOutputFormatter
HypermediaApiJsonOutputFormatter renders output JSON content indented.
Utilities
API Mappers
ApiMapper<,>
Provides a convenient interface and utilities to implement mappers to and from API models with facilities to map pages and collections.
To register all API Mappers in an assembly is enough with the follow line:
public void ConfigureServices(IServiceCollection services)
{
// . . .
services.AddApiMappers(typeof(ACustomMapperClass).GetAssembly);
// . . .
Then, the mappers could be injected using the interface:
public class ListsController : Controller
{
private readonly ILogger _logger;
private readonly IApiMapper<InternalClass, ApiOutputClass> _internalToOutputMapper;
public ListsController(
[NotNull] ILogger<ListsController> logger,
[NotNull] IApiMapper<InternalClass, ApiOutputClass> internalToOutputMapper)
{
_logger = logger;
_internalToOutputMapper = internalToOutputMapper;
}
// . . .