Intermediate

ASP.NET Core MVC Deep Dive

MVC fundamentals through routing, Razor views, models, filters, security and performance.

Demo Application: GloboTicket / Globomantics (eCommerce — event tickets) ASP.NET Core with .NET 6/8, Visual Studio


Table of Contents

  1. Getting Started and MVC Fundamentals
  2. Routing
  3. Views and Razor
  4. Models and Data
  5. State Management
  6. Filters and Middleware
  7. Advanced Features

1. Getting Started and MVC Fundamentals

1.1 MVC Architecture

┌──────────────────────────────────────────────────────────────────┐
│                    MVC PATTERN                                   │
│                                                                  │
│  Browser                                                         │
│     │                                                            │
│     ▼ HTTP Request                                               │
│  ┌─────────┐     ┌────────────┐     ┌──────────┐               │
│  │ Routing │────►│ Controller │────►│  Model   │               │
│  └─────────┘     └────────────┘     └──────────┘               │
│                       │                   │                      │
│                       │◄──────────────────┘                     │
│                       │ (data)                                   │
│                       ▼                                          │
│                  ┌──────────┐                                    │
│                  │   View   │                                    │
│                  │ (Razor)  │                                    │
│                  └──────────┘                                    │
│                       │                                          │
│                       ▼ HTML Response                            │
│                  Browser                                         │
└──────────────────────────────────────────────────────────────────┘

1.2 Data Models

// Models/Product.cs
public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public string ImageUrl { get; set; } = string.Empty;
}

// Models/Cart.cs
public class Cart
{
    public Guid Id { get; set; }
    public List<LineItem> LineItems { get; set; } = new();
    public decimal Total => LineItems.Sum(li => li.Price * li.Quantity);
}

// Models/LineItem.cs
public class LineItem
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

// Models/Order.cs
public class Order
{
    public Guid Id { get; set; }
    public string CustomerFirstName { get; set; } = string.Empty;
    public string CustomerLastName { get; set; } = string.Empty;
    public string CustomerEmail { get; set; } = string.Empty;
    public List<LineItem> LineItems { get; set; } = new();
}

1.3 Dependency Injection

// Program.cs — Register services
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// Lifetimes
builder.Services.AddTransient<IProductService, ProductService>();   // New instance on each call
builder.Services.AddScoped<ICartService, CartService>();            // One instance per HTTP request
builder.Services.AddSingleton<IEventService, EventService>();       // Single instance throughout app

// EF Core
builder.Services.AddDbContext<GlobomanticsContext>(options =>
    options.UseSqlite("Data Source=globomantics.db"));
// Controllers/ProductController.cs
public class ProductController : Controller
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductController> _logger;
    
    public ProductController(IProductService productService, ILogger<ProductController> logger)
    {
        _productService = productService;
        _logger = logger;
    }
    
    public async Task<IActionResult> Index()
    {
        _logger.LogInformation("Loading products...");
        var products = await _productService.GetAllProductsAsync();
        return View(products);
    }
    
    public async Task<IActionResult> Details(Guid id)
    {
        var product = await _productService.GetProductByIdAsync(id);
        if (product == null) return NotFound();
        return View(product);
    }
}

2. Routing

2.1 Convention-based vs Attribute Routing

// Program.cs — Convention-based routing (globally defined)
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

// Generated route examples:
// /Home/Index        → HomeController.Index()
// /Product           → ProductController.Index()
// /Product/Details/5 → ProductController.Details(5)
// Attribute Routing — defined directly on controller/action
[Route("products")]
public class ProductController : Controller
{
    [Route("")]         // /products
    [Route("list")]     // /products/list
    public IActionResult Index() => View();
    
    [Route("{id:guid}")]  // /products/xxxxxxxx-...
    public IActionResult Details(Guid id) => View();
    
    [HttpPost]
    [Route("add-to-cart")]  // POST /products/add-to-cart
    public IActionResult AddToCart(AddToCartModel model) => RedirectToAction("Index");
}

2.2 Route Constraints

// Built-in constraints
[Route("{id:int}")]        // Integer: /product/123
[Route("{id:guid}")]       // GUID: /product/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
[Route("{id:min(1)}")]     // Integer >= 1
[Route("{id:range(1,99)}")] // Integer between 1 and 99
[Route("{name:alpha}")]    // Letters only
[Route("{name:maxlength(50)}")] // Max 50 characters

// Custom constraint with regex
[Route("{slug:regex(^[a-z0-9-]+$)}")]  // URL slug

// Create a custom constraint
public class SlugConstraint : IRouteConstraint
{
    private static readonly Regex SlugRegex = new("^[a-z0-9-]+$", RegexOptions.Compiled);
    
    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey,
        RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (!values.TryGetValue(routeKey, out var routeValue)) return false;
        var stringValue = Convert.ToString(routeValue);
        return !string.IsNullOrEmpty(stringValue) && SlugRegex.IsMatch(stringValue);
    }
}
// Program.cs — Register the custom constraint
builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("slug", typeof(SlugConstraint));
});

// Usage
[Route("{productSlug:slug}")]
public IActionResult BySlug(string productSlug) => View();

2.3 Parameter Transformers

// Transform parameters in URLs (e.g.: PascalCase → kebab-case)
public class SlugParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        if (value == null) return null;
        
        var stringValue = value.ToString()!;
        // "ProductDetails" → "product-details"
        return Regex.Replace(stringValue, "([a-z])([A-Z])", "$1-$2").ToLower();
    }
}

// Program.cs — Apply to all routes
builder.Services.AddControllersWithViews(options =>
{
    options.Conventions.Add(
        new RouteTokenTransformerConvention(new SlugParameterTransformer()));
});

2.4 URL Generation

// In a controller — via LinkGenerator
public class ProductController : Controller
{
    private readonly LinkGenerator _linkGenerator;
    
    public ProductController(LinkGenerator linkGenerator)
    {
        _linkGenerator = linkGenerator;
    }
    
    public IActionResult Index()
    {
        // Generate a URL in a type-safe way
        var url = _linkGenerator.GetPathByAction("Details", "Product", new { id = Guid.NewGuid() });
        // → "/product/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        
        return View();
    }
}
@* In views — Tag Helpers for URL generation *@
<a asp-controller="Product" asp-action="Details" asp-route-id="@product.Id">
    View Details
