DEV SCRIPTS

Mvc Code FAQs

ASP.NET Core MVC FAQ

ASP.NET Core MVC

68 Questions & Answers with Code Examples

What is MVC

MVC assigns each concern to exactly one layer: the Model owns data and business rules, the View owns HTML rendering, and the Controller owns request handling and coordination. A change to the database schema should only touch Models; a redesign of the UI should only touch Views; neither change should ripple into the other layer.

csharp
// Model — owns data shape and rules
public class Product {
    public int     Id    { get; set; }
    [Required] public string Name  { get; set; }
    [Range(0.01, 99999)] public decimal Price { get; set; }
}

// Controller — handles request, picks view
public class ProductsController : Controller {
    public IActionResult Details(int id) {
        var p = _repo.Find(id);          // talks to Model layer
        if (p is null) return NotFound();
        return View(p);                  // delegates rendering to View
    }
}

// View (Views/Products/Details.cshtml) — renders HTML only
// @model Product
// <h1>@Model.Name</h1>  <p>@Model.Price.ToString("C")</p>

WebForms hid HTTP behind event-driven abstractions that were hard to test and slow to render. Classic MVC 5 was tightly coupled to System.Web and Windows/IIS. ASP.NET Core was rewritten from scratch to be cross-platform, lightweight, modular, and host-agnostic — it runs on Linux containers and doesn’t require IIS.

csharp
// Classic MVC 5 — tied to IIS pipeline (Global.asax, System.Web.HttpContext)
// protected void Application_Start() { AreaRegistration.RegisterAllAreas(); ... }

// ASP.NET Core — host-agnostic, runs anywhere
var builder = WebApplication.CreateBuilder(args);  // Kestrel, IIS, Docker, etc.
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.Run();  // works on Windows, Linux, macOS

The Kestrel web server receives the TCP connection → middleware pipeline runs (auth, static files, routing) → the router matches the URL to a controller action → model binding populates parameters → action filters run → the action method executes → a ViewResult or JSON result is rendered → the response is written back to the socket.

csharp
// Full lifecycle for GET /products/5:
// 1. Kestrel receives request
// 2. UseStaticFiles → no match, passes through
// 3. UseRouting → matches ProductsController.Details, id=5
// 4. UseAuthentication / UseAuthorization → user OK
// 5. [ActionFilter].OnActionExecuting() runs
// 6. ProductsController.Details(5) executes
// 7. return View(product) → Razor compiles Details.cshtml → HTML string
// 8. Response written: HTTP 200, Content-Type: text/html
// 9. [ActionFilter].OnActionExecuted() runs

In .NET 6+ the Startup class was merged into Program.cs. The builder.Services collection is populated before builder.Build(); after that call the middleware pipeline is configured on app. This two-phase pattern keeps service wiring separate from pipeline construction.

csharp
// ── Phase 1: register services (DI container) ───────────
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IOrderService, OrderService>();

// ── Phase 2: configure middleware pipeline ───────────────
var app = builder.Build();   // DI container is now locked

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.Run();

Controllers are hard to unit-test without a full HTTP context. Business logic in a service class can be tested with a plain new ServiceClass(mockRepo) call. Thin controllers delegate to services, return results, and nothing more — the logic lives where it is testable and reusable from multiple entry points (MVC, API, background jobs).

csharp
// Fat controller — logic trapped inside HTTP context, hard to test
public IActionResult PlaceOrder(OrderDto dto) {
    var product = _db.Products.Find(dto.ProductId);
    if (product.Stock < dto.Qty) { /* business rule inline */ }
    product.Stock -= dto.Qty;
    _db.SaveChanges();
    return RedirectToAction("Confirmation");
}

// Thin controller — delegates to service
public async Task<IActionResult> PlaceOrder(OrderDto dto) {
    var orderId = await _orderService.PlaceAsync(dto);  // testable independently
    return RedirectToAction("Confirmation", new { id = orderId });
}

ASP.NET Core MVC discovers controllers by scanning assemblies for classes ending in Controller. Views are found at Views/{Controller}/{Action}.cshtml automatically. Adding a new controller and its views requires zero registration — the structure itself is the configuration.

csharp
// Project layout — no registration needed for any of these:
// Controllers/
//   HomeController.cs       → handles /Home/*
//   ProductsController.cs   → handles /Products/*
//   OrdersController.cs     → handles /Orders/*
// Views/
//   Home/Index.cshtml       → rendered by HomeController.Index()
//   Products/Details.cshtml → rendered by ProductsController.Details()
//   Shared/_Layout.cshtml   → shared by all views

// The only wiring required:
builder.Services.AddControllersWithViews();  // scan + register all controllers
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");

The Service layer holds business operations that orchestrate multiple repositories, enforce cross-cutting rules, and can be called from controllers, background jobs, or CLI tools. Without it, business logic is forced into either the controller (untestable) or the repository (wrong abstraction layer).

csharp
// Service — owns the business operation
public class OrderService : IOrderService {
    public async Task<int> PlaceAsync(CreateOrderDto dto) {
        var product = await _productRepo.FindAsync(dto.ProductId);
        if (product.Stock < dto.Qty) throw new InsufficientStockException();
        product.Stock -= dto.Qty;
        var order = new Order { ProductId = dto.ProductId, Qty = dto.Qty };
        await _orderRepo.AddAsync(order);
        await _emailService.SendConfirmationAsync(dto.UserEmail, order);
        return order.Id;
    }
}
// Controller calls _orderService.PlaceAsync(dto) — one line, fully testable

Classic frameworks loaded everything at startup regardless of need. ASP.NET Core is composed of NuGet packages — you add only what the app uses. The resulting binary is smaller, startup is faster, and the attack surface is reduced because unused features are never loaded.

csharp
// Minimal MVC app — only what's needed
builder.Services
    .AddControllersWithViews()
    .AddRazorRuntimeCompilation();    // dev: hot-reload views

builder.Services.AddResponseCompression(o => o.EnableForHttps = true);
builder.Services.AddResponseCaching();

