This post is a follow on to an article by Steve Gordon I read the other day on how to HTML encode deserialized JSON content from a request body. It's an interesting post, but it spurred me thinking about a tangential issue - using injected services when configuring MvcOptions
.
The setting - Steve's post in brief
I recommend you read Steve's post first, but the key points to this discussion are described below.
Steve wanted to ensure that HTML POSTed inside a JSON string property was automatically HTML encoded, so that potentially malicious script couldn't be stored in the database. This wouldn't necessarily be something you'd always want to do, but it worked for his use case. It ensured that a string such as
{
"text": "<script>alert('got you!')</script>"
}
was automatically converted to
{
"text": "<script>alert('got you!')</script>"
}
by the time it was received in an Action method. He describes creating a custom ContractResolver
and ValueProvider
to override the CreateProperties
method and automatically encode any string
properties.
The section I am interested in is where he wires up his new resolver and provider using a small extension method UseHtmlEncodeJsonInputFormatter
. This requires providing a number of services in order to correctly create the JsonInputFormatter
. I have reproduced his extension method below:
ublic static class MvcOptionsExtensions
{
public static void UseHtmlEncodeJsonInputFormatter(this MvcOptions opts, ILogger<MvcOptions> logger, ObjectPoolProvider objectPoolProvider)
{
opts.InputFormatters.RemoveType<JsonInputFormatter>();
var serializerSettings = new JsonSerializerSettings
{
ContractResolver = new HtmlEncodeContractResolver()
};
var jsonInputFormatter = new JsonInputFormatter(logger, serializerSettings, ArrayPool<char>.Shared, objectPoolProvider);
opts.InputFormatters.Add(jsonInputFormatter);
}
}
For the full details of this method, check out his post. For our discussion, all that's necessary is to appreciate that we are modifying the MvcOptions
by adding a new JsonInputFormatter
, and that to do so we need instances of an ILogger<T>
and ObjectPoolProvider
.
The need for these services is a little problematic - we will be calling this extension method when we are first configuring MVC, within the ConfigureServices
method, but at that point, we don't have an easy method of accessing other configured services.
The approach Steve used was to build a service provider, and then create the required services using it, as shown below:
public void ConfigureServices(IServiceCollection services)
{
var sp = services.BuildServiceProvider();
var logger = sp.GetService<ILoggerFactory>();
var objectPoolProvider = sp.GetService<ObjectPoolProvider>();
services
.AddMvc(options =>
{
options.UseHtmlEncodeJsonInputFormatter(
logger.CreateLogger<MvcOptions>(),
objectPoolProvider);
});
}
This approach works, but it's not the cleanest, and luckily there's a handy alternative!
What does AddMvc actually do?
Before I get into the cleaned up approach, I just want to take a quick diversion into what the AddMvc
method does. In particular, I'm interested in the overload that takes an Action<MvcOption>
setup action.
Taking a look at the source code, you can see that it is actually pretty simple:
public static IMvcBuilder AddMvc(this IServiceCollection services, Action<MvcOptions> setupAction)
{
// precondition checks removed for brevity
var builder = services.AddMvc();
builder.Services.Configure(setupAction);
return builder;
}
This overload calls AddMvc()
without an action, which returns an IMvcBuilder
. We then call Configure
with the Action<>
to configure an instance of MvcOptions
.
ConfigureOptions to the rescue!
When I saw the Configure
call, I immediately thought of a post I wrote previously, about using ConfigureOptions
to inject services when configuring IOptions
implementations.
Using this technique, we can avoid having to call BuildServiceProvider
inside the ConfigureServices
method, and can leverage dependency injection instead by creating an instance of IConfigureOptions<MvcOptions>
.
Implementing the interface is simply a case of calling our already defined extension method, from within the required Configure
method:
public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
private readonly ILogger<MvcOptions> _logger;
private readonly ObjectPoolProvider _objectPoolProvider;
public ConfigureMvcOptions(ILogger<MvcOptions> logger, ObjectPoolProvider objectPoolProvider)
{
_logger = logger;
_objectPoolProvider = objectPoolProvider;
}
public void Configure(MvcOptions options)
{
options.UseHtmlEncodeJsonInputFormatter(_logger, _objectPoolProvider);
}
}
We can then update our configuration method to use the basic AddMvc()
method and inject our new configuration class:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddSingleton<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();
}
With this configuration in place, we have the same behaviour as before, just with some nicer wiring in the Setup
class! For a more detailed explanation of why this works, check out my previous post.
Summary
This post was a short follow-up to a post by Steve Gordon in which he showed how to create a custom JsonInputFormatter
. I showed how you can use IConfigureOptions<>
to use dependency injection when adding MvcOptions
as part of your MVC configuration.