Skip to content

02 - ASP.NET Core MVC

MVC

Just a reminder

MVC

  • Model
    • Business logic and its persistence
  • View
    • Template for displaying data
  • Controller
    • Communication between Model, View and end-user
  • ViewModel
    • Models used for supplying views with complex (strongly typed) data
    • MVC Viewmodels vs MVVM (Model-View-ViewModel - WPF)

MVVM

Request Pipeline

Pipeline

Simplest aspnet middleware/app

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Run(
    async httpContext =>
    {
        await httpContext.Response.WriteAsync("Hello, World!");
    }
);

app.Run();

Chain multiple request delegates together with Use. The next parameter represents the next delegate in the pipeline.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var rnd = new Random();

app.Use(async (context, next) =>
{
    // Do work that doesn't write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});

app.Use(async (httpContext, next) =>
{
    if (rnd.NextDouble() < 0.5)
    {
        await next.Invoke();
    }
    else
    {
        // short-circuit
        await httpContext.Response.WriteAsync("Shortcut executed");
    }
});

app.Run(
    async httpContext =>
    {
        await httpContext.Response.WriteAsync("Hello, World!");
    }
);

app.Run();

Don't call next.Invoke after the response has been sent to the client (terminal middleware).
Check httpContext.Response.HasStarted!

Branching middleware

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/do-first", HandleDoFirst);
app.Map("/do-second", HandleDoSecond);

app.Run(async context =>
{
    await context.Response.WriteAsync("<h1>Hello from non-Map delegate. </h1>");
});

app.Run();

static void HandleDoFirst(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("First");
    });
}

static void HandleDoSecond(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Second");
    });
}

When Map is used, the matched path segments are removed from HttpRequest.Path and appended to HttpRequest.PathBase for each request.

Pipeline is configured with methods

  • Run – short circuit, return
  • Map - branch
  • Use – Chaining middleware-s together

Custom middleware

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CustomMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        // Call the next delegate/middleware in the pipeline.
        await _next(context);
        // do something
    }
}

// extension method for registering
public static class CustomMiddlewareExtensions
{
    public static IApplicationBuilder UseCustomMiddleware(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<CustomMiddleware>();
    }
}

And inject it into pipeline

1
2
app.UseCustomMiddleware();
app.Run();

NB! Dependncy injection and scoped dependencies

Middleware is singleton, use constructor DI.
For scoped DI use InvokeAsync additional parameters.

Middleware ordering

Middlewares are called in order they are added. So the ordering is important/critical for severel reasons - security, performance, and functionality.

1
2
3
4
5
6
7
8
9
app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                        // thrown in the following middleware modules.

app.UseStaticFiles();                   // Return static files and end pipeline.

app.UseIdentity();                      // Authenticate before you access
                                        // secure resources.

app.UseMvc();                           // Add MVC to the request pipeline.

ASP.NET Middleware

Typical ASP.NET Core MVC pipeline

Pipeline

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// catch errors, redirect to error page
app.UseExceptionHandler("/Error");
//  redirects HTTP requests to HTTPS
app.UseHsts();
app.UseHttpsRedirection();
// returns static files and short-circuits 
app.UseStaticFiles();
// GDPR
app.UseCookiePolicy();
// endpoint routing
app.UseRouting();
// authenticate user
app.UseAuthentication();
// check the rights
app.UseAuthorization();
// maintain session state
app.UseSession();
// mvc routing
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
// razor pages support
app.MapRazorPages();

Endpoint middleware...

Pipeline

Filters in MVC

Filters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Do something before the action executes.
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do something after the action executes.
    }
}

public class AsyncActionFilter : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(
        ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Do something before the action executes.
        await next();
        // Do something after the action executes.
    }
}

Controller

  • Controller – define and group actions (or action method) for servicing incoming web requests.
  • Controller can be any class that ends in “Controller” or inherits from class that ends with “Controller”
  • Convention is (but are not required)
    • Controllers are located in "Controllers" folder
    • Controllers inherit from Microsoft.AspNetCore.Mvc.Controller
  • The Controller is a UI level abstraction. Its responsibility is to ensure incoming request data is valid and to choose which view or result should be returned.
    In well-factored apps it will not directly include data access or business logic, but instead will delegate to services handling these responsibilities.