// NOT included unless explicitly added:
// SignalR, gRPC, Blazor, Identity, Swagger — each is an opt-in package
// This keeps the deployed image lean and dependencies explicit
MVC Pattern & Architecture

The Model holds domain data and business rules; the View renders HTML from that data; the Controller handles HTTP input, coordinates the model, and selects a view. Each layer has one reason to change — schema changes touch only Models, UI redesigns touch only Views, and request logic changes touch only Controllers.

csharp
// Model
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } }

// Controller
public class ProductsController : Controller {
    private readonly IProductService _svc;
    public ProductsController(IProductService svc) => _svc = svc;

    public IActionResult Index() {
        var products = _svc.GetAll();   // orchestrates model
        return View(products);          // selects view
    }
}

// View  (Views/Products/Index.cshtml)
// @model IEnumerable<Product>
// @foreach (var p in Model) { <p>@p.Name — @p.Price.ToString("C")</p> }

Conventions eliminate boilerplate: a controller named OrdersController automatically maps to the /Orders URL segment, and a method named Details maps to /Orders/Details/{id}. Configuration is only needed to override the defaults, which keeps Program.cs small.

csharp
// Program.cs — one line wires up all conventional routes
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

// No explicit mapping needed:
// GET /Orders        → OrdersController.Index()
// GET /Orders/5      → OrdersController.Details(5)
// GET /Orders/Create → OrdersController.Create()

Classic MVC 5 is coupled to System.Web and IIS HttpModules/HttpHandlers. ASP.NET Core uses a lightweight middleware pipeline independent of the host. Each middleware component calls next() to pass control forward, forming a bidirectional chain. MVC itself is just one middleware registered at the end.

csharp
var app = builder.Build();

app.UseExceptionHandler("/Error");   // outermost — catches all
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();                // short-circuits for static assets
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();                // MVC endpoint routing — innermost

ViewData is a Dictionary<string, object> — type-safe access requires a cast. ViewBag is a dynamic wrapper over ViewData — convenient but loses compile-time checking. TempData survives one redirect via session/cookie — used for post-redirect-get patterns but cleared after first read.

csharp
// Controller
ViewData["Title"] = "Dashboard";       // cast needed in view
ViewBag.Message   = "Welcome!";        // no cast, no compile check
TempData["Alert"] = "Saved OK";        // lives across one redirect

// After RedirectToAction("Index"):
// TempData["Alert"] is still available in the next request, then gone

Model binding reads from route values, query string, and request body in that order, then maps values to action parameter names by convention. Complex types are populated recursively. Binding sources can be pinned with attributes like [FromBody], [FromRoute], or [FromQuery].

csharp
// Route: /orders/{id}?includeItems=true
// Body:  { "note": "urgent" }
public IActionResult Process(
    [FromRoute]  int    id,            // from URL segment
    [FromQuery]  bool   includeItems,  // from ?includeItems=true
    [FromBody]   OrderNote note)       // from JSON body
{
    // id=5, includeItems=true, note.Text="urgent"
    return Ok();
}

Static helpers create hidden coupling and make unit tests impossible without spinning up infrastructure. DI makes dependencies explicit, allows mock substitution in tests, and lets the container manage lifetime (singleton, scoped, transient). Controllers receive fully-configured services without knowing how they are constructed.

csharp
// Registration (Program.cs)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();

// Controller — receives interfaces, not concrete types
public class OrdersController : Controller {
    public OrdersController(IOrderRepository repo, IEmailService mail) {
        _repo = repo; _mail = mail;
    }
    // In tests: pass mock implementations — no real DB or SMTP needed
}

Razor Pages co-locate the page model and HTML in one folder, ideal for simple CRUD screens with minimal shared logic. MVC Controllers shine when actions serve multiple views, when you need fine-grained routing control, or when building an API alongside UI. Large apps often mix both.

csharp
// Razor Page — Pages/Products/Edit.cshtml.cs
public class EditModel : PageModel {
    [BindProperty] public Product Product { get; set; }
    public async Task<IActionResult> OnPostAsync() {
        if (!ModelState.IsValid) return Page();
        await _repo.UpdateAsync(Product);
        return RedirectToPage("./Index");
    }
}

// MVC Controller — one action, multiple callers
public IActionResult Edit(int id, [FromQuery] string returnUrl) { ... }

WebForms baked UI logic into code-behind files tightly coupled to the HTTP context, making tests require a browser or full server. In MVC, controllers are plain classes whose methods return objects — no HTTP context needed. You call the action, assert the returned IActionResult, and never start a server.

csharp
[Fact]
public async Task Index_ReturnsViewWithProducts() {
    var mockRepo = new Mock<IProductRepo>();
    mockRepo.Setup(r => r.GetAllAsync()).ReturnsAsync(new[] { new Product { Name = "X" } });

    var controller = new ProductsController(mockRepo.Object);
    var result = await controller.Index() as ViewResult;

    Assert.NotNull(result);
    Assert.IsAssignableFrom<IEnumerable<Product>>(result.Model);
}
Controllers & Actions

The action selector first matches by HTTP verb using [HttpGet], [HttpPost], etc. Among remaining candidates it picks by name (conventional) or route template (attribute). If two methods match equally, an AmbiguousActionException is thrown. The [ActionName] attribute lets you alias a method under a different name.

csharp
public class ItemsController : Controller {
    [HttpGet]                    // GET /Items/Edit/5
    public IActionResult Edit(int id) => View(_repo.Find(id));

    [HttpPost]                   // POST /Items/Edit/5  — same name, different verb
    [ValidateAntiForgeryToken]
    public IActionResult Edit(Item item) {
        if (!ModelState.IsValid) return View(item);
        _repo.Update(item);
        return RedirectToAction(nameof(Index));
    }
}

Returning IActionResult lets one action produce different outcomes — a view on success, a redirect on creation, a 404 on missing resource — without changing the method signature. The concrete result type (ViewResult, RedirectResult, NotFoundResult) is chosen at runtime. ActionResult<T> extends this for Web API by also supporting content-negotiation.

