In this first part of a series of occasional posts, I discuss the thinking behind taking string based HTTP Headers and presenting them to your .NET code via dependency injection as strongly typed objects.
If you have read my previous blog posts or seen my talks, you will be aware that I am a big fan of the configuration binding functionality in .NET Core (now .NET 5) that takes string key/value pairs stored in various ways (such as JSON or environmental variables) and binding them to a typed object that is exposed via the .NET Dependency Injection container.
Recently, I have been giving some thought to other areas in ASP.NET Core that would benefit from this ability to bind string data into object instances, in particular looking at HTTP request headers.
In ASP.NET Core, HTTP request headers are presented as an instance of the IHeaderDictionary interface which is primarily a (dictionary) collection of key/value pairs made up of a string value for the key (the header key) and the header values within a StringValues struct (which itself is a special collection of strings that "represents zero/null, one, or many strings in an efficient way").
The purpose of the interface is to present a collection of HTTP headers in a consistent manner. HTTP headers can have multiple declarations within the incoming request (see the RFC specification) which need to be parsed and grouped together by key before being presented in the collection.
Whilst the interface provides a consistent manner to access the headers, if you want interrogate the incoming request headers, you have a few hoops to jump through, namely
- the HttpContext needs to be directly accessible in controllers, it is a property on the controller, or indirectly via the (singleton) IHttpContextAccessor (which is not registered by default) when consuming elsewhere
- once you have the context, having to navigate to the Request property
- Within the request object, navigate to the Headers property
In other words you need code to get to HttpContext.Request.Headers.
If you have several headers that you want to access, you have to repeat this process for each of the headers you require.
Once you have your value(s) for a header, these are still strings that you may want to convert into some other type such as integer or GUID, which then means having to parse the value(s) which then raises a number of other questions:
- What should be the default if the header is not present?
- What to do if the header is present, but the value is not in the correct format to parse to the required type?
- What to do if only expecting a single value, but multiple values are presented - first or last in wins?
- What to do if expecting multiple values and a single value is presented
- What to do if the value(s) can be parsed, but fail any domain validation that needs to be applied (E.g. integer value must be within a domain range)
- If any of the above cannot be worked around, how can an error be safely marshalled back to the caller without raising an exception?
In each of these scenarios, there is the potential for an exception to be thrown if assumptions are made about the incoming data and guards are not put in place to handle non-expected scenarios (E.g. using TryParse instead of Parse when converting strings to other primitive types to avoid exceptions when parsing fails).
Assuming all of the above is working correctly and you have managed to get the value(s) you require out of the headers, there is the question of passing the data from the controller action (or middleware) to some domain model or function.
Ideally, your domain logic and entities should avoid any bindings to web related types such as IHeaderDictionary & HttpContext. Avoiding this coupling to web concepts means they can be used in other applications (console, desktop or mobile applications) where the values that are received via the headers in a web application/API service may (or may not) be obtained in other ways (user input, configuration or hard coded).
I recently read Andrew Lock's blog about primitive obsession where he discusses the danger of passing values around as standard data type (string, integer, GUID et al) as these are interchangeable values that do not have context.
The post goes on to put forward an implementation of wrapping these values into 'strong types' that give context to a value. E.g. a customer id value expressed as a GUID is interchangeable with a product id that is also a GUID, but an instance of a CustomerIdentity type is not interchangeable with a ProductIdentity.
After having read Andrew's series that shows how to create strong types that are struct based, I then went to to read Thomas Levesque's series that is based on Andrew's series, but this time implementing using C# 9 record types in .NET 5.
I highly recommend reading both of these.
The principle I want to carry through is that each HTTP header of interest to the application should be mapped into a strongly typed instance to give the value(s) some meaning and context beyond just being an incoming value. In addition, these types should abstract away the need for callers to have any knowledge of HTTP related classes and interfaces.
With all of the above in mind, I have started a library to remove the need to keep writing a lot of the commonly required code. The requirements I have are
- To remove the need for any of my controller or domain code to need to have knowledge of HTTP headers to retrieve the values that have been supplied in a request. Instead I want strongly typed values to be available from the Dependency Injection container that encapsulate one or more pieces of data that have some meaning to the application
- To have a standard generic factory process to create the strong types using a standard signature that provides a collection of zero, one or many strings for a particular header name to be provided by the factory
- For the factory process to have a simple fluent syntax that can be used within the container registration process
- For the factory to be able to consume an expression that can take that collection of string values and decide how to map the values (or lack of values) into an instance of a type that can be consumed elsewhere
- For the constructed types to encapsulate not just the incoming value(s), but also be able to express a lack of value(s) or an invalid state due to invalid value formats (as raising exceptions during the factory process will be difficult to catch)
- For the constructed types to be automatically registered as scoped lifetime services in the container for use by controllers
- For the constructed types to be available via a singleton that is aware of the async context so that can be injected into standard middleware constructors (by encapsulating the HttpContextAccessor).
The library is still an initial work in progress at time of writing, but if you are interested in how I have approached the above, the code is available in a repository at https://github.com/stevetalkscode/TypedHttpHeaders
As I progress with the library, I plan to have more posts in this series where I will be walking through how the code works in order to achieve the goals above and eventually hope to release a NuGet package.