Quick start: ASP.Net Core 3.1, Entity Framework Core, CQRS, React JS Series — Part 6: Hateoas implementation and configuration options usage
In this content, Hateoas implementation and AddOptions usage for config settings will be demonstrated.
The parent content of this series: Quick start: ASP.Net Core 3.1, Entity Framework Core, CQRS, React JS Series
Outline
- What is Hateoas?
- Github feature branch
- Configuring on Extensios
- Seeing Links in Response by hateoas request
- Options usage for configuration settings
- Registering class as AddOptions
What is Hateoas?
Hypermedia as the Engine of Application State (HATEOAS) is a component of the REST application architecture that distinguishes it from other network application architectures.
With HATEOAS, a client interacts with a network application whose application servers provide information dynamically through hypermedia. A REST client needs little to no prior knowledge about how to interact with an application or server beyond a generic understanding of hypermedia.
Basically, with this feature implementation, the API is discoverable by itself for consumers.
Github feature branch
Configuring on Extensions
JsonHateoasFormatter should be in placed for application/json+hateoas specific requests. It will be used to render the response with links.
public class JsonHateoasFormatter : OutputFormatter
{
public JsonHateoasFormatter()
{
SupportedMediaTypes.Add("application/json+hateoas");
}
private T GetService<T>(OutputFormatterWriteContext context)
{
return (T)context.HttpContext.RequestServices.GetService(typeof(T));
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
var contextAccessor = GetService<IActionContextAccessor>(context);
var urlHelperFactory = GetService<IUrlHelperFactory>(context);
var options = GetService<IOptions<HateoasOptions>>(context).Value;
var actionDescriptorProvider = GetService<IActionDescriptorCollectionProvider>(context);
var urlHelper = urlHelperFactory.GetUrlHelper(contextAccessor.ActionContext);
var response = context.HttpContext.Response;
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
};
var resource = default(Resource);
if (context.Object is SerializableError error)
{
var errorOutput = JsonConvert.SerializeObject(error, serializerSettings);
response.ContentType = "application/json";
return response.WriteAsync(errorOutput);
}
var result = context.Object;
if (context.ObjectType.GetInterfaces().Contains(typeof(IEnumerable)))
{
var listType = context.ObjectType.GetGenericArguments().First();
var resourceList = ((IEnumerable<object>)result)
.Select(r => CreateResource(listType, r, options, actionDescriptorProvider, urlHelper))
.ToList();
resource = CreateResource(context.ObjectType, resourceList, options, actionDescriptorProvider, urlHelper);
}
else
{
resource = CreateResource(context.ObjectType, context.Object, options, actionDescriptorProvider, urlHelper);
}
var output = JsonConvert.SerializeObject(resource, serializerSettings);
response.ContentType = "application/json+hateoas";
return response.WriteAsync(output);
}
private static Resource CreateResource(Type type, object value, HateoasOptions options, IActionDescriptorCollectionProvider actionDescriptorProvider, IUrlHelper urlHelper)
{
var resourceOptions = options.Requirements.Where(r => r.ResourceType == type);
var isEnumerable = type.GetInterfaces().Contains(typeof(IEnumerable));
var resource = default(Resource);
if (isEnumerable)
{
resource = new ListItemResource(value);
}
else
{
resource = new ObjectResource(value);
}
foreach (var option in resourceOptions.Where(x => x.IsEnabled(value)))
{
var route = actionDescriptorProvider.ActionDescriptors.Items.FirstOrDefault(i => i.AttributeRouteInfo.Name == option.Name);
var method = route.ActionConstraints.OfType<HttpMethodActionConstraint>().First().HttpMethods.First();
var routeValues = default(object);
if (!isEnumerable)
{
routeValues = option.RouteValues(value);
}
var url = urlHelper.Link(option.Name, routeValues).ToLower();
resource.Links.Add(new Link(option.Name, url, method));
}
return resource;
}
}
In extensions, we should configure the API’s based on response and parameters:
public static void ConfigureHateoas(this IMvcBuilder builder)
{
builder.AddHateoas(options =>
{
options
.AddLink<TagDto>("get-tag-by-id", t => new { id = t.Id })
.AddLink<IEnumerable<TagDto>>("get-all-tags")
.AddLink<TagDetailsDto>("get-tag-details", t => new { id = t.Id }) .AddLink<TagDto>("create-tag")
.AddLink<TagDto>("update-tag", t => new { id = t.Id })
.AddLink<TagDto>("delete-tag", t => new { id = t.Id });
options
.AddLink<ProductDto>("get-product-by-id", p => new { id = p.Id })
.AddLink<IEnumerable<ProductDto>>("get-all-products")
.AddLink<ProductDetailDto>("get-product-details", p => new { id = p.Id })
.AddLink<ProductDto>("create-product")
.AddLink<ProductDto>("update-product", p => new { id = p.Id })
.AddLink<ProductDto>("delete-product", p => new { id = p.Id });
options
.AddLink<IEnumerable<CategorizationDto>>("get-categorizations")
.AddLink<CategorizationDto>("add-tag-to-product");
});
}
Seeing Links in Response by hateoas request
Options usage for configuration settings
For strongly types configuration classes, AddOptions registration can be used.
public class ApiInfoConfiguration : IApiInfoConfiguration
{
[Required(ErrorMessage ="Api version info is required!")]
public string Version { get; set; }
}
public class LoggingLevelConfiguration : ILoggingLevelConfiguration
{
[Required(ErrorMessage = "Default logger is required!")]
public string Default { get; set; }
}
Registering class as AddOptions
Correct binding is important to make the class and json definition mapped correctly. For example: ApiInfoConfiguration should be mapped ApiInfo.
public static void ConfigureAppConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.AddOptions<LoggingLevelConfiguration>().Bind(configuration.GetSection("Logging:LogLevel")).ValidateDataAnnotations();
services.AddOptions<ApiInfoConfiguration>().Bind(configuration.GetSection("ApiInfo")).ValidateDataAnnotations();
}
And, that can be used in controller in this way:
[Route("api/[controller]")]
[ApiController]
public class ConfigurationController : ControllerBase
{
private readonly IApiInfoConfiguration _apiInfoConfiguration;
private readonly ILoggingLevelConfiguration _loggingLevelConfiguration;
public ConfigurationController(
IOptionsSnapshot<LoggingLevelConfiguration> loglevelConfigurationOptions,
IOptionsSnapshot<ApiInfoConfiguration> apiInfoConfigurationOptions)
{
_loggingLevelConfiguration = loglevelConfigurationOptions.Value;
_apiInfoConfiguration = apiInfoConfigurationOptions.Value;
}
[HttpGet(Name = "get-configurations")]
public IActionResult Index()
{
return Ok(new
{
_apiInfoConfiguration.Version,
_loggingLevelConfiguration.Default
});
}
}
Conclusion
By specific JsonFormatter, hateoas implementation and IOptionsSnapshot usage for configuration settings are demonstrated.