csharp
public IActionResult Details(int id) {
    var item = _repo.Find(id);
    if (item is null)     return NotFound();         // 404
    if (!User.Identity.IsAuthenticated) return Unauthorized(); // 401
    return View(item);                               // 200 + HTML
}

// Web API variant — allows return type inference
public ActionResult<Product> Get(int id) {
    var p = _repo.Find(id);
    return p is null ? NotFound() : p;  // implicit 200 wrap
}

Async actions release the thread back to the pool while awaiting I/O (database, HTTP calls). This allows a single server to handle far more concurrent requests than one thread per request. The request itself is not finished; it’s parked until the awaited task completes, then resumed on any available thread.

csharp
// Sync — thread is blocked for the full DB duration
public IActionResult SlowSync(int id) {
    var data = _repo.FindSync(id);   // thread parked doing nothing
    return View(data);
}

// Async — thread returned to pool during the await
public async Task<IActionResult> FastAsync(int id) {
    var data = await _repo.FindAsync(id);  // thread free to serve other requests
    return View(data);
}

[ApiController] enables automatic model validation responses (returns 400 without reaching your action), infers binding sources ([FromBody] on complex types, [FromRoute] on route params), and disables HTML error pages in favor of RFC 7807 ProblemDetails JSON.

csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsApiController : ControllerBase {

    [HttpPost]
    public IActionResult Create(CreateProductDto dto) {
        // Without [ApiController] you'd check ModelState here.
        // With it, invalid dto = automatic 400 ProblemDetails before this line.
        var product = _svc.Create(dto);
        return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
    }
}

View() renders a template and returns HTTP 200 in the same request — the browser URL stays the same. RedirectToAction() sends HTTP 302, telling the browser to issue a new GET request. The Post-Redirect-Get pattern uses this to prevent duplicate form submissions on page refresh.

csharp
[HttpPost]
public IActionResult Create(Order order) {
    if (!ModelState.IsValid)
        return View(order);             // re-render form, URL stays /Orders/Create

    _repo.Add(order);
    TempData["Success"] = "Order placed";
    return RedirectToAction("Index");   // 302 → browser GETs /Orders — prevents re-post
}

All MVC controllers inherit Controller (or ControllerBase for APIs), which exposes User, HttpContext, ModelState, and helper methods like View(), Ok(), BadRequest(). A custom base controller centralizes shared behavior — auth checks, logging, common ViewData — without repeating it in every controller.

csharp
public abstract class AppBaseController : Controller {
    protected string CurrentUserId =>
        User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";

    protected void SetAlert(string msg) => TempData["Alert"] = msg;
}

public class OrdersController : AppBaseController {
    public IActionResult Create() {
        ViewBag.Owner = CurrentUserId;  // inherited
        return View();
    }
}

After binding populates the parameter, the framework runs data annotation validators and adds errors to ModelState. The action body checks ModelState.IsValid. If invalid, the controller re-renders the view with the current model so Razor Tag Helpers display field-level error messages from the validation attributes.

csharp
public class Product {
    [Required] [StringLength(100)] public string Name  { get; set; }
    [Range(0.01, 9999)]            public decimal Price { get; set; }
}

[HttpPost]
public IActionResult Save(Product p) {
    if (!ModelState.IsValid) {
        // ModelState["Name"].Errors[0].ErrorMessage = "The Name field is required."
        return View(p);           // view shows asp-validation-for messages
    }
    _repo.Save(p);
    return RedirectToAction("Index");
}

Calling HttpContext.RequestServices.GetService<T>() (service locator) hides dependencies, making the class harder to understand and test. Constructor injection makes every dependency visible in the signature — a controller with 6 constructor parameters signals it needs to be split, a feedback that service locator silences.

csharp
// Service locator — hidden deps, untestable without full DI container
public IActionResult Bad() {
    var svc = HttpContext.RequestServices.GetRequiredService<IOrderService>();
    return View(svc.GetAll());
}

// Constructor injection — explicit, mockable
private readonly IOrderService _svc;
public OrdersController(IOrderService svc) => _svc = svc;
public IActionResult Good() => View(_svc.GetAll());
Views & Razor

Razor’s parser uses implicit transitions: a single @ followed by a C# identifier or keyword switches to C# until the expression ends. An HTML tag inside a C# block switches back. Explicit @{ } blocks are used for multi-statement logic. The engine never emits the @ itself.

html
@* Implicit C# expression *@
<h1>Hello, @Model.Name!</h1>

@* Explicit block for multi-statement logic *@
@{
    var discount = Model.IsPremium ? 0.1m : 0m;
    var final    = Model.Price * (1 - discount);
}
<p>Price: @final.ToString("C")</p>

@* Loop — HTML tag inside C# block auto-transitions back *@
@foreach (var item in Model.Items) {
    <li>@item.Name</li>
}

Partial Views are simple template fragments — no logic, no DI, they receive whatever the parent passes. View Components run their own InvokeAsync method, accept their own DI services, and are independently testable. Partials are lighter; View Components are richer but add a class file.

csharp
// View Component — has own DI and logic
public class CartSummaryViewComponent : ViewComponent {
    private readonly ICartService _cart;
    public CartSummaryViewComponent(ICartService cart) => _cart = cart;

    public async Task<IViewComponentResult> InvokeAsync() {
        var count = await _cart.GetItemCountAsync(HttpContext.User);
        return View(count);
    }
}
// In Razor: @await Component.InvokeAsync("CartSummary")

// Partial View — just a template fragment
// <partial name="_ProductCard" model="product" />

@RenderBody() marks where each page’s unique content is inserted into the layout. @RenderSection("scripts", required: false) defines optional injection points — pages opt in by declaring @section scripts { }. This lets individual pages add page-specific CSS or JS without polluting other pages.

html
<!-- _Layout.cshtml -->
<html><body>
  <nav>...shared nav...</nav>
  <main>@RenderBody()</main>       @* page content injected here *@
  <footer>...</footer>
  @RenderSection("scripts", required: false)  @* optional per-page JS *@
</body></html>

<!-- Index.cshtml -->
@{ Layout = "_Layout"; }
<h1>Dashboard</h1>
@section scripts {
  <script src="~/js/dashboard.js"></script>
}

