This blog post will take a deep dive in the world of .NET Core Routing. It will try to enhance your understanding of how routing works. What role middleware and endpoints play.
.NET Core provides a powerful framework for building web applications, offering robust features like routing, middleware, and endpoints. These components play crucial roles in managing incoming HTTP requests, processing them, and generating responses.
What is Routing?
Routing refers to the process of matching incoming HTTP requests to specific endpoints or actions within the application. It determines which code should handle a particular request based on the URL pattern.
Apps can configure routing using:
- Controllers
- Razor Pages
- SignalR
- gRPC Services
- Endpoint-enabled middleware.
- Delegates and lambdas registered with routing.
What is a Middleware?
Middleware’s are software code, it is injected into the .NET Core app pipeline to handle requests and responses. In almost all cases they are not the final destination of the request, instead they are used to do some pre-tasks before the request can be passed on to the Action.
- Each of these component has a choice if it wants to pass on the control to the next middleware in the pipeline.
- These can perform action before and after the next component in the pipeline.
Web Application pipeline using middleware
What are Endpoints?
For each app the smallest unit of code or a method which can be directly hit by an HTTP request is called Endpoints. These URL based addresses can carry relevant data which are parsed and sent to the Method being requested by the endpoint.
Basic Routing.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
As you may have guessed that this is the basic form of routing, we have mapped the HTTP GET call to the root URL to a anonymous function which writes Hello World on the HTTP response. This is the simplest form of middleware.
Let us modify the code a bit more to show how to use custom middleware in the pipeline which runs before the URL is mapped .
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World extended!");
app.Run();
The code above does the following
app.Use
registers a custom middleware that should run at the beginning of the pipeline.UseRouting
makes sure that the custom middleware euns before the route matching middleware.MapGet
registers the endpoint which runs at the end of the pipeline.
Important aspect of this code is that UseRouting
is responsible for the custom middleware running before the route matching middleware.
Middleware use the following statements to handle routing Run,Map and Use.
Run,Map and Use Middleware
Run Method
This can be called as terminal middleware. A terminal middleware doesnot call the next component in the pipeline. It is advised that these type of middleware is placed at the end of the pipeline.
Use Method
This middleware can be used for configuration and is designed to call the next middleware in the pipeline and wait for it to complete before continuing further. Use method may be forced to become a terminal component like Run but it isnt advised. If you look at .NET MVC you will see USE is by far the most used middleware.
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.MapRazorPages();
This is an example of the components used by .NET Core MVC.
Lets try and see how the flow of Use and Run works. In the following code we will see that the Use Middleware after running the initial logic waits for the next to finish and then continues to finish its logic.
app.Use(async (ctx, nxt) =>
{
await ctx.Response.WriteAsync("Middleware 1 Fired \n");
await nxt();
await ctx.Response.WriteAsync("Middleware 1 Ends \n");
});
app.Use(async (ctx, nxt) =>
{
await ctx.Response.WriteAsync("Middleware 2 Fired\n");
await nxt();
await ctx.Response.WriteAsync("Middleware 2 Ends \n");
});
app.Run(async (ctx ) =>
{
await ctx.Response.WriteAsync("Middleware RUN 1 Fired\n");
});
app.Run(async (ctx) =>
{
await ctx.Response.WriteAsync("Middleware RUN 2 Fired \n");
});
The out put looks something like this.
As you can see in the output the second RUN is never called because the RUN middleware is a terminal component and each USE middleware waits for the next component to finish. The flow looks something like this.
Map Method
As the name suggests this method maps the request URL to a predefined action. They try to match the request path to the paths defined in the app.
app.Map("/path1", appVar =>
{
appVar.Run(async context =>
{
await context.Response.WriteAsync("PATH1 RUN FIRED");
});
});
app.Map("/path2", appVar2 =>
{
appVar2.Run(async context =>
{
await context.Response.WriteAsync("PATH2 RUN FIRED");
});
});
app.Map("/path3", appVar3 =>
{
appVar3.Run(async context =>
{
await context.Response.WriteAsync("PATH3 RUN FIRED");
});
});
app.Run(async (context) =>
{
await context.Response.WriteAsync("Default Path Fired By RUN");
});
app.Run();
In the preceding code we are mapping different Paths/Endpoints to our anonymous methods. The output is something like this
Writing Custom Middleware
.NET Core gives you an option that you can create your own custom middleware. Middleware is generally a class which is exposed through an extension method.
- This class must have a public contructor which accepts a RequestDelegate
- A public method named
Invoke
orInvokeAsync
. This method must:- Return a
Task
. - Accept a first parameter of type HttpContext.
- Return a
Following code shows a sample class which sets the culture for the current request. The culture info is passed through the URL. https://localhost:5001/?culture=es-es.
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline.
await _next(context);
}
}
public static class RequestCultureMiddlewareExtensions
{
public static IApplicationBuilder UseRequestCulture(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestCultureMiddleware>();
}
}
it can be used later as app.UseRequestCulture();
Conclusion
Routing, middleware, and endpoints form the backbone of request handling in ASP.NET Core applications. Understanding how routing directs incoming requests, how middleware processes them, and how endpoints define response generation is crucial for building robust and efficient web applications.
By leveraging these components effectively, developers can design scalable and maintainable applications in .NET Core, catering to various HTTP request-response scenarios.
Leave a Reply