Pipeline mechanism in ASP.NET Core

Original: Pipeline mechanism in ASP.NET Core

First of all, thank you very much for your last article. C# Pipeline Programming Thank you for your support and affirmation. I hope that every time I share with each other, I can get some benefits. Of course, if I have some incorrect or inappropriate narrative, please point it out. Okay, let's move on to the text.

Preface

Before we start, we need to make it clear that in Web applications, the user's request flow is linear. In ASP.NET Core programs, there is a request pipeline. In this request pipeline, we can dynamically configure middleware corresponding to various business logic. (middleware), so that the server can respond to different requests for different users. In ASP.NET Core, pipeline programming is a core and basic concept. Many of its middleware are eventually configured into the request pipeline by pipeline. Therefore, understanding pipeline programming in ASP.NET Core is very important for us to write more robust DotNet Core programs.

Analysis of Pipeline Mechanism

In the above discussion, we mentioned two important concepts: request pipeline and middleware. As for the relationship between them, my personal understanding is that first, request pipeline serves users. Secondly, request pipeline can connect multiple independent business logic modules (i.e. middleware) in series, and then serve users'requests. The advantage of doing so is that business logic can be hierarchical, because in the actual business scenario, some business processes are independent of each other and depend on other business operations, and the relationship between business modules is actually dynamic and unstable.

Next, we try to step by step analyze the pipeline mechanism in ASP.NET Core.

theoretical explanation

First, let's look at the official illustrations:

From the above figure, it is not difficult to see that when a user makes a request, the application creates a request pipeline for it. In this request pipeline, each middleware will be processed sequentially (may or may not be executed, depending on the specific business logic), and the last one. After processing, the request will be returned to the user in the opposite direction.

Code Interpretation

To validate our theoretical explanations, we started to create a DotNetCore console project and then quoted the following packages:

  • Microsoft.AspNetCore.App

Write the following sample code:

class Program
{
    static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    private static IHostBuilder CreateHostBuilder(string[] args) =gt;
        Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =gt;
        {
            webBuilder.UseStartuplt;Startupgt;();
        });
}

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // Middleware A
        app.Use(async (context, next) =gt;
        {
            Console.WriteLine(quot;A (in)quot;);
            await next();
            Console.WriteLine(quot;A (out)quot;);
        });

        // Middleware B
        app.Use(async (context, next) =gt;
        {
            Console.WriteLine(quot;B (in)quot;);
            await next();
            Console.WriteLine(quot;B (out)quot;);
        });

        // Middleware C
        app.Run(async context =gt;
        {
            Console.WriteLine(quot;Cquot;);
            await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;);
        });
    }
}

The above code snippet shows the simplest ASP.NET Core Web program, try F5 to run our program, and then open the browser access. http://127.0.0.1:5000 You can see that the browser displays information about Hello World from the terminal middleware. The corresponding console information is shown in the following figure:

The above example program successfully validates some assumptions in our theoretical explanation, which indicates that a completed request pipeline has been successfully constructed in the Configure function. In this case, we can modify it to the way we used the pipeline before. The sample code is as follows:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =gt;
        {
            Console.WriteLine(quot;A (int)quot;);
            await next();
            Console.WriteLine(quot;A (out)quot;);
        }).Use(async (context, next) =gt;
        {
            Console.WriteLine(quot;B (int)quot;);
            await next();
            Console.WriteLine(quot;B (out)quot;);
        }).Run(async context =gt;
        {
            Console.WriteLine(quot;Cquot;);
            await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;);
        });
    }
}

Both of these ways can make our request pipeline work properly, but in different ways. It depends entirely on personal preferences. It should be noted that the last console middleware needs to be registered last because its processing is one-way and does not involve returning user requests after modification.

Similarly, we can also make conditional assembly (bifurcation routing) for our pipeline middleware. The assembly conditions can be based on specific business scenarios. Here I use routing as a condition to assemble. Different access routes end up accessing different middleware. The sample code is as follows:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // Middleware A
        app.Use(async (context, next) =gt;
        {
            Console.WriteLine(quot;A (in)quot;);
            await next();
            Console.WriteLine(quot;A (out)quot;);
        });

        // Middleware B
        app.Map(
                new PathString(quot;/fooquot;),
                a =gt; a.Use(async (context, next) =gt;
                {
                    Console.WriteLine(quot;B (in)quot;);
                    await next();
                    Console.WriteLine(quot;B (out)quot;);
                }));

        // Middleware C
        app.Run(async context =gt;
        {
            Console.WriteLine(quot;Cquot;);
            await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;);
        });
    }
}