HTML Helpers use C# method calls inside Razor (@Html.TextBoxFor(...)), which breaks the HTML-editing experience in IDEs. Tag Helpers look like native HTML attributes (asp-for, asp-action), allowing designers to work with the markup without touching C# syntax, while still benefiting from IntelliSense and strong typing.

html
<!-- Old HTML Helper style -->
@Html.LabelFor(m => m.Email)
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.Email)

<!-- Tag Helper style — reads like plain HTML -->
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>

By convention the engine looks for Views/{Controller}/{Action}.cshtml, then Views/Shared/{Action}.cshtml. Calling View("CustomName") overrides the action name lookup. At runtime Razor compiles .cshtml to a C# class that is cached; subsequent requests run the compiled class, not the template parser.

csharp
// Lookup order for OrdersController.Details():
// 1. Views/Orders/Details.cshtml
// 2. Views/Shared/Details.cshtml
// 3. throws InvalidOperationException

public IActionResult Details(int id) {
    return View();                   // → Views/Orders/Details.cshtml
    // return View("Summary");       // → Views/Orders/Summary.cshtml
    // return View("~/Views/Admin/Report.cshtml", model);  // absolute path
}

Declaring @model Product at the top of a view makes Model typed as Product. The IDE provides IntelliSense for Model.Name, Model.Price, etc. Any property access to a non-existent member is a compile error (or pre-compilation error in VS), not a NullReferenceException at runtime.

html
@model Product           @* strongly-typed *@

<h2>@Model.Name</h2>                  @* IntelliSense here *@
<p>@Model.Price.ToString("C")</p>     @* compile error if Price removed *@

@* Without @model — dynamic ViewBag, no compile-time check *@
<h2>@ViewBag.ProductName</h2>         @* typo = silent null at runtime *@

_ViewImports.cshtml in a folder applies to all views in that folder and below. Placing @using, @addTagHelper, and @inject directives here removes the need to repeat them at the top of every .cshtml file. Multiple _ViewImports at different levels are merged, deeper ones taking precedence.

html
@* Views/_ViewImports.cshtml — applies to all views *@
@using MyApp.Models
@using MyApp.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyApp           @* register custom tag helpers *@
@inject ICurrentUser CurrentUser  @* inject service into every view *@

@* Now every view can use Product, OrderViewModel, asp-for, CurrentUser
   without individual @using or @inject statements *@

By default in ASP.NET Core 3+, Razor views are compiled into the application assembly at build time (Razor SDK). This eliminates the first-request compilation delay and surfaces template errors at build rather than runtime. The tradeoff is that changing a view requires a redeployment — runtime compilation can be re-enabled for dev workflows.

csharp
// Re-enable runtime compilation (dev only — adds Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation)
// Program.cs
builder.Services.AddControllersWithViews()
       .AddRazorRuntimeCompilation();   // hot-reload views without restarting

// To verify pre-compilation is working — no .cshtml files in published output:
// dotnet publish -c Release
// ls publish/ → MyApp.Views.dll  (views baked in)
Models, Validation & Data

Annotations on the model class are read by the model binder to populate ModelState. In the view, asp-validation-for Tag Helpers read ModelState and emit field-level error messages. jQuery Unobtrusive Validation also reads the data-val-* attributes emitted from the annotations to validate client-side before the form posts.

csharp
public class RegisterDto {
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required, MinLength(8)]
    [DataType(DataType.Password)]
    public string Password { get; set; }
}

// View emits: data-val="true" data-val-required="The Email field is required."
// Client-side jQuery validates before POST
// Server-side: ModelState.IsValid checks same rules again

EF Core maps plain C# classes (entities) to database tables. A DbContext subclass exposes DbSet<T> properties per entity. Controllers receive the context via DI and query with LINQ. MVC scaffolding can auto-generate controller and view code from a DbContext and entity class.

csharp
public class AppDbContext : DbContext {
    public AppDbContext(DbContextOptions<AppDbContext> o) : base(o) {}
    public DbSet<Order> Orders { get; set; }
}

// In controller
public async Task<IActionResult> Index() {
    var orders = await _db.Orders
        .Include(o => o.Items)
        .Where(o => !o.IsCancelled)
        .OrderByDescending(o => o.CreatedAt)
        .ToListAsync();
    return View(orders);
}

Domain models are shaped by persistence and business rules; ViewModels are shaped by what a single screen needs. Exposing domain models directly to views risks over-posting (binding fields the user should not set), leaks sensitive columns, and couples view changes to schema changes. ViewModels act as an explicit contract.

csharp
// Domain — has IsAdmin, PasswordHash, etc.
public class User { public int Id; public string Email; public bool IsAdmin; public string PasswordHash; }

// ViewModel — exposes only what the profile page needs
public class ProfileViewModel {
    [Required] [EmailAddress] public string Email    { get; set; }
    [Display(Name = "Full name")] public string Name { get; set; }
    // No IsAdmin, no PasswordHash — can't be over-posted
}

ModelState.IsValid returns false if any binding or validation error was recorded — including type conversion failures (e.g., “abc” for an int field). Checking it before processing prevents acting on partial or invalid data. Adding manual errors via ModelState.AddModelError allows business-rule violations to use the same validation UI.

csharp
[HttpPost]
public IActionResult Register(RegisterDto dto) {
    if (!ModelState.IsValid) return View(dto);  // annotation errors

    if (_userService.EmailExists(dto.Email)) {
        ModelState.AddModelError(nameof(dto.Email), "Email is already registered.");
        return View(dto);  // business-rule error shown in same validation UI
    }

    _userService.Create(dto);
    return RedirectToAction("Confirm");
}

FluentValidation uses a fluent API to express rules in a validator class separate from the model, enabling database lookups, cross-field comparisons, and conditional rules that annotations cannot express. It integrates with ModelState via an adapter, so the same validation pipeline applies.

