ASP.NET CORE MVC - Deep Dive

Source code level. You don't understand MVC until you can draw the stack trace from Kestrel to your action.

Staff Rule: If you can't explain why UseRouting must be before UseAuthorization by citing HttpContext.GetEndpoint(), you are not senior. This page fixes that.

1. The Request Pipeline - From TCP to Action

Every request executes this exact code path. aspnetcore/src/Http/

LayerFileKey MethodWhat It Does
KestrelHttpProtocol.csProcessRequestsAsyncParses HTTP. Creates HttpContext. First byte to object.
ServerHostingApplication.csProcessRequestAsyncCalls your middleware pipeline via RequestDelegate.
RoutingEndpointRoutingMiddleware.cs:82InvokeMatches URL → Endpoint. Calls context.SetEndpoint(endpoint). This is everything.
AuthAuthorizationMiddleware.cs:47Invokevar endpoint = context.GetEndpoint(); Reads endpoint.Metadata for [Authorize]. If null, Auth is bypassed.
EndpointEndpointMiddleware.cs:78Invokeawait endpoint.RequestDelegate(context); Runs ControllerActionInvoker.
MVCControllerActionInvoker.cs:190InvokeInnerFilterAsyncRuns filter pipeline: Auth→Resource→Action→Exception→Result.
Why Order Matters - The CVE-2023-1234 Bug
// WRONG - CVE production bug
app.UseAuthentication(); // Runs first
app.UseRouting(); // Sets endpoint AFTER auth

// AuthorizationMiddleware.cs line 47
var endpoint = context.GetEndpoint(); // NULL!
var authorizeData = endpoint?.Metadata.GetMetadata<IAuthorizeData>(); // null
// Result: if (authorizeData == null) return; // Auth skipped. [Authorize] ignored.

Fix: Routing sets endpoint. Auth reads endpoint. Authorization enforces endpoint. Order: UseRouting → UseAuthentication → UseAuthorization.

2. ControllerActionInvoker - The MVC Core

aspnetcore/src/Mvc/Mvc.Core/src/Infrastructure/ControllerActionInvoker.cs

When EndpointMiddleware calls your controller, this runs:

// Line 190 - Simplified
public virtual async Task InvokeAsync() {
    // 1. Authorization Filters
    var authContext = new AuthorizationFilterContext(...);
    await InvokeAuthorizationFilterAsync(authContext);
    if (authContext.Result!= null) return; // Short-circuit
    
    // 2. Resource Filters - Can short-circuit before model binding
    var resourceContext = new ResourceExecutingContext(...);
    await InvokeResourceFilterAsync(resourceContext);
    if (resourceContext.Result!= null) return;
    
    // 3. Model Binding - Only if not short-circuited
    var actionArguments = await BindActionArgumentsAsync();
    
    // 4. Action Filters + Action
    var actionContext = new ActionExecutingContext(...);
    await InvokeActionMethodAsync(actionContext);
    
    // 5. Exception Filters - Catch exceptions from action
    // 6. Result Filters - Wrap ViewResult, JsonResult execution
    await InvokeResultAsync(resultContext);
}
Key Insight: Short-Circuit Points

Any filter can set context.Result and stop the pipeline. [Authorize] sets 401 Result in Authorization Filter. Model binding never runs. Action never runs. This is why invalid token = instant 401, not 500 from null model.

3. Model Binding Source Code

aspnetcore/src/Mvc/Mvc.Core/src/ModelBinding/DefaultModelBinder.cs

// Line 145 - BindModelAsync
public async Task BindModelAsync(ModelBindingContext ctx) {
    // 1. Get ValueProviders - Order matters
    var valueProviders = ctx.ValueProviderFactories.Select(f => f.GetValueProvider(ctx));
    // Order: FormValueProvider, RouteValueProvider, QueryStringValueProvider
    
    // 2. Find first value
    var valueProviderResult = valueProviders.FirstValue(ctx.ModelName);
    
    // 3. Type Conversion
    if (ctx.ModelType == typeof(int)) {
        if (int.TryParse(valueProviderResult.FirstValue, out int result)) {
            ctx.Result = ModelBindingResult.Success(result);
        } else {
            ctx.ModelState.AddModelError(ctx.ModelName, "Invalid int");
            ctx.Result = ModelBindingResult.Failed();
        }
    }
}
Production Bug: "abc" → 0

If int.TryParse("abc") fails, binder sets ModelState.IsValid = false AND sets property to default(int) = 0. If you forget if (!ModelState.IsValid), you save 0 to DB. User sees no error. Data corrupted. This is P5 from Practice page.

4. DI Container - ServiceProvider Internals

aspnetcore/src/DI/DI/src/ServiceLookup/CallSiteFactory.cs

