I have worked on cross-disciplinary teams with a polyglot stack, and I imagine most developers have at this point in web develpoment history. The number one rule for team productivity is to help your downstream team members. In my case, I work on the server-side, providing value to frontend developers in the form of HTTP APIs. Frontend developers are an exotic breed; they can make anything work on the client, for better or worse. In this blog post, I’ll show you how, with a little ASP.NET Core infrastructure code, you can empower your frontend team. Benefits include a clearer understanding of resources, less frontend logic, and a better UX experience for users.
Goals
TL;DR: Download the GitHub Project
Front end objectives are commonly about displaying data to the user and having them perform an action based on that data. While UIs can be simple, they require metadata to make them work. For example, let’s take a weather forecast. The user may need to know what the outlook is for their particular area. What might an API provide in terms of data? Location, Temperature, Dates, Summary.
A Frontend developer may also need to know additional metadata to create an enjoyable experience. Hypothetically, they may need permissions for the user, links to related resources, display labels based on user language preferences, and so on. Metadata is a user experience’s best friend. Commonly, frontend teams take it upon themselves to hardcode metadata into their implementations: URLs, label text, permissions. While this approach works, it is very brittle, and can erode quickly due to changes at the server.
So, in our sample, let’s build a framework to enrich our results with metadata, without complicating our ASP.NET Core API controllers.
Vocabulary
Let’s start with a few vocabulary terms: Resource
, Representation
, and Enricher
.
A resource is a valuable data we need to pass to the caller of our API. We will be dealing with Weather Forecasts
in our example, so in this case, forecast data is our resource.
A representation is a form our resource may take. In this example, we’ll have two representations for weather forecasts. One is a single forecast, and the other is a collection of weather predictions.
Finally, enrichers are pieces of code that will process our results and add additional metadata. In this example, we’ll be adding URL links to our representations. The access to links should empower our frontend team to spend less time hardcoding strings, and more time building a better user experience.
Let’s jump into the code.
Our Representations
Let’s first take a look at our base Representation
class. For uniformity, we will inherit from this class, so that our enrichers know what properties to expect.
public abstract class Representation
{
public List<Link> Links { get; set; }
= new List<Link>();
public Representation AddLink(Link link)
{
var exists = Links.FirstOrDefault(x => x.Id == link.Id);
if (exists != null)
{
Links.Remove(exists);
}
Links.Add(link);
return this;
}
}
public class Link
{
public string Id { get; set; }
public string Label { get; set; }
public string Url { get; set; }
}
A Link
could be anything you want it to be, but for the sake of consistency, I recommend making it something that is general use and fits the scenarios of your frontend team. Note that a Representation
has a collection of Link
objects.
Let’s see how the representations look using our Representation
base class.
public class WeatherForecasts : Representation
{
public List<WeatherForecast> Results { get; set; }
= new List<WeatherForecast>();
}
public class WeatherForecast : Representation
{
public int Id { get; set; }
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int) (TemperatureC / 0.5556);
public string Summary { get; set; }
}
Finally, let’s build our API Controller.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
[Route("", Name = "weather#index")]
public ActionResult<WeatherForecasts> Get()
{
var results = GetForecasts();
return Ok(
new WeatherForecasts
{
Results = GetForecasts()
});
}
[HttpGet]
[Route("{id:int}", Name = "weather#show")]
public ActionResult<WeatherForecast> Get(int id)
{
var result = GetForecasts().FirstOrDefault(x => x.Id == id);
if (result == null)
return NotFound();
return Ok(result);
}
private List<WeatherForecast> GetForecasts()
{
var rng = new Random();
return Enumerable.Range(1, 5)
.Select(index => new WeatherForecast
{
Id = index,
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToList();
}
}
If you’ve built a web API with ASP.NET Core, this shouldn’t be shocking so far. Let’s get to enriching our representations.
Enriching Representations
We first need to create a base class and an interface for our enrichment methodology.
public abstract class Enricher<T>
: IEnricher where T : Representation
{
public virtual Task<bool> Match(object target) => Task.FromResult(target is T);
public Task Process(object representation) => Process(representation as T);
public abstract Task Process(T representation);
}
public interface IEnricher
{
Task<bool> Match(object target);
Task Process(object representation);
}
Next, let’s implement some enrichers for WeatherForecast
and WeatherForecasts
.
/// <summary>
/// An enricher for our Weather#Show response
/// </summary>
public class WeatherForecastEnricher : Enricher<WeatherForecast>
{
private readonly IHttpContextAccessor accessor;
private readonly LinkGenerator linkGenerator;
public WeatherForecastEnricher(IHttpContextAccessor accessor, LinkGenerator linkGenerator)
{
this.accessor = accessor;
this.linkGenerator = linkGenerator;
}
public override Task Process(WeatherForecast representation)
{
var httpContext = accessor.HttpContext;
representation
.AddLink(new Link
{
Id = representation.Id.ToString(),
Label = $"Weather #{representation.Id}",
Url = linkGenerator.GetUriByName(
httpContext,
"weather#show",
new {id = representation.Id},
scheme: "https"
)
});
return Task.CompletedTask;
}
}
In the enricher for WeatherForecast
, we use LinkGenerator
to generate links to the endpoints necessary to get a single forecast representation. In our WeatherForecastsEnricher
, we reuse our single representation enricher.
/// <summary>
/// An enricher for our Weather#Index respponse
/// </summary>
public class WeatherForecastsEnricher : Enricher<WeatherForecasts>
{
private readonly WeatherForecastEnricher enricher;
public WeatherForecastsEnricher(WeatherForecastEnricher enricher)
{
this.enricher = enricher;
}
public override async Task Process(WeatherForecasts representation)
{
foreach (var forecast in representation.Results)
{
await enricher.Process(forecast);
}
}
}
So, how does the enricher add more information to our representations?
Wiring It All Up
We will lean into ASP.NET Core’s pipeline and utilize a ResultFilter
. Filters allow us to repeatable steps between an incoming request and the response we send our clients.
public class RepresentationEnricher : IAsyncResultFilter
{
private readonly IEnumerable<IEnricher> enrichers;
public RepresentationEnricher(IEnumerable<IEnricher> enrichers)
{
this.enrichers = enrichers;
}
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is ObjectResult result)
{
var value = result.Value;
foreach (var enricher in enrichers)
{
if (await enricher.Match(value))
{
await enricher.Process(value);
}
}
}
// call this or everything is blank!
await next();
}
}
Let’s break down what we are doing here.
- We inject all registered enrichers into our
RepresentationEnricher
. - After the action on the controller has executed, we determine if the result is an
ObjectResult
. - We find the enrichers that apply to our representation.
- We process the object with matching enrichers.
The next step is to register our ResultFilter
so that this code can process results.
Let’s look at our Startup
class.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// we need you MVC!
services.AddHttpContextAccessor();
// We also have to register the object twice by type (use AutoFac)
services
.AddScoped<WeatherForecastEnricher>()
.AddScoped<IEnricher, WeatherForecastEnricher>();
services.AddScoped<IEnricher, WeatherForecastsEnricher>();
services.AddScoped<RepresentationEnricher>();
services.AddControllers(cfg =>
{
cfg.Filters.Add<RepresentationEnricher>();
});
}
Since we are using ASP.NET Core Dependency Injection (DI) for this example, we have to be very explicit about registrations. We need to register every enricher as an IEnricher
. We are using the WeatherForecastEnricher
directly, so we also need to register it as its concrete type. We add the RepresentationEnricher
with DI, and we also add it to the collection of filters in the call to AddControllers
. I admit, a lot is happening in this one method, so take the time to review it.
Let’s Call Our APIs
Starting up the project, we immediately see our first representation.
{
"results": [
{
"id": 1,
"date": "2020-01-30T12:45:31.721116-05:00",
"temperatureC": 53,
"temperatureF": 127,
"summary": "Bracing",
"links": [
{
"id": "1",
"label": "Weather #1",
"url": "https://localhost:5001/WeatherForecast/1"
}
]
},
//... more
}
Yay! Let’s follow that link through to the single endpoint.
{
"id": 1,
"date": "2020-01-30T12:46:55.824525-05:00",
"temperatureC": 51,
"temperatureF": 123,
"summary": "Hot",
"links": [
{
"id": "1",
"label": "Weather #1",
"url": "https://localhost:5001/WeatherForecast/1"
}
]
}
Great news, everyone! It works!
Bonus Round
Sharing information with calling clients is critical. The approach outlined here lets you keep your controller methods cleaner and focused on solving a problems. We allow our enricher infrastucture to do the hard work of adding additional metadata. It’s a win-win.
Remember, this is a sample blog post. I would likely make more enhancements before using it in a production application. Context matters, and you may want to weigh the trade-offs based on your circumstances. Some enhancements you may consider making:
Recursive Enrichment of Representations
Instead of writing an enricher for every representation, you can create a RepresentationEnricher
that scans a result and enriches all nested properties. Recursively enhancing your results can reduce enrichers, but it can come at the cost of reflection and recursion.
Change The Links Object
The Link
class is limited and focused on the idea of URLs. In my experience, you may want a more general use metadata class that can express concepts like actions, permissions, labels, and more. Don’t go overboard, but having the right pieces of metadata can make the development experience on the frontend more enjoyable.
Conventional Registration
The ASP.NET Core DI registration is tedious. Creating a pattern of scanning and registration for enrichers can reduce the amount of time you spend plumbing, allowing you to spend more time on solving business problems.
Common Enrichers
As you build out APIs, you might realize a pattern appearing on the surface of your implementation. The repetition of concepts is an excellent opportunity to introduce common enrichers. Some examples of common enrichers might be:
- Paging enrichers for first, last, next, and previous links.
- Cursor enrichers for cursor-based paging.
- Documentation enrichers that can link to resource definitions.
- Localization enrichers that understand a user’s language and change responses with appropriate labels.
This is the perfect “Don’t Repeat Yourself” moment.
Conclusion
The ASP.NET Core pipeline is a powerful mechanism that allows all developers to build infrastructure code to enhance their runtime experience. By taking advantage of result filters, we can improve our API results to make our frontend work more pleasant. Frontend developers can use this additional metadata to provide our users with a more enjoyable UX experience and cut down on frontend bloat of hardcoded values and complex logic. In the end, making a few changes on the API can have substantial positive impacts.
To try out this code sample, check out the GitHub repo.