csharp
public class OrderValidator : AbstractValidator<CreateOrderDto> {
    public OrderValidator(IProductRepo repo) {
        RuleFor(x => x.ProductId)
            .NotEmpty()
            .MustAsync(async (id, _) => await repo.ExistsAsync(id))
            .WithMessage("Product not found.");

        RuleFor(x => x.ShipDate)
            .GreaterThan(x => x.OrderDate)
            .WithMessage("Ship date must be after order date.");
    }
}

The dotnet aspnet-codegenerator tool reads a model class and DbContext, then emits a controller with Index/Details/Create/Edit/Delete actions and corresponding Razor views. It is a starting point — the generated code is meant to be modified, not used verbatim, because it does no authorization and puts DB calls directly in the controller.

csharp
// CLI scaffold command:
// dotnet aspnet-codegenerator controller -name ProductsController \
//   -m Product -dc AppDbContext --relativeFolderPath Controllers \
//   --useDefaultLayout --referenceScriptLibraries

// Generates:
// Controllers/ProductsController.cs  (Index/Details/Create/Edit/Delete)
// Views/Products/Index.cshtml
// Views/Products/Edit.cshtml
// Views/Products/Create.cshtml
// Views/Products/Delete.cshtml
// Views/Products/Details.cshtml
Routing

Conventional routing defines URL patterns centrally and applies them by name — good for uniform REST surfaces. Attribute routing decorates each action with its own [Route] — good for APIs that need non-uniform URLs, versioning (/api/v2/orders), or when the URL must not change as controllers are renamed.

csharp
[Route("api/v{version:int}/orders")]
public class OrdersApiController : ControllerBase {

    [HttpGet]                         // GET api/v1/orders
    public IActionResult List() => Ok(_repo.GetAll());

    [HttpGet("{id:int}")]             // GET api/v1/orders/5
    public IActionResult Get(int id) { ... }

    [HttpPost("bulk-cancel")]         // POST api/v1/orders/bulk-cancel
    public IActionResult BulkCancel([FromBody] int[] ids) { ... }
}

Inline constraints after a colon in the route template ({id:int}, {slug:minlength(3)}, {status:regex(^(open|closed)$)}) reject non-matching segments before the action is invoked. This prevents a route from consuming a URL intended for a different action lower in the routing table.

csharp
// GET /products/42   — matches (42 is int)
// GET /products/abc  — 404, skips this action
[HttpGet("products/{id:int:min(1)}")]
public IActionResult GetById(int id) { ... }

// GET /reports/2024-01  — matches (datetime format)
// GET /reports/latest   — does NOT match, falls through to next route
[HttpGet("reports/{month:datetime}")]
public IActionResult MonthlyReport(DateTime month) { ... }

The endpoint routing system scores each candidate: more-specific literal segments beat parameters, constrained parameters beat unconstrained, shorter templates beat longer catch-alls. If two candidates score equally, an AmbiguousMatchException is thrown at startup, making routing conflicts visible immediately.

csharp
// GET /users/profile  → literal "profile" beats {id} parameter
[HttpGet("users/profile")]
public IActionResult Profile() { ... }

// GET /users/42  → constrained {id:int} wins over unconstrained {slug}
[HttpGet("users/{id:int}")]
public IActionResult ById(int id) { ... }

// GET /users/john-doe  → unconstrained fallback
[HttpGet("users/{slug}")]
public IActionResult BySlug(string slug) { ... }

Areas add a top-level namespace to the route and a matching folder structure (Areas/{Area}/Controllers, Areas/{Area}/Views). This allows two controllers with the same name in different areas (Admin/HomeController vs Shop/HomeController) to coexist without naming conflicts.

csharp
// Areas/Admin/Controllers/HomeController.cs
[Area("Admin")]
public class HomeController : Controller {
    [HttpGet("/admin")]
    public IActionResult Index() => View();
}

// Program.cs — area route registration
app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

// Link to admin area from Razor
// <a asp-area="Admin" asp-controller="Home" asp-action="Index">Admin</a>

Url.Action looks up the registered route table and builds the URL from the controller/action name and route values. If the route pattern changes, generated URLs update automatically. Hardcoded strings silently break. The equivalent in Razor is the asp-action Tag Helper.

csharp
// In controller — redirect without hardcoding the path
return Redirect(Url.Action("Details", "Orders", new { id = order.Id }));
// → /orders/42  (correct even if route template changes)

// In Razor view
// <a asp-controller="Orders" asp-action="Details" asp-route-id="@order.Id">
//     View Order
// </a>

The router extracts named segments from the matched template and places them in RouteData.Values. The model binder then reads that dictionary, matching keys to parameter names by convention (case-insensitive). Optional parameters default to null or the route default when the segment is absent.

csharp
// Route template: {controller}/{action}/{id?}
// URL: /products/edit/7
// RouteData.Values: { controller="products", action="edit", id="7" }

public IActionResult Edit(int id) {
    // id = 7 — automatically parsed from route string "7"
    var product = _repo.Find(id);
    return View(product);
}

// URL: /products/create  (no id segment)
public IActionResult Create(int? id) {
    // id = null — optional segment missing
}
Filters & Middleware

Middleware operates on raw HttpContext before MVC routing resolves the endpoint — it knows nothing about controllers or actions. Filters run inside the MVC pipeline after routing, with full access to ActionExecutingContext (the action name, parameters, controller instance). Use middleware for cross-cutting HTTP concerns; use filters for MVC-specific concerns.

csharp
public class AuditActionFilter : IActionFilter {
    public void OnActionExecuting(ActionExecutingContext ctx) {
        // Access controller name, action name, bound arguments
        var action = ctx.ActionDescriptor.DisplayName;
        var userId = ctx.HttpContext.User.Identity?.Name;
        _audit.Log($"{userId} → {action}");
    }
    public void OnActionExecuted(ActionExecutedContext ctx) { }
}

// Register globally
builder.Services.AddControllersWithViews(o => o.Filters.Add<AuditActionFilter>());

Exception filters (IExceptionFilter) catch only exceptions thrown inside MVC actions and filters. Middleware-based handlers (UseExceptionHandler) catch everything including exceptions from other middleware. A typical app uses both: exception filter for MVC-specific error responses (e.g., returning ProblemDetails), middleware for the outer error page.

