Search code examples
c#.net-coredependenciesstartupasp.net-core-middleware

How To Register Services For Custom .Net Core Middleware


I'm currently trying to understand and work with custom middleware in DotNet Core.

According to the Microsoft docs:

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-3.1

Middleware should follow the Explicit Dependencies Principle by exposing its dependencies in its constructor.

So if I follow this principle, I end up with something like this

Explicit Dependency Version

public static class ApplicationBuilderFeatureHeaderExtension
    {
        public static IApplicationBuilder UseFeatureHeaders(this IApplicationBuilder app, Action<FeaturePolicyStringBuilder> builder)
        {
            return app.UseMiddleware<FeaturePolicyHeaderMiddleware>(builder);
        }
    }

public class FeaturePolicyHeaderMiddleware
{
    private RequestDelegate _next;
    private Action<FeaturePolicyStringBuilder> _builder;
    private FeaturePolicyStringBuilder _builderInstance;

    public FeaturePolicyHeaderMiddleware(RequestDelegate next, Action<FeaturePolicyStringBuilder> builder, FeaturePolicyStringBuilder builderInstance)
    {
        _next = next;
        _builderInstance = builderInstance;
        _builder = builder;
        _builder(_builderInstance);
    }

    public async Task Invoke(HttpContext context)
    {
        var header = _builderInstance.CreateFeaturePolicyHeader();
        if (!context.Response.Headers.ContainsKey(header.Key))
        {
            context.Response.Headers.Add(_builderInstance.CreateFeaturePolicyHeader());
        }

        await _next.Invoke(context);
    }
}

Here the FeaturePolicyStringBuilder is being provided as a service and has been registered in the startup file.


The Other Version (Using New)

  public class FeaturePolicyHeaderMiddleware
{
    private RequestDelegate _next;
    private Action<FeaturePolicyStringBuilder> _builder;
    private FeaturePolicyStringBuilder _builderInstance;

    public FeaturePolicyHeaderMiddleware(RequestDelegate next, Action<FeaturePolicyStringBuilder> builder)
    {
        _next = next;
        _builderInstance = new FeaturePolicyStringBuilder();
        _builder = builder;
        _builder(_builderInstance);
    }

    public async Task Invoke(HttpContext context)
    {
        var header = _builderInstance.CreateFeaturePolicyHeader();
        if (!context.Response.Headers.ContainsKey(header.Key))
        {
            context.Response.Headers.Add(_builderInstance.CreateFeaturePolicyHeader());
        }

        await _next.Invoke(context);
    }
}

In this version I am simply "Newing Up" the dependency, meaning that I don't have to register it as a service, this makes it easier to encapsulate into it's own project.
(Im not sure really how evil this is a it seems to me I'm already tightly coupled to the FeaturePolicyStringBuilder due to the Action requiring it as a type, so why not more glue!)

The Question Is there a way to still follow the Explicit Dependencies Principle while not having to explicitly register those dependencies with the Service Provider? or some way to register them within the middlware component itself?

Thanks!

P.S: I have added the builder code below so that the purpose of the code is more clear: as per gerry.inc's comment

public class FeaturePolicyStringBuilder
{
    private string _featurePolicyString;

    public KeyValuePair<string, StringValues> CreateFeaturePolicyHeader()
    {
        return new KeyValuePair<string, StringValues>("feature-policy", _featurePolicyString);
    }

    private void CreateDirective(string directiveName, Action<SourceBuilder> builder)
    {
        var builderObj = new SourceBuilder();
        builder(builderObj);
        _featurePolicyString += $"{directiveName} '{builderObj.GetPrefix()}' {builderObj.GetSources()};";
    }

    public FeaturePolicyStringBuilder Camera(Action<SourceBuilder> builder)
    {
        CreateDirective("camera", builder);
        return this;
    }

    public FeaturePolicyStringBuilder Accelerometer(Action<SourceBuilder> builder)
    {
        CreateDirective("accelerometer", builder);
        return this;
    }

    public FeaturePolicyStringBuilder Battery(Action<SourceBuilder> builder)
    {
        CreateDirective("battery", builder);
        return this;
    }
}

What the call in the Configure method looks like

app.UseFeatureHeaders(x => 
            x.Camera(b =>
            b.AddPrefix(HeaderPrefixEnum.HeaderPrefix.Self)
                .AddSource("Test"))
            .Accelerometer(b => 
                b.AddPrefix(HeaderPrefixEnum.HeaderPrefix.Self)
                    .AddSource("Test")
                    .AddSource("Test")
                    .AddPrefix(HeaderPrefixEnum.HeaderPrefix.None)
                    .AddPrefix(HeaderPrefixEnum.HeaderPrefix.Src)));

Solution

  • Given the way the builder is being configured, then the builder and configuration action should be invoked before adding the middleware

    public static class ApplicationBuilderFeatureHeaderExtension {
    
        public static IApplicationBuilder UseFeatureHeaders(this IApplicationBuilder app, Action<FeaturePolicyStringBuilder> configureBuilder) {
            FeaturePolicyStringBuilder builder = new FeaturePolicyStringBuilder();
            configureBuilder?.Invoke(builder);
            var header = builder.CreateFeaturePolicyHeader();
            return app.UseMiddleware<FeaturePolicyHeaderMiddleware>(header);
        }
    }
    

    And the middleware refactored accordingly

    public class FeaturePolicyHeaderMiddleware {
        private RequestDelegate _next;
        private KeyValuePair<string, StringValues> header;
    
        public FeaturePolicyHeaderMiddleware(RequestDelegate next, KeyValuePair<string, StringValues> header) {
            _next = next;
            this.header = header;
        }
    
        public async Task Invoke(HttpContext context) {
            if (!context.Response.Headers.ContainsKey(header.Key)) {
                context.Response.Headers.Add(header);
            }
    
            await _next.Invoke(context);
        }
    }
    

    This DRY approach allows for better separation of concerns and explicitly indicates what the middleware actual needs to perform its function.