. NET CORE Middleware

Posted by bagpuss03 on Fri, 05 Jun 2020 04:01:27 +0200

Original text: . NET CORE Middleware

What is Middleware

We are not new to middleware. Before the emergence of. NET CORE, the concept of middleware has been widely used in win applications.
Official definition of middleware: middleware is a module integrated into the application pipeline to process requests and responses. Each middleware can:

  • Choose whether to pass the request to the next component of the pipeline
  • Work can be performed before and after the next component of the pipeline

ASP.NETCORE The middleware in is essentially a request delegation func < requestdelegate, requestdelegate > middleware.
RequestDelegate itself is also a delegate, defined as public delegate Task RequestDelegate(HttpContext Context).
stay ASP.NETCORE In the request pipeline, a delegation chain is formed.



Request pipeline short circuit: when the delegate does not choose to pass the request to the next delegate, it is called "short circuit".

How to create Middleware

stay ASP.NETCORE Use iaapplicationbuilder to create / insert middleware pipes. There are two kinds of ways: Run and use. Dependency package Microsoft.AspNetCore.Http.Abstractions
Run is a kind of agreed terminal pipeline, i.e. short circuit. The next delegation is no longer executed

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

		
        app.Run(async context => { await context.Response.WriteAsync("hello world 1"); });
		//This is not going to happen!!
		app.Run(async context => { await context.Response.WriteAsync("hello world 2"); });

    }

Use usually provides middleware with extension method, which is suitable for dealing with some AOP transactions.

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.Use(async (context, next) =>
        {
            //You can do something before you invoke
            await next.Invoke();
            //You can do something after invoking
        });

        app.Run(async context => { await context.Response.WriteAsync("hello world"); });
    }

In actual development, we usually need to define middleware by ourselves, which can be implemented in two ways.

Agreed mode

public class RequestIdInRequestMiddleware
{
    private readonly RequestDelegate _next;

    public RequestIdInRequestMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public Task Invoke(HttpContext httpContext,IService service)
    {
		service.SayHello();
        //request head
        var requestId = Guid.NewGuid().ToString("n");
        httpContext.Request.Headers.Add("REQUESTID", requestId);

        return _next(httpContext);
    }
}

As agreed above:

  • Has a parameter public constructor of type RequestDelegate
  • A public method named Invoke or InvokeAsync, which must:
    • Return to Task
    • The first parameter is HttpContext

At present, it is officially recommended to use the agreed method. Note: the life cycle of this method added to the pipeline is a single example. Therefore, if you rely on some services, it is recommended to inject from the method parameters of Invoke or InvokeAsync, rather than from the constructor. (think about why? Singleton constructor injection requires the Service life cycle.

Strong type

The official also provides the IMiddleware interface to extend the creation of middleware. This approach has two advantages:

  • Can be injected on demand (lifecycle)

  • Middleware strong type words, easier to understand

      public class RequestIdInResponseMiddleware:IMiddleware
      {
          private readonly IService _service;
    
          public RequestIdInResponseMiddleware(IService service)
          {
              _service = service;
          }
    
          public Task InvokeAsync(HttpContext context, RequestDelegate next)
          {
              var requestId = Guid.NewGuid().ToString("n");
              context.Response.Headers.Add("REQUESTID", requestId);
    
              return next(context);
          }
      }
    

Middleware join pipeline

Middleware is generally based on iaapplicationbuilder extension method to join the pipeline.

public static class RequestIdMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestIdInResponseMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestIdInResponseMiddleware>();
    }
}

Can be invoked in the Configure method. app.UseRequestIdInResponseMiddleware();
If you create Middleware in a strongly typed way, you also need to register in ConfigureServices services.AddSingleton <RequestIdInResponseMiddleware>();

Order of Middleware

Middleware is significantly affected by the order of joining. The official default middleware sequence diagram

Middleware branch Map

The Map extension is used to contract the creation of a pipeline branch, similar to a pipeline short circuit, but it creates a request pipeline branch based on a given request path match. Official examples,

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

Respond to different results according to the request

request response
localhost:1234 Hello from non-Map delegate.
localhost:1234/map1 Map Test 1
localhost:1234/map2 Map Test 2
localhost:1234/map3 Hello from non-Map delegate.

In addition, you can use UseWhen to create a pipe branch, and the pipe will only be short circuited if certain conditions are matched.

public void Configure(IApplicationBuilder app)
{
	//The pipeline will be short circuited only if the request url contains the query string variable branch
    app.UseWhen(context => context.Request.Query.ContainsKey("branch"),
                builder => builder.Use(async (context, next) =>
                     {
                         var branchVer = context.Request.Query["branch"];
                         // Do work that doesn't write to the Response.
                         await next();
                         // Do other work that doesn't write to the Response.
                     }));

    app.Run(async context =>
    {
        await context.Response.WriteAsync("Hello from main pipeline.");
    });
}

Unit test of Middleware

For unit testing of middleware, test server can be used. It has the following advantages:

  • The request is sent to memory instead of being serialized over the network
  • Avoid additional problems, such as port number or Https, etc
  • Exceptions in middleware can flow back to call test directly
  • You can customize the server data structure directly in the test, such as HttpContext

http request sending simulation can use HttpClient and HttpContext to verify the relevant functions of Response and Request Context respectively. Next, test requestidinrequestmidleware and requestidinresponsemidleware respectively.
Create a new xunit unit test project and add a dependency package: Microsoft.AspNetCore.TestHost , Microsoft.Extensions.Hosting .
The test code is as follows:

public class MiddlewareTest
{
    /// <summary>
    ///HttpContext simulation to verify whether the request header is successfully added to the requestId
    /// </summary>
    [Fact]
    public void MiddlewareTest_RequestHeaderExistRequestId()
    {
        var hostBuilder = new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices((context, services) =>
                    {
                        services.AddTransient<IService, MyService>();
                    })
                    .Configure(app =>
                    {
                        app.UseRequestIdInRequestMiddleware();
                    });
            });
        using (var host = hostBuilder.Start())
        {
            var context = host.GetTestServer().SendAsync(c =>
                    {
                        c.Request.Path = "/map";
                        c.Request.Method = HttpMethods.Get;
                    }).Result;

            Assert.True(context.Request.Headers.ContainsKey("REQUESTID"));
        }
    }
    /// <summary>
    ///HttpClient simulation to verify whether the response header is successfully added to the requestId
    /// </summary>
    [Fact]
    public void MiddlewareTest_ResponseHeaderExistRequestId()
    {
        var hostBuilder = new HostBuilder()
            .ConfigureWebHost(webBuilder =>
            {
                webBuilder
                    .UseTestServer()
                    .ConfigureServices((context, services) =>
                    {
                        services.AddSingleton<RequestIdInResponseMiddleware>();
                        services.AddTransient<IService, MyService>();
                    })
                    .Configure(app =>
                    {
                        app.UseRequestIdInResponseMiddleware();
                    });
            });
        using (var host = hostBuilder.Start())
        {
            host.GetTestServer().CreateRequest("/map").GetAsync()
                .ContinueWith(task =>
                {
                    var response = task.Result;
                    Assert.True(response.Headers.Contains("REQUESTID"));
                }).Wait();
        }
    }
}

Topics: network