csharp
public class ApiExceptionFilter : IExceptionFilter {
    public void OnException(ExceptionContext ctx) {
        if (ctx.Exception is NotFoundException nfe) {
            ctx.Result = new NotFoundObjectResult(
                new ProblemDetails { Title = "Not found", Detail = nfe.Message });
            ctx.ExceptionHandled = true;
        }
        // Other exceptions bubble up to UseExceptionHandler middleware
    }
}

Authorization filters run before any other filter type. Setting context.Result immediately ends processing — no action runs, no other filters run. The built-in [Authorize] attribute implements this pattern, returning 401/403 for unauthenticated or unauthorized users.

csharp
public class ApiKeyFilter : IAuthorizationFilter {
    public void OnAuthorization(AuthorizationFilterContext ctx) {
        var key = ctx.HttpContext.Request.Headers["X-Api-Key"].FirstOrDefault();
        if (key != _config["ApiKey"]) {
            // Short-circuit: set Result → action never executes
            ctx.Result = new UnauthorizedObjectResult(
                new { error = "Invalid API key" });
        }
        // key valid → fall through, action executes normally
    }
}

Result filters run around IActionResult.ExecuteResultAsync — after the action returns a result but before the response is written. They can inspect or replace the result, add headers, or log response data. Common use: adding cache headers based on the result type.

csharp
public class NoCacheFilter : IResultFilter {
    public void OnResultExecuting(ResultExecutingContext ctx) {
        ctx.HttpContext.Response.Headers["Cache-Control"] = "no-store, no-cache";
        ctx.HttpContext.Response.Headers["Pragma"] = "no-cache";
    }
    public void OnResultExecuted(ResultExecutedContext ctx) {
        // runs after response written — useful for timing/logging
        var elapsed = ctx.HttpContext.Items["RequestStart"] as Stopwatch;
        elapsed?.Stop();
    }
}

Middleware executes in registration order. Placing UseAuthentication before UseAuthorization ensures the principal is populated before authorization checks. UseStaticFiles before UseRouting short-circuits static asset requests before the MVC pipeline runs, eliminating per-asset routing overhead.

csharp
app.UseExceptionHandler("/Error");   // 1 — outermost error boundary
app.UseHsts();                       // 2 — security header
app.UseHttpsRedirection();           // 3 — before anything processes HTTP
app.UseStaticFiles();                // 4 — short-circuits .js/.css before routing
app.UseRouting();                    // 5 — resolves endpoint
app.UseAuthentication();             // 6 — must precede Authorization
app.UseAuthorization();              // 7 — requires auth from step 6
app.MapControllers();                // 8 — actual endpoint execution

Global filters run for every request that reaches MVC. Controller-level filters apply to all actions in one controller. Action-level filters apply to a single action. They stack and run in order: global → controller → action (and unwind in reverse). A controller can override global filters using [OverrideResultFilter] from the MVC filter override interfaces.

csharp
// Global — every MVC request
builder.Services.AddControllersWithViews(o => o.Filters.Add<AuditFilter>());

// Controller-level — all actions in this controller
[ServiceFilter(typeof(TenantFilter))]
public class OrdersController : Controller { ... }

// Action-level — this action only
[ResponseCache(Duration = 60)]
public IActionResult Catalog() => View(_repo.GetAll());
Security & Authentication

Identity stores user records in a SQL table via EF Core. Passwords are hashed using PBKDF2 with a per-user salt and a configurable iteration count — never stored in plaintext. UserManager<TUser> provides APIs for creation, password verification, role assignment, claims, and two-factor setup.

csharp
// Registration
var user   = new AppUser { UserName = dto.Email, Email = dto.Email };
var result = await _userManager.CreateAsync(user, dto.Password);
// Internally: PasswordHasher.HashPassword() → PBKDF2 stored in PasswordHash column

// Sign-in
var signIn = await _signInManager.PasswordSignInAsync(
    dto.Email, dto.Password, isPersistent: false, lockoutOnFailure: true);

if (signIn.Succeeded) return RedirectToAction("Dashboard");
ModelState.AddModelError("", "Invalid credentials");

Plain [Authorize] only checks authentication. Policies encode requirements (minimum age, specific claim, custom handler logic) registered at startup and referenced by name. This separates the what (the attribute) from the how (the requirement logic), allowing complex rules without cluttering controllers.

csharp
// Registration
builder.Services.AddAuthorization(opts => {
    opts.AddPolicy("SeniorEditor", p =>
        p.RequireClaim("Department", "Editorial")
         .RequireClaim("Level", "Senior", "Lead"));
});

// Usage
[Authorize(Policy = "SeniorEditor")]
public IActionResult PublishArticle(int id) { ... }

// Custom requirement example
public class MinAgeRequirement : IAuthorizationRequirement { public int MinAge; }
public class MinAgeHandler : AuthorizationHandler<MinAgeRequirement> {
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext ctx, MinAgeRequirement req) {
        var dob = ctx.User.FindFirst("DateOfBirth");
        if (dob != null && DateTime.Today.Year - DateTime.Parse(dob.Value).Year >= req.MinAge)
            ctx.Succeed(req);
        return Task.CompletedTask;
    }
}

On GET, MVC embeds a hidden token in the form and a matching encrypted cookie. On POST, [ValidateAntiForgeryToken] checks that both tokens match. A malicious site cannot forge the form token because it cannot read the cookie (same-origin policy), so cross-site POST requests fail validation.

html
<!-- Razor form — Tag Helper adds hidden __RequestVerificationToken automatically -->
<form asp-action="Delete" asp-controller="Products" method="post">
  <input type="hidden" asp-for="Id" />
  <button type="submit">Delete</button>
</form>
csharp
[HttpPost]
[ValidateAntiForgeryToken]   // rejects request if tokens don't match
public IActionResult Delete(int id) {
    _repo.Delete(id);
    return RedirectToAction("Index");
}

UseHttpsRedirection sends a 301/302 redirect when plain HTTP is received. UseHsts adds the Strict-Transport-Security header telling browsers to always use HTTPS for the configured duration — even if a user manually types http://. Together they eliminate protocol downgrade attacks in transit.

