3
\$\begingroup\$

Building upon a recent question I answered here

Simple middleware pipeline builder (similar to asp.net-core)

I came across a cross site question

Stackoverflow: Adding middleware in Carter framework makes some URL to not trigger

which looked like it could benefit from what I had learned from the previous question.

It is related to a GitHub library called Carter

Carter is a library that allows Nancy-esque routing for use with ASP.Net Core.

The OP was hoping to be able to add tasks to the expected delegate for the middleware's CarterOptions class.

/// <summary>
/// Initializes <see cref="CarterOptions"/>
/// </summary>
/// <param name="before">A global before handler which is invoked before all routes</param>
/// <param name="after">A global after handler which is invoked after all routes</param>
/// <param name="openApiOptions">A <see cref="OpenApiOptions"/> instance to configure OpenApi</param>
public CarterOptions(Func<HttpContext, Task<bool>> before = null, Func<HttpContext, Task> after = null, OpenApiOptions openApiOptions = null) {
    //...
}

This is the before code where the options is created directly.

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
    var options = new CarterOptions(
        before: ctx => {
            ctx.Request.Header["x-request-id"] = Guid.NewGuid().ToString();
            return this.BeforeLog(ctx);
        },
        after: ctx => AfterLog(ctx)
    );

    app.UseCarter(options);

    //...
}

I came up with the following builder for the middleware's options class

sealed class CarterOptionsBuilder {
    delegate Task<bool> BeforeDelegate(HttpContext context);
    delegate Task AfterDelegate(HttpContext context);

    private readonly Stack<Func<BeforeDelegate, BeforeDelegate>> befores = new Stack<Func<BeforeDelegate, BeforeDelegate>>();
    private readonly Stack<Func<AfterDelegate, AfterDelegate>> afters = new Stack<Func<AfterDelegate, AfterDelegate>>();

    public CarterOptionsBuilder AddBeforeHook(Func<HttpContext, Task<bool>> handler) {
        befores.Push(next => async context => {
            return await handler(context) && await next(context);
        });
        return this;
    }

    public CarterOptionsBuilder AddAfterHook(Func<HttpContext, Task> handler) {
        afters.Push(next => context => {
            handler(context);
            return next(context);
        });
        return this;
    }

    public CarterOptions Build(OpenApiOptions openApiOptions = null) {
        var before = new BeforeDelegate(c => Task.FromResult(true));
        while (befores.Any()) {
            var current = befores.Pop();
            before = current(before);
        }
        var after = new AfterDelegate(c => Task.CompletedTask);
        while (afters.Any()) {
            var current = afters.Pop();
            after = current(after);
        }
        return new CarterOptions(before.Invoke, after.Invoke, openApiOptions);
    }
}

Which allowed for functionality like

public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
     CarterOptions options = new CarterOptionsBuilder()
        .AddBeforeHook(this.AddRequestId)
        .AddBeforeHook(this.BeforeLog)
        .AddAfterHook(this.AfterLog)
        .Build();

    app.UseCarter(options);
}

private Task AfterLog(HttpContext arg) {
    //...
    return Task.CompletedTask;
}

private Task<bool> BeforeLog(HttpContext arg) {
    //...
    return Task.FromResult(true);
}

private Task<bool> AddRequestId(HttpContext ctx) {
    ctx.Request.Header["x-request-id"] = Guid.NewGuid().ToString();
    return Task.FromResult(true);
}

This does open extensibility for more customization like

public static CarterOptionsBuilder AddLog(this CarterOptionsBuilder builder) {
    return builder
        .AddBeforeHook(this.BeforeLog)
        .AddAfterHook(this.AfterLog);        
}
private static Task AfterLog(HttpContext arg) {
    //...
    return Task.CompletedTask;
}

private static Task<bool> BeforeLog(HttpContext arg) {
    //...
    return Task.FromResult(true);
}

Might consider contributing it as a pull-request to the repository.

Is it even worth the trouble?

Thoughts and opinions on the design, given the original feature request.

\$\endgroup\$
  • 1
    \$\begingroup\$ Cool, inspired by my question ;-) I think HandleAfter is incorrect. after(context); should be placed below return next(context);... if this means after-a-request. \$\endgroup\$ – t3chb0t Aug 19 at 18:00
  • \$\begingroup\$ @t3chb0t in this case the delegate(s) is not after a request per say, but a desired action/task that is to be perform after a request. \$\endgroup\$ – Nkosi Aug 19 at 18:02
  • \$\begingroup\$ ok, I guess then I need to take a look at that framework first as these concepts are new to me. \$\endgroup\$ – t3chb0t Aug 19 at 18:04
  • \$\begingroup\$ @t3chb0t it is technically two pipelines. one is invoked before request, the other after. But each pipeline acts like a normal pipeline. \$\endgroup\$ – Nkosi Aug 19 at 18:05
2
\$\begingroup\$

I still don't undestand that framework but there are few minor things that I think could be improved anyway:

  • AddBeforeHook & AddAfterHook could actually use the BeforeDelegate & AfterDelegate respectively instead of Funcs.
  • I would rename handler to before & after since the third one is already called next. I find this way it would be clearer what they handle.
  • I think you should be able to rewrite the whiles in the Build method with an Aggregate like:

    before = befores.Aggregate(before, (current, next) => next(current));
    

    or even

    var before = befores.Aggregate(new BeforeDelegate(c => Task.FromResult(true)), (current, next) => next(current));
    

    Elements are enumerated in the Pop order.

\$\endgroup\$
  • \$\begingroup\$ Like the aggregates idea. \$\endgroup\$ – Nkosi Aug 22 at 18:36
  • \$\begingroup\$ Had them named previously before and after, but thought they caused confusion which to me was evident in your comment. \$\endgroup\$ – Nkosi Aug 22 at 18:38
  • \$\begingroup\$ @Nkosi my comment was a sign of a lack of knowledge about how this framework works. I think to someone who knows the ins and outs of it, make before and after probably more sense than just handler. \$\endgroup\$ – t3chb0t Aug 22 at 18:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.