Appearance
Are you an LLM? You can read better optimized documentation at /advanced_topics/custom_middleware.md for this page in Markdown format
Custom middleware
This document provides a technical overview of the custom middleware components implemented in TheExampleApp. Middleware in ASP.NET Core is software that is assembled into an application pipeline to handle requests and responses. This application uses custom middleware for cross-cutting concerns such as generating breadcrumbs and enabling dynamic configuration via query parameters.
For a higher-level view of the application's structure, please see the Application Overview.
Building middleware
TheExampleApp leverages the ASP.NET Core middleware pipeline to inject custom logic into the request processing flow. The primary custom components are the Breadcrumbs middleware and the Deeplinker middleware.
These components are registered in the Configure method of the Startup.cs class. The order of registration is critical as it defines the order in which they process an incoming request.
csharp
// File: TheExampleApp/Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ... other middleware (exception handling, static files, etc.)
app.UseSession();
// ... request localization middleware
app.UseStatusCodePagesWithReExecute("/Error");
// Custom middleware is registered here
app.UseBreadcrumbs();
app.UseDeeplinks();
app.UseMvc(routes =>
{
// ... route configuration
});
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
As shown, our custom middleware is executed after session and localization have been configured but before the MVC framework handles the request.
Breadcrumbs middleware
The Breadcrumbs middleware is responsible for automatically generating a breadcrumb trail based on the current request's URL path. This data is then made available to the Razor views for rendering.
Implementation as extension method
The middleware is implemented in the Breadcrumbs class and is registered in the pipeline via the UseBreadcrumbs extension method, which is a standard convention in ASP.NET Core.
csharp
// File: TheExampleApp/Configuration/Breadcrumbs.cs
/// <summary>
/// Extension to add <see cref="Breadcrumbs"/> middleware to the middleware pipeline.
/// </summary>
public static class BreadcrumbsMiddlewareExtensions
{
/// <summary>
/// Adds <see cref="Breadcrumbs"/> to the middleware pipeline.
/// </summary>
/// <param name="builder">The application builder to use.</param>
/// <returns>The application builder.</returns>
public static IApplicationBuilder UseBreadcrumbs(this IApplicationBuilder builder)
{
return builder.UseMiddleware<Breadcrumbs>();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Request pipeline integration
The core logic resides in the Invoke method of the Breadcrumbs class. On each request, it performs the following steps:
- Retrieves the request path (e.g.,
/Courses/Course-X/Lessons/Lesson-Y). - Splits the path into its constituent parts.
- Initializes a
List<Breadcrumb>with a "Home" link. - Iterates through each part of the path, creating a
Breadcrumbobject for it. - For each part, it attempts to find a localized label using
IViewLocalizer. For example, it will look for a localization key likecoursesLabelfor the path segmentCourses. If no translation is found, it formats the segment by replacing hyphens with spaces (e.g.,Course-XbecomesCourse X). - Stores the final
List<Breadcrumb>in theHttpContext.Itemscollection with the key"breadcrumbs". This makes the list available for the duration of the request.
csharp
// File: TheExampleApp/Configuration/Breadcrumbs.cs
public async Task Invoke(HttpContext context)
{
var path = context.Request.Path;
var parts = path.ToString().Split("/", StringSplitOptions.RemoveEmptyEntries);
var items = new List<Breadcrumb>();
items.Add(new Breadcrumb { Label = _localizer["homeLabel"].Value, Path = "/" });
var translations = _localizer.GetAllStrings(false);
foreach (var part in parts)
{
var label = part.Replace("-", " ");
// Attempt to find a localized version of the label
if(translations.Any(c => c.Name == $"{LowerCaseFirstChar(label)}Label"))
{
label = _localizer[$"{LowerCaseFirstChar(label)}Label"].Value;
}
items.Add(new Breadcrumb { Label = label, Path = $"/{string.Join('/', parts.Take(Array.IndexOf(parts, part) + 1))}" });
}
// Store the breadcrumbs for the current request
context.Items["breadcrumbs"] = items;
// Pass control to the next middleware in the pipeline
await _next(context);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Dynamic Label Replacement with BreadcrumbsManager
The middleware generates breadcrumb labels from the URL path segments. However, these segments (slugs) are often not user-friendly. For example, a course's title might be "Advanced C# Programming" while its slug is advanced-c-sharp.
The BreadcrumbsManager service is designed to solve this. It allows other parts of the application (typically a Razor Page or MVC Controller) to replace the auto-generated label with a more descriptive one, usually fetched from the Contentful CMS.
It is injected via dependency injection as IBreadcrumbsManager and uses IHttpContextAccessor to access the List<Breadcrumb> that the middleware previously stored in HttpContext.Items.
csharp
// File: TheExampleApp/Configuration/Breadcrumbs.cs
public class BreadcrumbsManager : IBreadcrumbsManager
{
// ... constructor and private fields
/// <summary>
/// Replaces the label of a <see cref="Breadcrumb"/> for a specific slug with a new label.
/// </summary>
/// <param name="slug">The slug or "path" of the breadcrumb that will have its label replaced.</param>
/// <param name="label">The label to replace the current one with.</param>
public void ReplaceCrumbForSlug(string slug, string label)
{
if(_crumbs.Any(c => c.Label == slug))
{
_crumbs.First(c => c.Label == slug).Label = label;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
For more details on how this feature is used in the UI, see the Breadcrumbs Feature documentation.
Deeplinks middleware
The Deeplinker middleware provides a powerful mechanism for dynamically altering the application's configuration at runtime via query string parameters. This is primarily used to facilitate content previews from the Contentful CMS and to enable special editorial features without requiring a code deployment or configuration change.
UseDeeplinks() implementation
Like the breadcrumbs middleware, Deeplinker is added to the pipeline using a dedicated extension method, UseDeeplinks().
csharp
// File: TheExampleApp/Configuration/Deeplinker.cs
public static class DeeplinkerMiddlewareExtensions
{
public static IApplicationBuilder UseDeeplinks(this IApplicationBuilder builder)
{
return builder.UseMiddleware<Deeplinker>();
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
The Invoke method of the Deeplinker class inspects the request's query string for specific parameters:
- Contentful Configuration (
space_id,preview_token,delivery_token,api): If these parameters are present, the middleware constructs a newContentfulOptionsobject. This object is then validated and, if valid, serialized into the user's session. This effectively overrides the application's default Contentful settings for that user's session, allowing them to view content from a different space or toggle between the Delivery and Preview APIs. - Editorial Features (
editorial_features): If this parameter is present (e.g.,?editorial_features=enabled), its state is stored in the session. This allows the application to conditionally render UI elements for content editors. See the Editorial Features documentation for more information.
csharp
// File: TheExampleApp/Configuration/Deeplinker.cs
public async Task Invoke(HttpContext context)
{
var query = context.Request.Query;
// Check for full Contentful configuration override
if (query.ContainsKey("space_id") && query.ContainsKey("preview_token") && query.ContainsKey("delivery_token"))
{
// ... logic to build, validate, and store new ContentfulOptions in session
// On validation failure, it redirects to /settings
}
// Check for just API switching (cda vs cpa)
else if (query.ContainsKey("api"))
{
// ... logic to update UsePreviewApi and store in session
}
// Check for editorial features toggle
if (query.ContainsKey("editorial_features"))
{
var state = string.Equals(query["editorial_features"], "enabled", StringComparison.InvariantCultureIgnoreCase) ? "Enabled" : "Disabled";
context.Session.SetString("EditorialFeatures", state);
}
await _next(context);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
The session-based ContentfulOptions are then consumed by a custom IContentfulOptionsManager to configure the IContentfulClient for each request. For details on this mechanism, refer to the Service Configuration documentation.
Middleware ordering
The placement of app.UseDeeplinks() in Startup.cs is critical:
- It must be placed after
app.UseSession(). TheDeeplinkermiddleware relies heavily on reading from and writing to theHttpContext.Sessionto persist the dynamic configuration. Without the session middleware running first, any attempt to access the session would result in an exception. - It should be placed before
app.UseMvc(). This ensures that by the time the MVC framework selects a page or controller to handle the request, the dynamic configuration (especially the Contentful client settings) has already been applied. This allows services injected into the page/controller to use the correct configuration for the current request.
Creating your own middleware
You can create new middleware components by following the class-based pattern established in the project.
Middleware pattern
- Create a class with a public constructor that accepts
RequestDelegateas its first parameter. This delegate represents the next component in the middleware pipeline. - Inject dependencies by adding them as additional parameters to the constructor. The dependency injection container will provide them.
- Implement an
InvokeorInvokeAsyncmethod. This method must be public, return aTask, and acceptHttpContextas its first parameter. Additional dependencies can also be injected directly into this method.
Here is a basic template:
csharp
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
// using Your.Services;
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;
// private readonly IMyService _myService; // Example of injected service
// Dependencies are injected via the constructor
public MyCustomMiddleware(RequestDelegate next /*, IMyService myService */)
{
_next = next;
// _myService = myService;
}
public async Task InvokeAsync(HttpContext context)
{
// --- Code to execute before the next middleware runs ---
// This is where you can inspect and modify the incoming request.
// For example: context.Items["my-data"] = "some-value";
// Call the next middleware in the pipeline
await _next(context);
// --- Code to execute after the next middleware runs ---
// This code runs on the "way back" of the pipeline.
// You can inspect and modify the outgoing response here.
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Async processing
The InvokeAsync method is asynchronous. This is crucial for performance, as it allows the thread to be released while waiting for I/O-bound operations (e.g., database queries, API calls) to complete, preventing thread pool starvation. Always use await for asynchronous calls within your middleware.
Next() delegation
The _next delegate is the link to the rest of the pipeline.
await _next(context);: This call passes theHttpContextto the next middleware component. It is essential to include this line if you want the request to be processed further by subsequent middleware and eventually by the endpoint (e.g., the Razor Page).- Short-circuiting: You can choose not to call
_next(context). This effectively stops the pipeline and begins returning a response to the client. This is useful for middleware that handles authentication, authorization, or, as seen in theDeeplinker, performs a redirect. If you short-circuit the pipeline, you are responsible for writing to thecontext.Response.