</a>

@* With routevalues *@
<a asp-controller="Product" asp-action="Index" asp-route-category="events">
    Events
</a>

3. Views and Razor

3.1 Razor Syntax

@* Implicit expressions (simple return value) *@
<h1>@Model.Name</h1>
<p>Price: @Model.Price.ToString("C")</p>
<p>Today: @DateTime.Now.ToShortDateString()</p>

@* Explicit expressions (avoid ambiguity) *@
<p>Total with tax: @(Model.Price * 1.2)</p>
<p>Code: @("Item" + Model.Id)</p>

@* Code blocks *@
@{
    var greeting = DateTime.Now.Hour < 12 ? "Good morning" : "Good evening";
    var cssClass = Model.IsAvailable ? "available" : "unavailable";
}

@* Control structures *@
@foreach (var item in Model.Items)
{
    <div class="item">@item.Name — @item.Price.ToString("C")</div>
}

@if (Model.Items.Any())
{
    <p>@Model.Items.Count item(s) total</p>
}
else
{
    <p>No items</p>
}

@switch (Model.Status)
{
    case "pending":
        <span class="badge bg-warning">Pending</span>
        break;
    case "confirmed":
        <span class="badge bg-success">Confirmed</span>
        break;
    default:
        <span class="badge bg-secondary">Unknown</span>
        break;
}

@* Directives *@
@page                        @* For Razor Pages (not MVC) *@
@model MyViewModel           @* Model type *@
@using MyNamespace           @* Namespace import *@
@inject IMyService Service   @* Service injection *@

3.2 Layout and ViewStart

@* Views/Shared/_Layout.cshtml *@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>@ViewData["Title"] — GloboTicket</title>
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav>
            <a asp-controller="Home" asp-action="Index">Home</a>
            <a asp-controller="Product" asp-action="Index">Tickets</a>
            
            @* View Component in the layout *@
            @await Component.InvokeAsync("CartSummary")
        </nav>
    </header>
    
    <main>
        @RenderBody()  @* Content of the specific view *@
    </main>
    
    @* Optional sections *@
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>
@* Views/_ViewStart.cshtml — Automatically applied to all views *@
@{
    Layout = "_Layout";
}

3.3 View Components

// ViewComponents/CartSummaryViewComponent.cs
[ViewComponent]
public class CartSummaryViewComponent : ViewComponent
{
    private readonly ICartService _cartService;
    
    public CartSummaryViewComponent(ICartService cartService)
    {
        _cartService = cartService;
    }
    
    public async Task<IViewComponentResult> InvokeAsync()
    {
        var cart = await _cartService.GetCartAsync(/* user session id */);
        var count = cart?.LineItems.Count ?? 0;
        return View(count);  // Uses Views/Shared/Components/CartSummary/Default.cshtml
    }
}
@* Views/Shared/Components/CartSummary/Default.cshtml *@
@model int

<a asp-controller="Cart" asp-action="Index" class="cart-icon">
    🛒
    @if (Model > 0)
    {
        <span class="badge">@Model</span>
    }
</a>
@* Call in a view or layout *@
@await Component.InvokeAsync("CartSummary")

@* Or via Tag Helper *@
<vc:cart-summary></vc:cart-summary>

3.4 Tag Helpers

@* Built-in ASP.NET Core Tag Helpers *@

@* Link with route *@
<a asp-controller="Product" asp-action="Details" asp-route-id="@product.Id">
    @product.Name
</a>

@* Form with automatic anti-forgery token *@
<form asp-controller="Order" asp-action="Create" method="post">
    <input asp-for="CustomerEmail" class="form-control" />
    <span asp-validation-for="CustomerEmail" class="text-danger"></span>
    <button type="submit">Order</button>
</form>

@* Image with cache-busting version *@
<img src="~/images/logo.png" asp-append-version="true" />

@* Conditional script for environment *@
<environment include="Development">
    <script src="~/js/site.js"></script>
</environment>
<environment exclude="Development">
    <script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
// Custom Tag Helper — example: Extended Anchor Tag Helper
[HtmlTargetElement("a", Attributes = "asp-require-auth")]
public class AuthAnchorTagHelper : AnchorTagHelper
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public AuthAnchorTagHelper(IHtmlGenerator generator, IHttpContextAccessor accessor)
        : base(generator)
    {
        _httpContextAccessor = accessor;
    }
    
    [HtmlAttributeName("asp-require-auth")]
    public bool RequireAuth { get; set; }
    
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (RequireAuth && !(_httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false))
        {
            output.SuppressOutput(); // Hide link if not authenticated
            return;
        }
        
        base.Process(context, output);
    }
}

// Program.cs — Auto or manual registration
// @addTagHelper *, MyAssembly  (in _ViewImports.cshtml)

3.5 Partial Views and Sections

@* Partial Views — convention: underscore prefix *@

@* Views/Shared/_ProductCard.cshtml *@
@model Product

<div class="product-card">
    <img src="@Model.ImageUrl" alt="@Model.Name" />
    <h3>@Model.Name</h3>
    <p>@Model.Price.ToString("C")</p>
    <a asp-controller="Product" asp-action="Details" asp-route-id="@Model.Id">View</a>
</div>
@* Call the partial view *@
@await Html.PartialAsync("_ProductCard", product)

@* Or with Tag Helper *@
<partial name="_ProductCard" model="product" />
@* Sections in the layout *@
@* _Layout.cshtml *@
<body>
    @RenderBody()
    
    @* Scripts section — optional *@
    @await RenderSectionAsync("Scripts", required: false)
</body>

@* Views/Product/Index.cshtml — Define the section *@
@section Scripts {
    <script>
        console.log("Script specific to the products page");
    </script>
}

4. Models and Data

4.1 Model Binding

// ASP.NET Core automatically binds parameters from multiple sources
// Priority order: route → query string → form body

// Explicit source with attributes
public IActionResult Search(
    [FromQuery] string? term,           // ?term=blazor
    [FromRoute] int? categoryId,        // /search/5
    [FromForm] string? sortBy,          // form body
    [FromBody] SearchCriteria? criteria, // JSON body
    [FromHeader] string? acceptLanguage) // Accept-Language header
{
    // ...
}

