2
votes

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

According to the Microsoft docs:

https://docs.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)));
1
Why would you want to NOT register with the Service Provider? and also what exactly does _builderInstance.CreateFeaturePolicyHeader() do? I assume it produces a key value pair to be added to response headers, but is it more than just return new Kvp("Header","Value")? Im tempted to think the options pattern can help, but im not so sure what exactly your code does. - Gerald Chifanzwa
@gerryc.inc I have added the builder code so you can see more clearly how it works, essentially I am trying to create a FeatureHeaderBuilder similar to how NWebsec docs.nwebsec.com/en/latest works. I wanted specifically to understand how a builder can provide a fluent lamda style configuration. Note I DO want to register with the service provider. But I'm looking for a way to do so without explicitly adding it in services.Add<> if that makes sense - AFM-Horizon

1 Answers

1
votes

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.