Advanced ASP.NET Core MVC Topics

Deep dive into powerful features for building robust web applications.

Understanding Middleware Pipelines

Middleware forms the backbone of ASP.NET Core's request processing. It's a series of components that handle requests and responses sequentially. Each middleware component can:

  • Execute code before the rest of the pipeline.
  • Call the next middleware in the pipeline.
  • Short-circuit the pipeline by not calling the next middleware.
  • Execute code after the next middleware has completed.

Key built-in middleware includes:

  • UseRouting: Enables endpoint routing.
  • UseAuthentication: Handles authentication.
  • UseAuthorization: Handles authorization.
  • UseEndpoints: Executes the selected endpoint.
The order of middleware registration is crucial. Typically, routing should happen early, followed by authentication and authorization.

Custom Middleware

You can create your own middleware by implementing an interface or by using a factory function.

public class CustomMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        // Logic before calling next middleware
        Console.WriteLine("Request entering custom middleware...");

        await _next(context); // Call the next middleware

        // Logic after calling next middleware
        Console.WriteLine("Request exiting custom middleware...");
    }
}

// In Startup.cs (or Program.cs in .NET 6+)
app.UseMiddleware<CustomMiddleware>();

Advanced Routing Techniques

Beyond simple attribute routing, ASP.NET Core MVC offers more sophisticated routing capabilities.

Convention-Based Routing

While less common now with attribute routing, convention-based routing allows defining URL patterns globally.

endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Route Constraints

You can define constraints to validate parts of the URL.

[Route("products/[controller]/{id:int}")]
public class ProductController : Controller
{
    // ...
}

Common constraints include: int, guid, alpha, decimal, datetime, minlength, maxlength, regex.

Parameter Defaults

Specify default values for route parameters.

[Route("search/[action=Index]")]
public IActionResult Search() { /* ... */ }

Action-Based Routing with Attributes

Use [HttpGet], [HttpPost], etc., for finer control.

[HttpGet("details/{productId}")]
public IActionResult GetProductDetails(int productId) { /* ... */ }

[HttpPost("create")]
public IActionResult CreateProduct([FromBody] ProductModel product) { /* ... */ }

Implementing Filters

Filters provide a powerful mechanism to encapsulate cross-cutting concerns like logging, authorization, caching, and exception handling. They can be applied at the controller or action level.

Filter Types

  • Authorization Filters: Implement IAsyncAuthorizationFilter. Used to check user permissions.
  • Resource Filters: Implement IAsyncResourceFilter. Execute before and after other filters, useful for caching.
  • Action Filters: Implement IAsyncActionFilter. Execute before and after an action method.
  • Result Filters: Implement IAsyncResultFilter. Execute before and after the action result is executed.
  • Exception Filters: Implement IAsyncExceptionFilter. Handle exceptions thrown during controller execution.
  • Page Filters (Razor Pages): Implement IAsyncPageFilter.

Applying Filters

// Apply to a specific action
[HttpGet("special")]
[ServiceFilter(typeof(CustomActionFilter))]
public IActionResult SpecialAction() { /* ... */ }

// Apply to an entire controller
[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
    // ...
}

// Applying globally in Startup.cs (or Program.cs)
services.AddControllersWithViews(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});

Custom Exception Filter Example

public class CustomExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        // Log the exception
        Console.WriteLine($"Exception: {context.Exception.Message}");

        // Optionally handle the exception
        context.Result = new ContentResult { Content = "An unexpected error occurred." };
        context.ExceptionHandled = true;
    }
}

Dependency Injection Deep Dive

ASP.NET Core has built-in support for Dependency Injection (DI). Understanding its nuances is key for testability and maintainability.

Service Lifetimes

  • Singleton: A single instance is created for the entire application lifetime.
  • Scoped: An instance is created for each client request.
  • Transient: A new instance is created every time it's requested.

Registering Services

// In Startup.cs (or Program.cs)
// Singleton
services.AddSingleton<IMyService, MyService>();

// Scoped
services.AddScoped<IMyScopedService, MyScopedService>();

// Transient
services.AddTransient<IMyTransientService, MyTransientService>();

Injecting Services

Services can be injected into constructors of controllers, Razor Pages, middleware, and other services.

public class MyController : Controller
{
    private readonly IMyService _myService;

    public MyController(IMyService myService)
    {
        _myService = myService;
    }

    public IActionResult Index()
    {
        var data = _myService.GetData();
        return View(data);
    }
}
Favor injecting interfaces over concrete implementations to promote loose coupling and easier testing.

Working with Background Tasks

For operations that don't need to be completed within the HTTP request pipeline, background tasks are essential.

IHostedService

The standard way to run background tasks in ASP.NET Core. Implement this interface and register it with the DI container.

public class MyBackgroundService : BackgroundService
{
    private readonly ILogger<MyBackgroundService> _logger;

    public MyBackgroundService(ILogger<MyBackgroundService> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Background service is running at: {time}", DateTimeOffset.Now);
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
}

// Registering in Startup.cs (or Program.cs)
services.AddHostedService<MyBackgroundService>();

Queued Background Tasks

For more complex scenarios, consider libraries like Hangfire or Quartz.NET, or implement your own message queue system.