Demo Application: GloboTicket / Globomantics (eCommerce — event tickets)
ASP.NET Core with .NET 6/8, Visual Studio
Table of Contents
- Getting Started and MVC Fundamentals
- Routing
- Views and Razor
- Models and Data
- State Management
- Filters and Middleware
- 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();
// 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 Helper | Attributes | Usage |
|---|
<a> | asp-controller, asp-action, asp-route-* | Typed links |
<form> | asp-controller, asp-action, method | Forms with CSRF |
<input> | asp-for | Model-bound field |
<label> | asp-for | Label with Display attribute text |
<span> | asp-validation-for | Validation message |
<div> | asp-validation-summary | Error summary |
<img> | asp-append-version | Cache-busting |
<script> | asp-src-include, asp-append-version | Versioned scripts |
<cache> | expires-after, vary-by-* | HTML section caching |
<partial> | name, model | Partial view |
<vc:*> | custom attributes | View 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
8.2 Recommended MVC Project Organization
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: <script>alert('XSS')</script>
// 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
}
// 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.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
| Category | Best Practice | Example |
|---|
| Controllers | Keep controllers thin | Delegate logic to services |
| Models | Dedicated ViewModels per view | Don’t expose domain entities |
| Validation | ALWAYS validate server-side | Client-side = UX only |
| CSRF | Activate globally | AutoValidateAntiforgeryTokenAttribute |
| Routes | Use Attribute Routing | More explicit than convention-based |
| State | Prefer cookies over sessions | For distributed systems |
| Caching | Cache-control headers | ResponseCache or OutputCache |
| Logging | Log errors and key events | ILogger<T> via DI |
| Async | Async/await for all I/O | Avoid .Result and .Wait() |
| Security | Principle 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
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
| Mechanism | Lifetime | Storage | Security | Distributed | Use Case |
|---|
| ViewData/ViewBag | Current request | Server memory | N/A | N/A | Controller→view data |
| TempData (cookie) | Next 1 request | Encrypted cookie | ✅ HTTPS | ✅ | Post-redirect messages |
| TempData (session) | Next 1 request | Server | ✅ | ❌ Single server | Messages + data |
| Session | Session duration | Server/Distributed | ✅ Secure cookie | ⚠️ Config required | Cart, preferences |
| Cookie | Configurable | Client | ✅ Encrypted | ✅ | Persistent preferences |
| Distributed cache | Configurable (TTL) | Redis/SQL | ✅ | ✅ | Shared data |
13.2 ASP.NET Core MVC Filter Types
| Filter | Interface | Order | Typical Usage |
|---|
| Authorization | IAuthorizationFilter | 1st | JWT, roles, policies |
| Resource | IResourceFilter | 2nd | Cache, short-circuit |
| Action | IActionFilter | 3rd | Logging, validation, timer |
| Exception | IExceptionFilter | Global | Centralized error handling |
| Result | IResultFilter | Last | Result transformation |
13.3 When to Use What?
| Need | Recommended Solution |
|---|
| Logic in multiple actions | Action filter |
| Logic in all requests | Middleware |
| Simple reusable HTML component | Partial View |
| Component with its own logic/data | View Component |
| Navigation data (cart, menu) | View Component + IMemoryCache |
| Granular security | Authorization Policy + [Authorize] |
| Sharing routes in a sub-domain | Areas |
| Temporary data between requests | TempData |
| Persistent client-side data | Secure cookie |
| Server-side session data | Distributed session |
14. Technical Terms Glossary
| Term | Definition |
|---|
| Action | Public method on a Controller returning an IActionResult |
| ActionResult | Generic return type for an MVC action (View, Redirect, JSON…) |
| Antiforgery Token | Security token injected in forms to prevent CSRF |
| Area | Application sub-section with its own controllers/views |
| Attribute Routing | Route definition via [Route] attributes on actions |
| Controller | Class that handles HTTP requests and returns responses |
| CSRF | Cross-Site Request Forgery — attack exploiting user identity |
| Data Annotations | Validation attributes (Required, StringLength, Email…) |
| Filter Pipeline | Execution sequence of filters before/after an action |
| IActionResult | Common interface for all action return types |
| Layout | HTML template shared among multiple views |
| Middleware | Component in the HTTP request processing pipeline |
| Model Binding | Automatic mapping of HTTP data to action parameters |
| ModelState | State of model validation during processing |
| MVC | Model-View-Controller — architectural design pattern |
| Output Caching | Caching full HTTP responses |
| Over-Posting | Attack exploiting model binding to modify unauthorized fields |
| Parameter Transformer | Transformer for modifying parameters in URL generation |
| Partial View | Reusable partial view included in other views |
| Razor | Template engine combining C# and HTML |
| Route Constraint | Route parameter validation (type, regex pattern…) |
| Section | Extensible zone in a Razor layout |
| ServiceFilter | Attribute allowing DI in action filters |
| Tag Helper | C# component that generates HTML from attributes on HTML elements |
| TempData | Data dictionary persisting for the next request only |
| View Component | Reusable component with C# logic and its own view |
| ViewBag | Dynamic dictionary for passing data from a controller to a view |
| ViewData | Strongly typed dictionary for passing data to a view |
| ViewModel | Model created specifically for a view (different from domain entity) |
| _ViewImports | File declaring namespaces and tag helpers available in all views |
| _ViewStart | File defining common settings for all views (e.g.: layout) |
| XSS | Cross-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