csharp
// Program.cs
if (!app.Environment.IsDevelopment()) {
    app.UseHsts();   // Strict-Transport-Security: max-age=31536000; includeSubDomains
}
app.UseHttpsRedirection();  // HTTP → HTTPS 301

// Fine-tuning HSTS duration
builder.Services.AddHsts(opts => {
    opts.MaxAge            = TimeSpan.FromDays(365);
    opts.IncludeSubDomains = true;
    opts.Preload           = true;  // submit to browser preload lists
});

Cookie authentication stores a session ID; the server holds session state. JWT is stateless — the token carries all claims and is signed, so the server validates the signature without any store. Cookies suit traditional MVC apps (automatic browser handling, anti-forgery integration); JWT suits APIs consumed by SPAs or mobile clients.

csharp
// JWT configuration (Program.cs)
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opts => {
        opts.TokenValidationParameters = new() {
            ValidateIssuer           = true,
            ValidIssuer              = builder.Configuration["Jwt:Issuer"],
            ValidateAudience         = true,
            ValidAudience            = builder.Configuration["Jwt:Audience"],
            ValidateLifetime         = true,
            IssuerSigningKey         = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
        };
    });

Implementing IClaimsTransformation lets you add, remove, or rewrite claims after the identity provider authenticates the user but before authorization policies evaluate them. Common use: load application-specific roles from the database for a user authenticated via an external provider (Azure AD, Google).

csharp
public class AppClaimsTransformer : IClaimsTransformation {
    private readonly IUserRepo _repo;
    public AppClaimsTransformer(IUserRepo repo) => _repo = repo;

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) {
        var identity = (ClaimsIdentity)principal.Identity!;
        var email    = identity.FindFirst(ClaimTypes.Email)?.Value;
        if (email != null) {
            var roles = await _repo.GetRolesAsync(email);
            foreach (var role in roles)
                identity.AddClaim(new Claim(ClaimTypes.Role, role));
        }
        return principal;
    }
}
Performance & Caching

[ResponseCache] sets HTTP cache headers (Cache-Control, Vary) telling proxies and browsers to reuse the response for the specified duration. For server-side storage, UseResponseCaching middleware caches the response body in memory, serving identical requests without executing the action or querying the database.

csharp
// Program.cs
builder.Services.AddResponseCaching();
app.UseResponseCaching();

// Action — cached for 5 minutes, vary by Accept header
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "Accept")]
public async Task<IActionResult> Catalog() {
    var items = await _repo.GetCatalogAsync();  // DB hit once per 5 min
    return Ok(items);
}

Response caching delegates to the HTTP cache spec (requires correct headers from the client). Output caching (.NET 7+) is server-controlled — it always caches regardless of request headers, supports tags for selective invalidation, and can vary by custom keys. It is more predictable for app-level caching.

csharp
// Program.cs (.NET 7+)
builder.Services.AddOutputCache();
app.UseOutputCache();

// Cache by query string, tag for invalidation
[HttpGet]
[OutputCache(Duration = 120, VaryByQueryKeys = new[] {"page","sort"}, Tags = new[] {"catalog"})]
public IActionResult Catalog(int page = 1, string sort = "name") => View(_repo.GetPage(page, sort));

// Invalidate when a product is added
await _outputCacheStore.EvictByTagAsync("catalog", CancellationToken.None);

IMemoryCache stores data in the process’s RAM — fast, but lost on restart and not shared across multiple server instances. IDistributedCache (backed by Redis or SQL Server) is shared across all instances, survives restarts, and is consistent — necessary for any multi-instance or containerized deployment.

csharp
// IMemoryCache — single server
if (!_cache.TryGetValue("catalog", out List<Product> products)) {
    products = await _repo.GetAllAsync();
    _cache.Set("catalog", products, TimeSpan.FromMinutes(10));
}

// IDistributedCache — Redis (shared across pods)
builder.Services.AddStackExchangeRedisCache(o => o.Configuration = "localhost:6379");

var bytes = await _dist.GetAsync("catalog");
if (bytes is null) {
    var data = await _repo.GetAllAsync();
    await _dist.SetAsync("catalog", JsonSerializer.SerializeToUtf8Bytes(data),
        new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromMinutes(10) });
}

ASP.NET Core uses a bounded thread pool. A synchronous action blocks a thread for the entire I/O wait. With 100 concurrent DB calls of 200 ms each, 100 threads are consumed at once — exceeding default pool sizes causes queuing and latency spikes. Async returns threads during I/O, so 100 concurrent requests may use only 5–10 threads.

csharp
// Bad — thread blocked the full duration of DB + HTTP call
public IActionResult Report() {
    var orders  = _db.Orders.ToList();          // blocks thread
    var weather = _http.GetStringAsync(url).Result; // blocks thread again
    return View((orders, weather));
}

// Good — interleaved I/O, minimal thread usage
public async Task<IActionResult> Report() {
    var ordersTask  = _db.Orders.ToListAsync();
    var weatherTask = _http.GetStringAsync(url);
    await Task.WhenAll(ordersTask, weatherTask);  // both run concurrently
    return View((ordersTask.Result, weatherTask.Result));
}

Without pre-compilation, the Razor engine parses and compiles .cshtml files on first access. With Razor SDK build compilation, all views are compiled into an assembly at build time — no filesystem reads or Roslyn compilation at startup. Combined with AOT publishing, the JIT cost of the first view render is also eliminated.

csharp
// .csproj — Razor views compiled into assembly (default since .NET Core 3)
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <!-- RazorCompileOnBuild defaults to true — no .cshtml shipped -->
    <!-- To force runtime compilation (dev only): -->
    <!-- <RazorCompileOnBuild>false</RazorCompileOnBuild> -->
  </PropertyGroup>
</Project>

// Verify: publish output contains MyApp.Views.dll, no .cshtml files

Bundling concatenates multiple CSS or JS files into a single file, reducing round-trips. Minification strips whitespace and renames variables, shrinking payload. ASP.NET Core uses LibMan or npm for client assets, and the bundleconfig.json / WebOptimizer / Gulp pipeline to produce the bundles at build time.