When we visit directly http://127.0.0.1:5000 The corresponding request routing output is as follows:

The corresponding page echoes Hello World from the terminal middleware

When we visit directly httP://127.0.0.1:5000/foo The corresponding request routing output is as follows:

When we try to look at the corresponding request page, we find that the corresponding page is HTTP ERROR 404. From the above output, we can find the reason that the last registered terminal route failed to be successfully invoked, resulting in the failure to return the corresponding request results. There are two solutions to this situation.

One is to return the request result directly in our Route B. The sample code is as follows:

app.Map(
    new PathString(quot;/fooquot;),
    a =gt; a.Use(async (context, next) =gt;
    {
        Console.WriteLine(quot;B (in)quot;);
        await next();
        await context.Response.WriteAsync(quot;Hello World from the middleware Bquot;);
        Console.WriteLine(quot;B (out)quot;);
    }));

This approach is not recommended because it easily leads to inconsistencies in business logic and violates the single responsibility principle.

Another solution is through routing matching. The sample code is as follows:

app.UseWhen(
    context =gt; context.Request.Path.StartsWithSegments(new PathString(quot;/fooquot;)),
    a =gt; a.Use(async (context, next) =gt;
    {
        Console.WriteLine(quot;B (in)quot;);
        await next();
        Console.WriteLine(quot;B (out)quot;);
    }));

By using UseWhen, a business condition corresponding to a business middleware is added, which automatically returns to the main request pipeline after execution. The final corresponding log output is shown in the following figure:

Similarly, we can customize a middleware. The sample code is as follows:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // app.UseMiddlewarelt;CustomMiddlewaregt;();
        //Equivalent to the following invocation
        app.UseCustomMiddle();

        // Middleware C
        app.Run(async context =gt;
        {
            Console.WriteLine(quot;Cquot;);
            await context.Response.WriteAsync(quot;Hello World from the terminal middlewarequot;);
        });
    }
}

public class CustomMiddleware
{
    private readonly RequestDelegate _next;
    public CustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        Console.WriteLine(quot;CustomMiddleware (in)quot;);
        await _next.Invoke(httpContext);
        Console.WriteLine(quot;CustomMiddleware (out)quot;);
    }
}

public static class CustomMiddlewareExtension
{
    public static IApplicationBuilder UseCustomMiddle(this IApplicationBuilder builder)
    {
        return builder.UseMiddlewarelt;CustomMiddlewaregt;();
    }
}

The log output is shown in the following figure:

Because the custom Middleware in ASP.NET Core is instantiated by Dependency Injection (DI). So the corresponding constructor, we can inject the data type we want, not just Request Delegate; secondly, our custom middleware also needs to implement a public public void Invoke(HttpContext httpContext) or public async Task Invoke Async (HttpContext httpContext) method, the party. The internal law mainly deals with our custom business and links the middleware, playing a pivotal role.

Source code analysis

Since ASP.NET Core is completely open source and cross-platform, we can easily use it in Github Find its corresponding trusteeship warehouse. Finally, we can take a look at some of the official ASP.NET Core implementation code. As shown in the following figure:

Officially open source all the implementation code of built-in middleware. Here I take Heath Checks middleware as an example to verify the implementation of the custom middleware we mentioned above.

By looking at the source code, we can see that the above custom middleware meets the official implementation standards. Similarly, when we use a built-in middleware later, if we are interested in its specific implementation, we can view it in this way.

summary

When we configure ASP.NET Core's request pipeline middleware, one thing we should pay attention to is that the configuration of middleware must be in specific business logic order. For example, gateway configuration must precede routing configuration. The code is the following example:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    //......
    app.UseAuthentication();
    //......
    app.UseMvc();
}

If our middleware is not configured in proper order, it is likely to cause problems in the corresponding business.

As far as the technical framework of ASP.NET Core is concerned, pipeline programming is only a very small and basic part of it. The design and implementation of the whole technical framework uses many excellent technologies and architectural ideas. However, these high-level implementations are based on the evolution of basic technology, so the foundation is very important. Only when the foundation is solid, it will not be eliminated by the technology wave.

All of the above is my personal understanding and humble opinion on pipeline programming in ASP.NET Core. If there are incorrect or inappropriate points, please correct them.

Let's hope so!

Relevant references

Keywords: PHP Programming github

Added by jminscoe on Thu, 25 Jul 2019 03:41:56 +0300