Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Accessing services when configuring MvcOptions in ASP.NET Core

$
0
0

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": "&lt;script&gt;alert(&#x27;got you!&#x27;)&lt;/script&gt;" 
}

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.


Viewing all articles
Browse latest Browse all 743

Trending Articles