csharp
// WebOptimizer (NuGet: LigerShark.WebOptimizer.Core)
builder.Services.AddWebOptimizer(pipeline => {
    pipeline.AddCssBundle("/css/site.min.css",
        "css/bootstrap.css",
        "css/app.css",          // 3 files → 1 HTTP request
        "css/theme.css");

    pipeline.AddJavaScriptBundle("/js/app.min.js",
        "js/jquery.js",
        "js/bootstrap.js",
        "js/main.js");
});

app.UseWebOptimizer();          // serves /css/site.min.css bundled + minified
Testing & Dependency Injection

Because controller dependencies are passed via the constructor, tests instantiate the controller directly with mock implementations — no web server, no HTTP pipeline needed. The test controls exactly what each service returns, enabling fast, deterministic unit tests for every action path.

csharp
[Fact]
public async Task Create_RedirectsOnSuccess() {
    var repo = new Mock<IOrderRepo>();
    repo.Setup(r => r.AddAsync(It.IsAny<Order>())).Returns(Task.CompletedTask);

    var ctrl = new OrdersController(repo.Object);
    var dto  = new CreateOrderDto { ProductId = 1, Qty = 2 };

    var result = await ctrl.Create(dto) as RedirectToActionResult;

    Assert.NotNull(result);
    Assert.Equal("Index", result.ActionName);
    repo.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
}

Cast the IActionResult to ViewResult and inspect .ViewName and .Model. For API controllers cast to OkObjectResult, NotFoundResult, etc. No HTML rendering occurs — the test validates the controller’s decision, not the template.

csharp
[Fact]
public async Task Details_ReturnsNotFound_WhenProductMissing() {
    var repo = new Mock<IProductRepo>();
    repo.Setup(r => r.FindAsync(99)).ReturnsAsync((Product?)null);
    var ctrl = new ProductsController(repo.Object);

    var result = await ctrl.Details(99);

    Assert.IsType<NotFoundResult>(result);
}

[Fact]
public async Task Details_ReturnsViewWithProduct_WhenFound() {
    var expected = new Product { Id = 1, Name = "Widget" };
    var repo = new Mock<IProductRepo>();
    repo.Setup(r => r.FindAsync(1)).ReturnsAsync(expected);
    var ctrl = new ProductsController(repo.Object);

    var result = await ctrl.Details(1) as ViewResult;

    Assert.Equal(expected, result!.Model);
}

Singleton services are created once and shared for the app’s lifetime. Scoped services are created once per HTTP request. Transient services are created every time they are requested. Injecting a scoped service into a singleton causes a “captive dependency” bug — the scoped instance outlives the request and shares state incorrectly.

csharp
builder.Services.AddSingleton<IAppConfig, AppConfig>();     // one instance app-wide
builder.Services.AddScoped<IOrderRepo, EfOrderRepo>();      // one per HTTP request
builder.Services.AddTransient<IEmailFormatter, HtmlFormatter>(); // new every inject

// Captive dependency — DON'T do this:
builder.Services.AddSingleton<MySingleton>();  // holds a Scoped = bug
// builder.Services.AddScoped<IScopedDep, ...>() inside AddSingleton constructor

// Validate at startup (dev only):
builder.Host.UseDefaultServiceProvider(o => o.ValidateScopes = true);

Moq generates proxy implementations of interfaces at runtime. You configure return values and verify call counts without writing a real class. Tests run in milliseconds because no DB, HTTP, or filesystem is involved. Thrown exceptions simulate error paths that would be hard to trigger in integration.

csharp
var mock = new Mock<IPaymentGateway>();

// Simulate transient failure on first call, success on second
mock.SetupSequence(g => g.ChargeAsync(It.IsAny<decimal>()))
    .ThrowsAsync(new PaymentException("timeout"))
    .ReturnsAsync(new ChargeResult { Success = true });

var ctrl   = new CheckoutController(mock.Object);
var result = await ctrl.PlaceOrder(new OrderDto { Total = 99m });

// First call threw, controller retried, second call succeeded
Assert.IsType<RedirectToActionResult>(result);
mock.Verify(g => g.ChargeAsync(99m), Times.Exactly(2));

WebApplicationFactory<TProgram> boots the real app in-process using a test HTTP client — no network ports opened. Middleware, routing, filters, and views all execute. Services can be replaced with fakes via ConfigureTestServices. Integration tests verify behavior the full pipeline would produce, not just controller method logic.

csharp
public class OrdersIntegrationTests : IClassFixture<WebApplicationFactory<Program>> {
    private readonly HttpClient _client;

    public OrdersIntegrationTests(WebApplicationFactory<Program> factory) {
        _client = factory.WithWebHostBuilder(b => {
            b.ConfigureTestServices(s => {
                s.AddScoped<IOrderRepo, FakeOrderRepo>();  // replace real DB
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetOrders_Returns200() {
        var response = await _client.GetAsync("/orders");
        response.EnsureSuccessStatusCode();
    }
}

DbContext should be registered as Scoped — one instance per request. This ensures all EF operations in a single request share the same Unit of Work (change tracking, transaction), then the context is disposed at request end. A singleton DbContext leaks state across requests; a transient one breaks change tracking across service calls.

csharp
// Correct — AddDbContext defaults to Scoped
builder.Services.AddDbContext<AppDbContext>(opts =>
    opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// All services in the same HTTP request share the same DbContext instance:
public class OrderService {
    public OrderService(AppDbContext db) => _db = db;  // same instance as ...
}
public class CustomerService {
    public CustomerService(AppDbContext db) => _db = db;  // ... this one
}
// Both resolve the same scoped DbContext → coherent Unit of Work per request
Prev
Next
Drag
Map
HTML Snippets Powered By : XYZScripts.com
Select the fields to be shown. Others will be hidden. Drag and drop to rearrange the order.
  • Image
  • SKU
  • Rating
  • Price
  • Stock
  • Availability
  • Add to cart
  • Description
  • Content
  • Weight
  • Dimensions
  • Additional information
Click outside to hide the comparison bar
Compare