// ViewModel for forms
public class AddToCartModel
{
    [Required]
    public Guid ProductId { get; set; }
    
    [Range(1, 100)]
    public int Quantity { get; set; } = 1;
}

// POST action with model binding
[HttpPost]
public async Task<IActionResult> AddToCart(AddToCartModel model)
{
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    
    await _cartService.AddItemAsync(model.ProductId, model.Quantity);
    return RedirectToAction("Index", "Cart");
}

Array binding:

@* Form with list of items *@
<form method="post">
    @for (int i = 0; i < Model.Items.Count; i++)
    {
        <input name="Items[@i].ProductId" value="@Model.Items[i].ProductId" type="hidden" />
        <input name="Items[@i].Quantity" value="@Model.Items[i].Quantity" />
    }
    <button type="submit">Confirm</button>
</form>
// Controller receives the list
[HttpPost]
public IActionResult Confirm(List<OrderItem> items)
{
    // items is automatically populated from the form
}

4.2 Validation

// Data Annotations on the model
public class CustomerInfo
{
    [Required(ErrorMessage = "First name is required")]
    [StringLength(50, MinimumLength = 2)]
    public string FirstName { get; set; } = string.Empty;
    
    [Required]
    [StringLength(50)]
    public string LastName { get; set; } = string.Empty;
    
    [Required]
    [EmailAddress(ErrorMessage = "Email is invalid")]
    public string Email { get; set; } = string.Empty;
    
    [Phone]
    public string? PhoneNumber { get; set; }
    
    [Range(18, 120, ErrorMessage = "Age must be between 18 and 120")]
    public int Age { get; set; }
    
    [RegularExpression(@"^\d{5}(-\d{4})?$", ErrorMessage = "Invalid postal code")]
    public string PostalCode { get; set; } = string.Empty;
    
    [Compare("Email", ErrorMessage = "Emails do not match")]
    public string ConfirmEmail { get; set; } = string.Empty;
}
// Controller — check validation
[HttpPost]
public async Task<IActionResult> Checkout(CustomerInfo customerInfo)
{
    if (!ModelState.IsValid)
    {
        // Return view with errors
        return View(customerInfo);
    }
    
    // Business logic
    await _orderService.CreateOrderAsync(customerInfo);
    return RedirectToAction("Confirmation");
}
@* View with client-side validation *@
<form asp-action="Checkout" method="post">
    <div class="mb-3">
        <label asp-for="Email" class="form-label"></label>
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>
    
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>
    
    <button type="submit">Order</button>
</form>

@section Scripts {
    @* Client-side validation — jQuery Validate *@
    <partial name="_ValidationScriptsPartial" />
}

4.3 Anti-forgery Token

// Automatic with asp-action in Razor forms
// Server-side verification

[HttpPost]
[ValidateAntiForgeryToken]  // Verifies that the token is valid
public IActionResult Create(CreateProductModel model) { }

// Or globally for all POSTs
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

5. State Management

5.1 Sessions

// Program.cs — Session configuration
builder.Services.AddDistributedMemoryCache(); // Required for sessions
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

// In the pipeline
app.UseSession();
// Read/write to session
public IActionResult Index()
{
    // Write
    HttpContext.Session.SetString("userName", "Alice");
    HttpContext.Session.SetInt32("cartItemCount", 3);
    
    // Read
    var userName = HttpContext.Session.GetString("userName") ?? "Anonymous";
    var count = HttpContext.Session.GetInt32("cartItemCount") ?? 0;
    
    return View();
}

// Session-based cart repository
public class SessionCartRepository : ICartRepository
{
    private const string CartKey = "user_cart";
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public Cart GetCart()
    {
        var session = _httpContextAccessor.HttpContext!.Session;
        var json = session.GetString(CartKey);
        return json != null 
            ? JsonSerializer.Deserialize<Cart>(json)! 
            : new Cart();
    }
    
    public void SaveCart(Cart cart)
    {
        var session = _httpContextAccessor.HttpContext!.Session;
        session.SetString(CartKey, JsonSerializer.Serialize(cart));
    }
}
// SessionValueProvider — Model Binding from session
public class SessionValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        var session = context.ActionContext.HttpContext.Session;
        if (session != null)
        {
            context.ValueProviders.Add(new SessionValueProvider(session));
        }
        return Task.CompletedTask;
    }
}

// Program.cs
builder.Services.AddControllersWithViews(options =>
{
    options.ValueProviderFactories.Add(new SessionValueProviderFactory());
});

5.2 TempData

// TempData — Persists for one request (useful for redirects)
[HttpPost]
public IActionResult Create(CreateProductModel model)
{
    if (!ModelState.IsValid) return View(model);
    
    _productService.CreateProduct(model);
    
    // Store a success message (available after redirect)
    TempData["SuccessMessage"] = "Product created successfully!";
    
    return RedirectToAction("Index");
}

public IActionResult Index()
{
    // TempData available here (auto-consumed)
    var message = TempData["SuccessMessage"] as string;
    ViewBag.Message = message;
    return View();
}
// Keep TempData for the next request
[HttpGet]
public IActionResult Index()
{
    TempData.Keep("SuccessMessage"); // Do not consume
    return View();
}

// Or with [TempData] attribute on a property
[TempData]
public string? StatusMessage { get; set; }

5.3 Cookies

// Write a cookie
public IActionResult SetPreference(string theme)
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Strict,
        Expires = DateTimeOffset.Now.AddDays(30)
    };
    
    Response.Cookies.Append("userTheme", theme, cookieOptions);
    return Ok();
}

// Read a cookie
public IActionResult Index()
{
    var theme = Request.Cookies["userTheme"] ?? "light";
    ViewBag.Theme = theme;
    return View();
}

// Delete a cookie
public IActionResult ClearPreferences()
{
    Response.Cookies.Delete("userTheme");
    return RedirectToAction("Index");
}

5.4 Distributed Cache

// Redis for distributed systems
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "GloboTicket:";
});

// Or SQL Server
builder.Services.AddSqlServerCache(options =>
{
    options.ConnectionString = builder.Configuration.GetConnectionString("SqlServer");
    options.SchemaName = "dbo";
    options.TableName = "DistributedCache";
});

