Quick start: ASP.Net Core 3.1, Entity Framework Core, CQRS, React JS Series — Part 6: Hateoas implementation and configuration options usage

Ali Süleyman TOPUZ
.Net Programming
Published in
4 min readDec 29, 2020
Well organized pieces make it understandable and readable!

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

Response of the request with Accept type (application/json+hateoas)

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.

--

--

Ali Süleyman TOPUZ
.Net Programming

Software Engineering and Development Professional. Writes about software development & tech. 📍🇹🇷