Inheriting from base Controller gives lots of helpful methods and properties

Most importantly returning various responses

  • View
    • Return View(viewModel);
  • HTTP Status Code
    • Return BadRequest();
  • Formatted Response
    • Return Json(someObject);
  • Content negotiated response
    • Return Ok();
  • Redirect
    • Return RedirectToAction(“Complete”, viewModel);

Routing

Routing middleware is used to match incoming requests to suitable controller and action
Actions are routed by convention or by attribute
Routing middleware (sets up the convention)

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

By "classic" convention, request should take this form
.../SomeController/SomeAction/ID

Multiple routes

1
2
3
4
5
6
7
8
app.MapControllerRoute(
    name: "article",
    pattern: "article/{name}/{id}",
    defaults: new {controller = "Blog", action = "Article"}
    );
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
  • Routes are matched from top-down
  • On first match corresponding controller.action is called

Multiple matching action

When multiple actions match, MVC must select suitable action
Use HTTP verbs to select correct action

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ProductsController : Controller
{
    public IActionResult Edit(int id) { 

    }

    [HttpPost]
    public IActionResult Edit(int id, Product product) { 

    }
}

If not possible to choose, exception is thrown

show form -> submit form -> redirect pattern is common

Routing attributes

With attribute routing, controller and action name have no role!
Typically attribute based routing is used in api programming

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class AttributeRouteController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    public IActionResult MyIndex()
    {
        return View("Index");
    }

    [Route("Home/About")]
    public IActionResult MyAbout()
    {
        return View("About");
    }

    [Route("Home/Contact")]
    public IActionResult MyContact()
    {
        return View("Contact");
    }
}

Route Template

  • { } – define route parameters
    Can specify multiple, must be separated by literal value
    Must have a name, my have additional attributes

  • * - catch all parameter
    blog/{*foo} would match any URI that started with /blog and had any value following it (which would be assigned to the foo route value). 

  • Route parameters may have default values
  • name? – may be optional
  • name:int – use : to specify an route constraint blog/{article:minlength(10)}

Route constraints

Avoid using constraints for input validation, because doing so means that invalid input will result in a 404 (Not Found) instead of a 400 with an appropriate error message.
Route constraints should be used to disambiguate between similar routes, not to validate the inputs for a particular route.

Constraints are
int, bool, datetime, decimal, double, float, guid, long, minlength(value), maxlength(value), length(value), length(min,max), min(value), max(value), range(min, max), alpha, regex(expression), required

Model Binding

  • Maps data from from HTTP request to action parameters
  • Order of binding
    • Form values (POST)
    • Route values
    • Query string (…foo/?bar=fido)

Route: {controller=Home}/{action=Index}/{id?}
request: .../movies/edit/2
public IActionResult Edit(int? id)

Model binding also works on complex types – reflection, recursion – type must have default constructor

Model binding attributes

  • BindRequired – adds an model state error, if property cannot be binded (int Foo for example)
  • BindNever – switch off binder for this parameter

If you want to modify the default binding source order: - FromHeader, FromQuery, FromRoute, FromForm – select source - FromBody – from request body, use formatter based on content type (json, xml)

View scaffolding

Use [ScaffoldColumn(false)] attribute in your model to exclude some property from scaffolding.

Not a proper way to approach this, use correct ViewModels instead (DTO).

Model validation

  • Validation attributes mostly in System.ComponentModel.DataAnnotations
  • [Required], [MaxLength()], etc.
  • Model validation occurs prior to action invocation
  • Action has to inspect ModelState.IsValid
  • If needed, call TryValidateModel(someModel) again

Custom validation, client-side validation, remote validation - it's coming...

ModelState

  • ModelState builds up metadata representation of your viewmodel

  • When repopulating view with data in postback (Edit/Post) ModelState values are used first by tag-helpers/html helper, if not found then values from actual viewmodel. (ie when re-rendering edit view due to validation errors)

  • So, if you need update viewmodel data in Post method, remove responding fields from ModelState
1
2
ModelState.Remove("FirstName");
person.FirstName = "Foo Bar";