// Using IDistributedCache
public class ProductService
{
    private readonly IDistributedCache _cache;
    
    public ProductService(IDistributedCache cache) => _cache = cache;
    
    public async Task<List<Product>> GetFeaturedProductsAsync()
    {
        const string cacheKey = "featured_products";
        
        var cachedJson = await _cache.GetStringAsync(cacheKey);
        if (cachedJson != null)
            return JsonSerializer.Deserialize<List<Product>>(cachedJson)!;
        
        var products = await _dbContext.Products
            .Where(p => p.IsFeatured)
            .ToListAsync();
        
        await _cache.SetStringAsync(cacheKey, 
            JsonSerializer.Serialize(products),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
            });
        
        return products;
    }
}

6. Filters and Middleware

6.1 Middleware Pipeline

// Program.cs — IMPORTANT middleware order
app.UseExceptionHandler("/Error");     // 1. Global error handling
app.UseHsts();                         // 2. HSTS (production)
app.UseHttpsRedirection();             // 3. HTTPS redirect
app.UseStaticFiles();                  // 4. Static files (wwwroot)
app.UseRouting();                      // 5. Routing
app.UseAuthentication();               // 6. Auth
app.UseAuthorization();                // 7. Authorization
app.MapControllerRoute(...);           // 8. MVC endpoints

6.2 Custom Middleware

// Custom middleware via IMiddleware
public class RequestTimingMiddleware : IMiddleware
{
    private readonly ILogger<RequestTimingMiddleware> _logger;
    
    public RequestTimingMiddleware(ILogger<RequestTimingMiddleware> logger)
    {
        _logger = logger;
    }
    
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var stopwatch = Stopwatch.StartNew();
        
        await next(context);  // Pass to next middleware
        
        stopwatch.Stop();
        _logger.LogInformation(
            "Request {Method} {Path} completed in {Elapsed}ms with status {StatusCode}",
            context.Request.Method,
            context.Request.Path,
            stopwatch.ElapsedMilliseconds,
            context.Response.StatusCode);
    }
}

// Or via convention (InvokeAsync method)
public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    
    public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
    
    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers["X-Correlation-Id"]
            .FirstOrDefault() ?? Guid.NewGuid().ToString();
        
        context.Response.Headers["X-Correlation-Id"] = correlationId;
        
        using var scope = context.RequestServices.GetRequiredService<ILogger<CorrelationIdMiddleware>>()
            .BeginScope(new Dictionary<string, object> { { "CorrelationId", correlationId } });
        
        await _next(context);
    }
}
// Program.cs — Middleware registration
builder.Services.AddTransient<RequestTimingMiddleware>();

app.UseMiddleware<RequestTimingMiddleware>();
app.UseMiddleware<CorrelationIdMiddleware>();

6.3 Action Filters

Filter pipeline (in order):
─────────────────────────────────────
  1. Authorization Filters     → First, checks permissions
  2. Resource Filters          → Before binding (e.g.: cache)
  3. Action Filters            → Around action execution
  4. Exception Filters         → Exception handling
  5. Result Filters            → Around result rendering
// Custom action filter
public class TimerActionFilter : IAsyncActionFilter
{
    private readonly ILogger<TimerActionFilter> _logger;
    
    public TimerActionFilter(ILogger<TimerActionFilter> logger)
    {
        _logger = logger;
    }
    
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, 
        ActionExecutionDelegate next)
    {
        // BEFORE the action
        var stopwatch = Stopwatch.StartNew();
        var controllerName = context.Controller.GetType().Name;
        var actionName = context.ActionDescriptor.DisplayName;
        
        // Execute action
        var result = await next();
        
        // AFTER the action
        stopwatch.Stop();
        
        if (result.Exception != null)
        {
            _logger.LogError("Action {Action} failed after {Elapsed}ms", 
                actionName, stopwatch.ElapsedMilliseconds);
        }
        else
        {
            _logger.LogInformation("Action {Action} completed in {Elapsed}ms", 
                actionName, stopwatch.ElapsedMilliseconds);
        }
    }
}

// Register as ServiceFilter
builder.Services.AddScoped<TimerActionFilter>();

// Apply to a controller
[ServiceFilter(typeof(TimerActionFilter))]
public class ProductController : Controller { }

// Or globally
builder.Services.AddControllersWithViews(options =>
{
    options.Filters.AddService<TimerActionFilter>();
});
// Custom authorization filter
public class RequireAdminFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.IsInRole("Admin"))
        {
            context.Result = new ForbidResult();
        }
    }
}

7. Advanced Features

7.1 Areas

Folder structure with Areas:
─────────────────────────────────────
  Areas/
  └── Admin/
      ├── Controllers/
      │   └── ProductController.cs
      └── Views/
          ├── Product/
          │   ├── Index.cshtml
          │   └── Create.cshtml
          └── Shared/
              └── _Layout.cshtml   (area-specific layout)
// Controllers/Admin/ProductController.cs
[Area("Admin")]
[Authorize(Roles = "Admin")]
public class ProductController : Controller
{
    public IActionResult Index() => View();
    
    [HttpGet]
    public IActionResult Create() => View();
    
    [HttpPost]
    public async Task<IActionResult> Create(CreateProductModel model) { }
}
// Program.cs — Route for areas
app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
@* Link to an action in an area *@
<a asp-area="Admin" asp-controller="Product" asp-action="Index">
    Product Administration
</a>

7.2 Output Caching

// Program.cs
builder.Services.AddOutputCache();

app.UseOutputCache();

// On a controller or action
[OutputCache(Duration = 60)]  // Cache for 60 seconds
public async Task<IActionResult> Index()
{
    var products = await _productService.GetAllProductsAsync();
    return View(products);
}

// Cache with parameter variations
[OutputCache(Duration = 30, VaryByQueryKeys = new[] { "category", "page" })]
public async Task<IActionResult> List(string? category, int page = 1)
{
    var products = await _productService.GetByCategoryAsync(category, page);
    return View(products);
}
@* Cache Tag Helper in Razor views *@
<cache expires-after="@TimeSpan.FromMinutes(10)">
    @* Content cached for 10 minutes *@
    @foreach (var product in Model.FeaturedProducts)
    {
        <partial name="_ProductCard" model="product" />
    }