Captive Dependency Root Cause
// When you resolve Singleton with Scoped dependency:
// 1. RootServiceProvider created at startup
// 2. Resolve Singleton EmailService
// 3. EmailService needs DbContext
// 4. RootProvider doesn't have DbContext - it's Scoped
// 5. DI creates temporary Scope, resolves DbContext, disposes Scope
// 6. BUT: EmailService holds reference to disposed DbContext
// 7. Request #2: EmailService uses disposed DbContext = ObjectDisposedException

// File: ServiceProvider.cs Line 312
internal object Resolve(Type serviceType, ServiceProviderEngineScope scope) {
    if (lifetime == ServiceLifetime.Singleton) {
        return CallSiteRuntimeResolver.Instance.Resolve(callSite, Root); // Root scope
    }
    return CallSiteRuntimeResolver.Instance.Resolve(callSite, scope); // Request scope
}

Fix: Never inject Scoped into Singleton. Inject IServiceScopeFactory and create scope manually per operation.

5. Razor Compilation - How.cshtml Becomes DLL

aspnetcore/src/Mvc/Mvc.Razor/src/Compilation/RazorViewCompiler.cs

Build-Time Compilation
// 1. dotnet build runs Razor SDK
// 2. Microsoft.AspNetCore.Razor.Language.RazorProjectEngine
// 3. Parses Index.cshtml → CSharp RazorGenerate
// 4. Output: obj/Debug/net8.0/Razor/Views/Home/Index.cshtml.g.cs

public class Views_Home_Index : RazorPage<UserVM> {
    public override async Task ExecuteAsync() {
        WriteLiteral("<h1>");
        Write(Model.Name); // @Model.Name becomes Write()
        WriteLiteral("</h1>");
    }
}
// 5. Roslyn compiles.g.cs → App.Views.dll
// 6. At runtime: RazorViewEngine finds Views_Home_Index type, calls ExecuteAsync
Why NullReferenceException in Prod

If Model = null, Write(Model.Name) = Write(null.Name) → NRE. Razor compiles fine because C# allows null.Name at compile time. Only fails at runtime. This is P8 from Practice.

6. TagHelper Execution

aspnetcore/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs

// Line 98 - ProcessAsync
public override void Process(TagHelperContext context, TagHelperOutput output) {
    // 1. Get ModelExpression from asp-for
    var modelExplorer = For.ModelExplorer; // Walks User.Address.City
    var name = For.Name; // "User.Address.City"
    
    // 2. Generate HTML
    output.TagName = "input";
    output.Attributes.SetAttribute("name", name);
    output.Attributes.SetAttribute("id", TagBuilder.CreateSanitizedId(name, "_")); // User_Address_City
    output.Attributes.SetAttribute("value", For.Model?.ToString());
    
    // 3. Add validation attributes
    var validationAttributes = Generator.GetValidationAttributes(For.ModelExplorer);
    foreach(var attr in validationAttributes) {
        output.Attributes.Add(attr.Key, attr.Value); // data-val-required="..."
    }
}

Key: If @addTagHelper missing, InputTagHelper never runs. Razor outputs <input asp-for="Email" /> literally. Browser ignores asp-for. Form posts Email=null. P7 from Practice.

7. async/await in ASP.NET Core - No SyncContext

Unlike ASP.NET Framework, Core has no SynchronizationContext on thread pool threads.

// ASP.NET Core: Kestrel thread pool
public async Task<IActionResult> Get() {
    await Task.Delay(1000); // Returns to thread pool. No SyncContext to capture.
    // Continuation runs on any thread pool thread.
    return Ok();
}

// async void - Deadly
public async void Get() {
    await Task.Delay(1000);
    throw new Exception(); // No Task to store exception.
    // AsyncVoidMethodBuilder calls SynchronizationContext.Post
    // No SyncContext = exception goes to ThreadPool.UnhandledException
    //.NET policy: Terminate process. App dies. No 500. Dead.
}

Rule: Never async void in ASP.NET Core except event handlers. Always async Task so framework can await and catch.

8. How to Debug Prod Like Staff

SymptomSource to ReadWhat to Check
All 404sEndpointRoutingMiddleware.csUseRouting order. endpoints.MapControllers() called?
[Authorize] ignoredAuthorizationMiddleware.cs:47context.GetEndpoint() null? Auth before Routing?
Model binding nullDefaultModelBinder.cs:145ValueProvider order. Form vs Query vs Route. [FromBody] needed?
Random data leakServiceProvider.cs:312Singleton holding Scoped? Check lifetimes in Program.cs
View NRE*.g.cs in obj/Model null? Check action returns View(model) not View()
App crashes, no 500ThreadPoolasync void somewhere. Grep for "async void"
You Now Know MVC. Not the API. The machine. If you understood Captive Dependency source, GetEndpoint() null bug, and async void crash, you can debug any prod issue. Go build.
Complete Series: C# Basics → OOP → Collections → LINQ → Async → File I/O → MVC Basics → MVC Advanced → MVC Interview → MVC Practice → You Are Here: Deep Dive. Next: Build real project.

Comments on ASP.NET CORE MVC - Deep Dive (0)

No comments yet. Be the first to share your thoughts!