</cache>

7.3 Testing with SQLite

// Integration tests with in-memory SQLite
public class CartControllerTests : IDisposable
{
    private readonly GlobomanticsContext _context;
    private readonly CartController _controller;
    
    public CartControllerTests()
    {
        var options = new DbContextOptionsBuilder<GlobomanticsContext>()
            .UseSqlite("Data Source=:memory:")
            .Options;
        
        _context = new GlobomanticsContext(options);
        _context.Database.EnsureCreated();
        
        // Seed test data
        SeedTestData();
        
        var cartService = new CartService(_context);
        _controller = new CartController(cartService, /* other deps */);
    }
    
    private void SeedTestData()
    {
        _context.Products.AddRange(
            new Product { Id = Guid.NewGuid(), Name = "Concert", Price = 50 },
            new Product { Id = Guid.NewGuid(), Name = "Festival", Price = 150 }
        );
        _context.SaveChanges();
    }
    
    [Fact]
    public async Task AddToCart_WithValidProduct_ReturnsRedirect()
    {
        var product = _context.Products.First();
        var model = new AddToCartModel { ProductId = product.Id, Quantity = 2 };
        
        var result = await _controller.AddToCart(model);
        
        result.Should().BeOfType<RedirectToActionResult>();
    }
    
    public void Dispose()
    {
        _context.Database.EnsureDeleted();
        _context.Dispose();
    }
}

7.4 Distributed Scaling

// Recommendations for distributed systems

// 1. Avoid in-memory state → use distributed cache (Redis)
// 2. Distributed sessions → store in Redis or SQL Server
// 3. Async/await architecture for scalability
// 4. Message-based architecture for long-running operations

// Example: message-based architecture
public class OrderController : Controller
{
    private readonly IMessageBus _messageBus;
    
    [HttpPost]
    public async Task<IActionResult> PlaceOrder(PlaceOrderModel model)
    {
        // Publish a message instead of processing synchronously
        await _messageBus.PublishAsync(new OrderPlacedEvent
        {
            OrderId = Guid.NewGuid(),
            CustomerId = model.CustomerId,
            Items = model.Items
        });
        
        // Respond immediately (without waiting for processing)
        return Accepted(new { Message = "Order received, processing..." });
    }
}

Quick Reference — ASP.NET Core MVC Tag Helpers

Tag HelperAttributesUsage
<a>asp-controller, asp-action, asp-route-*Typed links
<form>asp-controller, asp-action, methodForms with CSRF
<input>asp-forModel-bound field
<label>asp-forLabel with Display attribute text
<span>asp-validation-forValidation message
<div>asp-validation-summaryError summary
<img>asp-append-versionCache-busting
<script>asp-src-include, asp-append-versionVersioned scripts
<cache>expires-after, vary-by-*HTML section caching
<partial>name, modelPartial view
<vc:*>custom attributesView Components

8. MVC Architecture — Detailed Request Flow

8.1 HTTP Request Lifecycle

sequenceDiagram
    participant Nav as Browser
    participant MW as Middleware Pipeline
    participant Router as Router
    participant AF as Action Filters
    participant Ctrl as Controller
    participant View as View Engine
    participant DB as Database

    Nav->>MW: HTTP GET /products?category=events
    MW->>MW: ExceptionHandler
    MW->>MW: HTTPS Redirect
    MW->>MW: Static Files (not found)
    MW->>Router: UseRouting — route matching
    Router->>Router: Resolved: ProductController.Index(category="events")
    Router->>AF: Authorization Filters
    AF->>AF: [Authorize] — user authenticated?
    AF->>AF: Resource Filters — cache?
    AF->>Ctrl: Action Filters (Before)
    Ctrl->>DB: _productService.GetByCategoryAsync("events")
    DB-->>Ctrl: List<Product>
    Ctrl->>View: return View(products)
    View->>View: Razor — compile .cshtml to HTML
    View-->>AF: Action Filters (After)
    AF-->>MW: Result Filters
    MW-->>Nav: HTTP 200 + HTML
MyApp/
├── Areas/
│   └── Admin/
│       ├── Controllers/
│       ├── Models/
│       └── Views/
├── Controllers/
│   ├── HomeController.cs
│   ├── ProductController.cs
│   └── CartController.cs
├── Filters/
│   ├── TimerActionFilter.cs
│   └── RequireAdminFilter.cs
├── Middleware/
│   ├── RequestTimingMiddleware.cs
│   └── CorrelationIdMiddleware.cs
├── Models/
│   ├── Product/
│   │   ├── ProductListViewModel.cs
│   │   └── CreateProductModel.cs
│   ├── Cart/
│   │   └── AddToCartModel.cs
│   └── Order/
│       └── PlaceOrderModel.cs
├── ViewComponents/
│   ├── CartSummaryViewComponent.cs
│   └── FeaturedProductsViewComponent.cs
├── Constraints/
│   └── SlugConstraint.cs
├── Transformers/
│   └── SlugParameterTransformer.cs
├── Views/
│   ├── Home/
│   │   └── Index.cshtml
│   ├── Product/
│   │   ├── Index.cshtml
│   │   ├── Details.cshtml
│   │   └── Create.cshtml
│   └── Shared/
│       ├── _Layout.cshtml
│       ├── _ValidationScriptsPartial.cshtml
│       ├── Components/
│       │   ├── CartSummary/
│       │   │   └── Default.cshtml
│       │   └── FeaturedProducts/
│       │       └── Default.cshtml
│       └── _ProductCard.cshtml
├── wwwroot/
│   ├── css/
│   ├── js/
│   └── images/
└── Program.cs

8.3 Comparison of Approaches for Passing Data to Views

flowchart TD
    A[Data to display in the view] --> B{What type of data?}
    B --> C[Main view data]
    B --> D[Secondary data\nfor layout]
    B --> E[Temporary messages\npost-redirect]
    B --> F[Real-time\ncalculated data]
    
    C --> C1["@model MyViewModel\nStrongly typed ✅\nCompilation safety ✅"]
    D --> D1["ViewData['Title']\nViewBag.Subtitle\nQuick but untyped ⚠️"]
    E --> E1["TempData['Success']\nPersists 1 request\nIdeal for RedirectToAction ✅"]
    F --> F1["@inject IService service\nDI in views\nUseful for navigation ✅"]

9. Security in ASP.NET Core MVC

9.1 CSRF Protection (Cross-Site Request Forgery)

// Global CSRF validation activation
builder.Services.AddControllersWithViews(options =>
{
    // Automatic validation for all POST/PUT/PATCH/DELETE requests
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

// Or manually on each POST action
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(CreateProductModel model)
{
    if (!ModelState.IsValid) return View(model);
    // processing...
    return RedirectToAction("Index");
}

// Exclude certain actions from CSRF validation (e.g.: public APIs)
[HttpPost]
[IgnoreAntiforgeryToken]
public IActionResult WebhookReceiver([FromBody] WebhookPayload payload)
{
    // Process webhook without CSRF validation
    return Ok();
}
@* Razor form automatically generates the CSRF token *@
<form asp-action="Create" method="post">
    @* Token injected automatically via the form tag helper *@
    <input asp-for="Name" class="form-control" />
    <button type="submit">Create</button>
</form>

@* For AJAX requests, include the token in headers *@
@section Scripts {
    <script>
        const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
        fetch('/product/create', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'RequestVerificationToken': token
            },
            body: JSON.stringify(formData)
        });
    </script>
}

9.2 XSS Protection (Cross-Site Scripting)

// ASP.NET Core automatically encodes Razor output
// Example: if Model.Name = "<script>alert('XSS')</script>"
// Razor generates: &lt;script&gt;alert('XSS')&lt;/script&gt;

// DANGEROUS — Disables HTML encoding
@Html.Raw(Model.HtmlContent)

// SAFE — Automatic encoding
@Model.HtmlContent

// For trusted HTML content (e.g.: WYSIWYG editor)
// Use a sanitization library like HtmlSanitizer
public class SafeHtmlService
{
    private readonly HtmlSanitizer _sanitizer;

    public SafeHtmlService()
    {
        _sanitizer = new HtmlSanitizer();
        _sanitizer.AllowedTags.Clear();
        _sanitizer.AllowedTags.Add("p");
        _sanitizer.AllowedTags.Add("b");
        _sanitizer.AllowedTags.Add("i");
        _sanitizer.AllowedTags.Add("a");
    }

    public string Sanitize(string html) => _sanitizer.Sanitize(html);
}

9.3 Preventing Over-Posting / Mass Assignment

// DANGEROUS — Exposing the domain entity directly
[HttpPost]
public IActionResult Update(User user)  // user.IsAdmin can be modified!
{
    _userRepository.Update(user);
    return Ok();
}

// SAFE — Use a dedicated ViewModel with [Bind]
public class UpdateUserModel
{
    [Required]
    public string FirstName { get; set; } = string.Empty;
    
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;
    
    // No IsAdmin — cannot be modified via the form!
}

[HttpPost]
public IActionResult Update(UpdateUserModel model)
{
    var user = _userRepository.GetById(HttpContext.User.GetUserId());
    user.FirstName = model.FirstName;
    user.Email = model.Email;
    // IsAdmin remains unchanged
    _userRepository.Update(user);
    return RedirectToAction("Profile");
}

// Or with [Bind] directly (less recommended)
[HttpPost]
public IActionResult Update([Bind("FirstName,Email")] User user)
{
    // Only FirstName and Email are bound from the form
}

9.4 Security Headers

// Middleware to add security headers
public class SecurityHeadersMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // Prevent MIME type sniffing
        context.Response.Headers["X-Content-Type-Options"] = "nosniff";
        
        // Prevent clickjacking
        context.Response.Headers["X-Frame-Options"] = "SAMEORIGIN";
        
        // XSS Protection (legacy browsers)
        context.Response.Headers["X-XSS-Protection"] = "1; mode=block";
        
        // Content Security Policy
        context.Response.Headers["Content-Security-Policy"] = 
            "default-src 'self'; " +
            "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " +
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
            "img-src 'self' data: https:;";
        
        // Referrer Policy
        context.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
        
        await next(context);
    }
}

10. Performance and Optimization

10.1 Multi-level Caching

graph TD
    A[User request] --> B{Output Cache\nASP.NET Core}
    B -->|HIT| Z[Cached response — very fast]
    B -->|MISS| C{IMemoryCache}
    C -->|HIT| D[In-memory data — fast]
    C -->|MISS| E{IDistributedCache\nRedis}
    E -->|HIT| F[Distributed cache — medium]
    E -->|MISS| G[Database — slow]
    G --> H[Cache and return]
    F --> H
    D --> H
// IMemoryCache — in-process memory cache
public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly IGlobomanticsContext _context;
    
    public async Task<List<Product>> GetFeaturedAsync()
    {
        return await _cache.GetOrCreateAsync("featured_products", async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            entry.SlidingExpiration = TimeSpan.FromMinutes(2);
            
            return await _context.Products
                .Where(p => p.IsFeatured)
                .AsNoTracking()  // Better perf for reads
                .ToListAsync();
        }) ?? new List<Product>();
    }
}

// OutputCache — full HTTP response caching
// Program.cs
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("ProductList", policy =>
        policy.Expire(TimeSpan.FromMinutes(10))
              .SetVaryByQuery("category", "page"));
});

[OutputCache(PolicyName = "ProductList")]
public async Task<IActionResult> Index(string? category, int page = 1)
{
    // Result cached by category and page
    return View(await _productService.GetPagedAsync(category, page));
}

10.2 Optimized Database Queries

// Efficient pagination with EF Core
public async Task<PagedResult<Product>> GetPagedProductsAsync(
    string? category, int page, int pageSize = 20)
{
    var query = _context.Products
        .AsNoTracking()
        .AsQueryable();
    
    if (!string.IsNullOrEmpty(category))
        query = query.Where(p => p.Category.Name == category);
    
    var totalCount = await query.CountAsync();
    
    var items = await query
        .OrderBy(p => p.Name)
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .Select(p => new ProductListViewModel
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price,
            ImageUrl = p.ImageUrl
        })
        .ToListAsync();
    
    return new PagedResult<Product>(items, totalCount, page, pageSize);
}

// Optimized loading with Include
public async Task<Order?> GetOrderWithDetailsAsync(Guid orderId)
{
    return await _context.Orders
        .Include(o => o.LineItems)
            .ThenInclude(li => li.Product)
        .AsSplitQuery()  // Avoids N+1 problem with multiple includes
        .FirstOrDefaultAsync(o => o.Id == orderId);
}

10.3 Async/Await in Controllers

// ALWAYS use async/await for I/O
public class ProductController : Controller
{
    // Good example — all I/O operations are async
    public async Task<IActionResult> Index()
    {
        // Parallel operations when independent
        var productsTask = _productService.GetFeaturedAsync();
        var categoriesTask = _categoryService.GetAllAsync();
        
        await Task.WhenAll(productsTask, categoriesTask);
        
        var viewModel = new HomeViewModel
        {
            FeaturedProducts = await productsTask,
            Categories = await categoriesTask
        };
        
        return View(viewModel);
    }
    
    // BAD — blocks the thread
    // public IActionResult Index()
    // {
    //     var products = _productService.GetFeaturedAsync().Result;  // ❌ Possible DEADLOCK
    //     return View(products);
    // }
}

11. Best Practices and Anti-patterns

11.1 Best Practices

CategoryBest PracticeExample
ControllersKeep controllers thinDelegate logic to services
ModelsDedicated ViewModels per viewDon’t expose domain entities
ValidationALWAYS validate server-sideClient-side = UX only
CSRFActivate globallyAutoValidateAntiforgeryTokenAttribute
RoutesUse Attribute RoutingMore explicit than convention-based
StatePrefer cookies over sessionsFor distributed systems
CachingCache-control headersResponseCache or OutputCache
LoggingLog errors and key eventsILogger<T> via DI
AsyncAsync/await for all I/OAvoid .Result and .Wait()
SecurityPrinciple of least privilege[Authorize] on each action

11.2 Common Anti-patterns to Avoid

// ❌ ANTI-PATTERN 1: Overloaded controller (Fat Controller)
public class ProductController : Controller
{
    [HttpPost]
    public async Task<IActionResult> Create(CreateProductModel model)
    {
        // Business logic in the controller — BAD
        var existing = await _context.Products
            .AnyAsync(p => p.Name == model.Name);
        if (existing) ModelState.AddModelError("", "Product already exists");
        
        var imageUrl = await UploadImageAsync(model.Image);
        var slug = model.Name.ToLower().Replace(" ", "-");
        
        var product = new Product
        {
            Name = model.Name,
            Price = model.Price,
            ImageUrl = imageUrl,
            Slug = slug
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        
        await _emailService.SendProductCreatedEmailAsync(product);
        
        return RedirectToAction("Index");
    }
}

// ✅ GOOD: Controller delegates to service
public class ProductController : Controller
{
    private readonly IProductService _productService;
    
    [HttpPost]
    public async Task<IActionResult> Create(CreateProductModel model)
    {
        if (!ModelState.IsValid) return View(model);
        
        await _productService.CreateAsync(model);  // All logic is in the service
        
        TempData["Success"] = "Product created successfully";
        return RedirectToAction("Index");
    }
}

// ❌ ANTI-PATTERN 2: Inject DbContext directly into views
@inject GlobomanticsContext Context
@foreach (var product in Context.Products) { }

// ✅ GOOD: Use a service via injection
@inject IProductService ProductService

// ❌ ANTI-PATTERN 3: Using ViewBag for everything (untyped)
ViewBag.Products = await _context.Products.ToListAsync();
ViewBag.Categories = await _context.Categories.ToListAsync();
ViewBag.PageTitle = "Catalog";

// ✅ GOOD: Strongly typed ViewModel
var vm = new ProductIndexViewModel
{
    Products = await _productService.GetFeaturedAsync(),
    Categories = await _categoryService.GetAllAsync(),
    PageTitle = "Catalog"
};
return View(vm);

// ❌ ANTI-PATTERN 4: No pagination
public async Task<IActionResult> Index()
{
    var allProducts = await _context.Products.ToListAsync(); // Entire table in memory!
    return View(allProducts);
}

// ✅ GOOD: Pagination and projection
public async Task<IActionResult> Index(int page = 1)
{
    var result = await _productService.GetPagedAsync(page, pageSize: 20);
    return View(result);
}

11.3 Common Pitfalls

// PITFALL 1: Middleware order
// ❌ BAD — UseAuthorization before UseAuthentication!
app.UseAuthorization();
app.UseAuthentication();

// ✅ CORRECT — Authentication first, then Authorization
app.UseAuthentication();
app.UseAuthorization();

// PITFALL 2: TempData and non-serializable JSON
// ❌ BAD — TempData doesn't directly support complex objects
TempData["product"] = product;  // Doesn't work!

// ✅ CORRECT — Serialize to JSON
TempData["product"] = JsonSerializer.Serialize(product);
// Read:
var product = JsonSerializer.Deserialize<Product>((string)TempData["product"]!);

// PITFALL 3: ModelState after redirect
[HttpPost]
public IActionResult Create(Model model)
{
    if (!ModelState.IsValid)
    {
        // ❌ BAD — ModelState lost after redirect
        return RedirectToAction("Create");
        
        // ✅ CORRECT — Return view directly
        return View(model);
    }
}

// PITFALL 4: Ambiguous route
// Both following actions match GET /product/
[Route("product")]
public IActionResult Index() => View();

[Route("product")]
public IActionResult List() => View(); // ❌ AmbiguousMatchException!

// PITFALL 5: ViewComponent vs Partial
// Use ViewComponent when:
// - Business logic / DB access needed
// - DI needed
// - Reusable component with its own data source
//
// Use Partial when:
// - Simple HTML reuse
// - Parent provides all data
// - No business logic needed

12. Practical Exercises

Exercise 1: Route with Transformed Parameter

Goal: Create a route /blog/my-article-title that calls BlogController.Show(slug).

// Step 1: SlugParameterTransformer
public class SlugParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value) =>
        value?.ToString()
            ?.ToLower()
            .Replace(" ", "-")
            .Replace("'", "")
            .TrimEnd('-');
}

// Step 2: Registration
builder.Services.AddControllersWithViews(options =>
{
    options.Conventions.Add(
        new RouteTokenTransformerConvention(new SlugParameterTransformer()));
});

// Step 3: Controller
[Route("blog")]
public class BlogController : Controller
{
    [Route("{slug}")]
    public async Task<IActionResult> Show(string slug)
    {
        var article = await _blogService.GetBySlugAsync(slug);
        if (article == null) return NotFound();
        return View(article);
    }
}

Exercise 2: View Component with Paging

Goal: Create a reusable PaginationViewComponent.

// ViewComponents/PaginationViewComponent.cs
public class PaginationViewComponent : ViewComponent
{
    public IViewComponentResult Invoke(int currentPage, int totalPages, string action, string controller)
    {
        var vm = new PaginationViewModel
        {
            CurrentPage = currentPage,
            TotalPages = totalPages,
            Action = action,
            Controller = controller,
            HasPrevious = currentPage > 1,
            HasNext = currentPage < totalPages
        };
        return View(vm);
    }
}
@* Views/Shared/Components/Pagination/Default.cshtml *@
@model PaginationViewModel

<nav aria-label="Pagination">
    <ul class="pagination justify-content-center">
        @if (Model.HasPrevious)
        {
            <li class="page-item">
                <a class="page-link" asp-controller="@Model.Controller"
                   asp-action="@Model.Action"
                   asp-route-page="@(Model.CurrentPage - 1)">Previous</a>
            </li>
        }
        
        @for (int i = 1; i <= Model.TotalPages; i++)
        {
            <li class="page-item @(i == Model.CurrentPage ? "active" : "")">
                <a class="page-link" asp-controller="@Model.Controller"
                   asp-action="@Model.Action"
                   asp-route-page="@i">@i</a>
            </li>
        }
        
        @if (Model.HasNext)
        {
            <li class="page-item">
                <a class="page-link" asp-controller="@Model.Controller"
                   asp-action="@Model.Action"
                   asp-route-page="@(Model.CurrentPage + 1)">Next</a>
            </li>
        }
    </ul>
</nav>

Exercise 3: Custom Cache Filter

// Role-based cache filter — different cache for admins and regular users
public class RoleBasedCacheFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        var user = context.HttpContext.User;
        var isAdmin = user.IsInRole("Admin");
        
        var cacheKey = $"page__{context.ActionDescriptor.DisplayName}_{isAdmin}";
        
        var cache = context.HttpContext.RequestServices
            .GetRequiredService<IMemoryCache>();
        
        if (cache.TryGetValue(cacheKey, out IActionResult? cachedResult))
        {
            context.Result = cachedResult!;
            return;
        }
        
        var result = await next();
        
        if (result.Exception == null)
        {
            cache.Set(cacheKey, result.Result, TimeSpan.FromMinutes(5));
        }
    }
}

13. Complete Comparison Tables

13.1 State Management Methods

MechanismLifetimeStorageSecurityDistributedUse Case
ViewData/ViewBagCurrent requestServer memoryN/AN/AController→view data
TempData (cookie)Next 1 requestEncrypted cookie✅ HTTPSPost-redirect messages
TempData (session)Next 1 requestServer❌ Single serverMessages + data
SessionSession durationServer/Distributed✅ Secure cookie⚠️ Config requiredCart, preferences
CookieConfigurableClient✅ EncryptedPersistent preferences
Distributed cacheConfigurable (TTL)Redis/SQLShared data

13.2 ASP.NET Core MVC Filter Types

FilterInterfaceOrderTypical Usage
AuthorizationIAuthorizationFilter1stJWT, roles, policies
ResourceIResourceFilter2ndCache, short-circuit
ActionIActionFilter3rdLogging, validation, timer
ExceptionIExceptionFilterGlobalCentralized error handling
ResultIResultFilterLastResult transformation

13.3 When to Use What?

NeedRecommended Solution
Logic in multiple actionsAction filter
Logic in all requestsMiddleware
Simple reusable HTML componentPartial View
Component with its own logic/dataView Component
Navigation data (cart, menu)View Component + IMemoryCache
Granular securityAuthorization Policy + [Authorize]
Sharing routes in a sub-domainAreas
Temporary data between requestsTempData
Persistent client-side dataSecure cookie
Server-side session dataDistributed session

14. Technical Terms Glossary

TermDefinition
ActionPublic method on a Controller returning an IActionResult
ActionResultGeneric return type for an MVC action (View, Redirect, JSON…)
Antiforgery TokenSecurity token injected in forms to prevent CSRF
AreaApplication sub-section with its own controllers/views
Attribute RoutingRoute definition via [Route] attributes on actions
ControllerClass that handles HTTP requests and returns responses
CSRFCross-Site Request Forgery — attack exploiting user identity
Data AnnotationsValidation attributes (Required, StringLength, Email…)
Filter PipelineExecution sequence of filters before/after an action
IActionResultCommon interface for all action return types
LayoutHTML template shared among multiple views
MiddlewareComponent in the HTTP request processing pipeline
Model BindingAutomatic mapping of HTTP data to action parameters
ModelStateState of model validation during processing
MVCModel-View-Controller — architectural design pattern
Output CachingCaching full HTTP responses
Over-PostingAttack exploiting model binding to modify unauthorized fields
Parameter TransformerTransformer for modifying parameters in URL generation
Partial ViewReusable partial view included in other views
RazorTemplate engine combining C# and HTML
Route ConstraintRoute parameter validation (type, regex pattern…)
SectionExtensible zone in a Razor layout
ServiceFilterAttribute allowing DI in action filters
Tag HelperC# component that generates HTML from attributes on HTML elements
TempDataData dictionary persisting for the next request only
View ComponentReusable component with C# logic and its own view
ViewBagDynamic dictionary for passing data from a controller to a view
ViewDataStrongly typed dictionary for passing data to a view
ViewModelModel created specifically for a view (different from domain entity)
_ViewImportsFile declaring namespaces and tag helpers available in all views
_ViewStartFile defining common settings for all views (e.g.: layout)
XSSCross-Site Scripting — injecting malicious scripts via user input

Additional Resources


Search Terms

asp.net · core · mvc · deep · dive · web · apis · c# · .net · development · data · middleware · request · views · anti-patterns · architecture · cache · caching · comparison · cross-site · custom · distributed · filter · filters

Interested in this course?

Contact us to book it or get a custom training plan for your team.