ASP.NET-core-pyynnön ja vastausputken ymmärtäminen - Osa 5: MVC, Razor-sivut ja minimaaliset rajapinnat (Suomi (Finnish))

ASP.NET-core-pyynnön ja vastausputken ymmärtäminen - Osa 5: MVC, Razor-sivut ja minimaaliset rajapinnat

Sunday, 09 November 2025

//

16 minute read

Johdanto

Olemme kulkeneet hosting-kerroksen, keskiohjelmistoputken ja reititysjärjestelmän läpi. Nyt saavumme sovelluskerrokseen, jossa liiketoimintalogiikkasi elää. ASP.NET Core tarjoaa kolme ensisijaista ohjelmointimallia verkkosovellusten rakentamiseen:

  1. MVC (Model-View-Controller) - Rakenteellinen malli monimutkaisten verkkosovellusten ja sovellusliittymien rakentamiseen
  2. Razor-sivut - Sivupohjainen malli, joka yksinkertaistaa web UI:n rakentamista
  3. Minimirajapinnat - Kevyt lähestymistapa HTTP-rajapintojen rakentamiseen minimaalisella seremonialla

Jokaisessa mallissa on sama reititys- ja pääteinfrastruktuuri, jota tutkimme osassa 4, mutta ne tarjoavat erilaisia kehittäjäkokemuksia ja ovat optimoituja erilaisiin skenaarioihin. Tässä osassa tutkimme, miten kukin malli käsittelee pyyntöjä ja tuottaa vastauksia.

HUOMAUTUS: Tämä on osa kokeilujani tekoälyllä / tapa käyttää 1000 dollaria Calude Code Web -arvosanoja. Olen syöttänyt tälle BUNCH-lehden, ymmärrykseni, kysymyksiä, joita jouduin tekemään tämän artikkelin. Se on hauska ja täyttää aukon, jota en ole nähnyt paikattavan missään muualla.

Sovellusmallin taajuus

graph LR
    A[Minimal APIs] --> B[Razor Pages] --> C[MVC]



    note1[Lightweight<br/>Simple APIs]
    note2[Page-focused<br/>Web UI]
    note3[Full-featured<br/>Complex apps]

    A -.-> note1
    B -.-> note2
    C -.-> note3

Milloin kutakin lääkettä käytetään:

  • Minimirajapinnat: Mikropalvelut, yksinkertaiset sovellusrajapinnat, palvelinkyvyttömät toiminnot, ASP.NET Core -oppiminen
  • Razor-sivut: Sivupainotteiset verkkosovellukset, CRUD-toiminnot, formaatit raskaita sovelluksia
  • MVC: Monimutkaiset verkkosovellukset, monimutkaisella reitityksellä varustetut sovellusrajapinnat, huolet selkeästi erotettavat sovellukset

Kaikki kolme jakavat saman putken, mutta ovat eri mieltä siitä, miten he järjestävät koodin ja käsittelevät pyyntöjä.

Minimirajapinnat: Kevytlähestyminen

Minimaaliset API-rajapinnat otettiin käyttöön .NET 6:ssa API-kehityksen yksinkertaistamiseksi. Ne vähentävät seremonia- ja kattilalaattaa, mikä nopeuttaa HTTP-rajapintojen rakentamista.

Perusrakenne

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddSingleton<IDataService, DataService>();

var app = builder.Build();

// Define endpoints
app.MapGet("/", () => "Hello World!");

app.MapGet("/users/{id}", (int id) => new User(id, "John Doe"));

app.MapPost("/users", (User user) =>
{
    // Save user
    return Results.Created($"/users/{user.Id}", user);
});

app.Run();

record User(int Id, string Name);

Pyyntöjen käsittelyvirta

sequenceDiagram
    participant Client
    participant Routing
    participant Binding as Parameter Binding
    participant Filter as Endpoint Filters
    participant Handler as Route Handler
    participant Result as Result Execution

    Client->>Routing: HTTP Request
    Routing->>Routing: Match route pattern
    Routing->>Binding: Extract parameters

    Note over Binding: Bind from:<br/>- Route values<br/>- Query string<br/>- Headers<br/>- Body<br/>- Services

    Binding->>Filter: Execute filters (if any)

    Filter->>Handler: Invoke handler delegate

    Note over Handler: Execute your code

    Handler->>Result: Return IResult or object

    Result->>Result: Execute result<br/>(JSON, file, redirect, etc.)

    Result->>Client: HTTP Response

Parametrin sidonta yksityiskohdissa

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IEmailService, EmailService>();
var app = builder.Build();

// Simple types from route
app.MapGet("/users/{id}", (int id) =>
    new { userId = id });

// Multiple sources
app.MapGet("/search",
    (string q,                                    // From query string
     int page = 1,                               // From query string with default
     [FromHeader(Name = "X-API-Key")] string apiKey,  // From header
     [FromServices] IEmailService emailService)  // From DI container
    =>
{
    return new
    {
        Query = q,
        Page = page,
        ApiKey = apiKey,
        EmailServiceType = emailService.GetType().Name
    };
});

// Complex type from body
app.MapPost("/users",
    ([FromBody] CreateUserRequest request,
     [FromServices] IUserService userService,
     HttpContext context) =>
{
    var user = userService.CreateUser(request);
    return Results.Created($"/users/{user.Id}", user);
});

// Custom binding with AsParameters
app.MapGet("/products", ([AsParameters] ProductQuery query) =>
{
    // All parameters bundled in one object
    return new { query.Category, query.MinPrice, query.MaxPrice };
});

app.Run();

record CreateUserRequest(string Name, string Email);

// AsParameters allows grouping parameters
record ProductQuery(string? Category, decimal MinPrice = 0, decimal MaxPrice = 1000);

interface IEmailService { }
class EmailService : IEmailService { }
interface IUserService
{
    User CreateUser(CreateUserRequest request);
}
class User
{
    public int Id { get; set; }
}

Sitovat lähteet visualisoivat:

graph TD
    Request[HTTP Request] --> Route[Route Values<br/>users/123]
    Request --> Query[Query String<br/>?page=1&size=10]
    Request --> Headers[Headers<br/>X-API-Key: abc123]
    Request --> Body[Request Body<br/>JSON/XML]
    Request --> Services[DI Container<br/>Registered Services]

    Route --> Handler[Handler Method]
    Query --> Handler
    Headers --> Handler
    Body --> Handler
    Services --> Handler

    style Request stroke:#ef4444,stroke-width:3px
    style Handler stroke:#10b981,stroke-width:3px

Tulokset ja vastaukset

Minimirajapinnat tukevat useita vastaustyyppejä:

var app = WebApplication.CreateBuilder().Build();

// 1. Direct return - serialized as JSON
app.MapGet("/users/{id}", (int id) =>
    new User(id, "John Doe", "[email protected]"));

// 2. IResult - typed results
app.MapGet("/ok", () => Results.Ok(new { status = "OK" }));
app.MapGet("/created", () => Results.Created("/users/1", new User(1, "Jane", "[email protected]")));
app.MapGet("/notfound", () => Results.NotFound());
app.MapGet("/redirect", () => Results.Redirect("/new-location"));
app.MapGet("/file", () => Results.File("file.pdf", "application/pdf"));

// 3. Typed results with Results<T1, T2, ...>
app.MapGet("/typed/{id}", Results<Ok<User>, NotFound> (int id) =>
{
    var user = GetUser(id);
    return user != null
        ? TypedResults.Ok(user)
        : TypedResults.NotFound();
});

// 4. Custom response
app.MapGet("/custom", (HttpContext context) =>
{
    context.Response.StatusCode = 200;
    context.Response.Headers["X-Custom-Header"] = "Value";
    return context.Response.WriteAsync("Custom response");
});

// 5. Stream response
app.MapGet("/stream", async (HttpContext context) =>
{
    context.Response.ContentType = "text/plain";
    await context.Response.StartAsync();

    for (int i = 0; i < 10; i++)
    {
        await context.Response.WriteAsync($"Line {i}\n");
        await context.Response.Body.FlushAsync();
        await Task.Delay(100);
    }
});

app.Run();

record User(int Id, string Name, string Email);

User? GetUser(int id) => id == 1 ? new User(1, "John", "[email protected]") : null;

Päätepistesuotimet

Suodattimien avulla voit suorittaa koodin ennen ja jälkeen käsittelijän:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Global filter using middleware
app.Use(async (context, next) =>
{
    Console.WriteLine($"Before: {context.Request.Path}");
    await next(context);
    Console.WriteLine($"After: {context.Response.StatusCode}");
});

// Endpoint-specific filter
app.MapGet("/filtered", () => "Hello")
    .AddEndpointFilter(async (context, next) =>
    {
        Console.WriteLine("Before handler");

        var result = await next(context);

        Console.WriteLine("After handler");

        return result;
    });

// Filter with logic
app.MapGet("/validated", (int age) => $"Age: {age}")
    .AddEndpointFilter(async (context, next) =>
    {
        var age = context.GetArgument<int>(0);

        if (age < 0)
        {
            return Results.BadRequest("Age cannot be negative");
        }

        return await next(context);
    });

// Reusable filter
app.MapGet("/timed", () =>
{
    Thread.Sleep(100);
    return "Done";
})
.AddEndpointFilter<TimingFilter>();

app.Run();

// Reusable filter class
class TimingFilter : IEndpointFilter
{
    private readonly ILogger<TimingFilter> _logger;

    public TimingFilter(ILogger<TimingFilter> logger)
    {
        _logger = logger;
    }

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var sw = Stopwatch.StartNew();

        var result = await next(context);

        sw.Stop();
        _logger.LogInformation(
            "Endpoint {Endpoint} took {Elapsed}ms",
            context.HttpContext.Request.Path,
            sw.ElapsedMilliseconds);

        return result;
    }
}

Suodatinputki:

graph LR
    A[Request] --> B[Filter 1 Before]
    B --> C[Filter 2 Before]
    C --> D[Handler]
    D --> E[Filter 2 After]
    E --> F[Filter 1 After]
    F --> G[Response]


Razor-sivut: Page-based Web UI

Razor Pages tarjoaa sivupainotteisen mallin, joka tekee web UI:n rakentamisesta yksinkertaisempaa kuin MVC:n monista skenaarioista.

Rakenne

Pages/
  _ViewImports.cshtml
  _ViewStart.cshtml
  Index.cshtml
  Index.cshtml.cs
  Products/
    List.cshtml
    List.cshtml.cs
    Details.cshtml
    Details.cshtml.cs

Razorin perussivu

Sivut/Index.cshtml:

@page
@model IndexModel

<h1>Welcome</h1>
<p>Current time: @Model.CurrentTime</p>

<form method="post">
    <input type="text" asp-for="Name" />
    <button type="submit">Submit</button>
</form>

@if (!string.IsNullOrEmpty(Model.Message))
{
    <p>@Model.Message</p>
}

Sivut/Index.cshtml.cs:

public class IndexModel : PageModel
{
    [BindProperty]
    public string Name { get; set; } = "";

    public string CurrentTime { get; set; } = "";
    public string Message { get; set; } = "";

    public void OnGet()
    {
        CurrentTime = DateTime.Now.ToString("HH:mm:ss");
    }

    public IActionResult OnPost()
    {
        if (string.IsNullOrWhiteSpace(Name))
        {
            Message = "Please enter a name";
            return Page();
        }

        Message = $"Hello, {Name}!";
        return Page();
    }
}

Pyyntöjen käsittelyvirta

sequenceDiagram
    participant Client
    participant Routing
    participant PageModel as Page Model
    participant Binding as Model Binding
    participant Handler as Handler Method
    participant Filters as Page Filters
    participant View as Razor View

    Client->>Routing: GET /products/details?id=123
    Routing->>PageModel: Create PageModel instance
    PageModel->>Binding: Bind parameters

    Note over Binding: Bind from:<br/>- Route data<br/>- Query string<br/>- Form data<br/>- Services

    Binding->>Filters: Execute filters (OnPageHandlerExecuting)
    Filters->>Handler: Call OnGet/OnPost
    Handler->>Handler: Execute logic<br/>Load data
    Handler->>Filters: Return IActionResult
    Filters->>Filters: OnPageHandlerExecuted
    Filters->>View: Render Razor view
    View->>View: Execute Razor syntax<br/>Generate HTML
    View->>Client: HTML Response

Reittimuuttujat

// Pages/Products/Details.cshtml.cs
@page "{id:int}"
@model DetailsModel

public class DetailsModel : PageModel
{
    private readonly IProductService _productService;

    public DetailsModel(IProductService productService)
    {
        _productService = productService;
    }

    public Product? Product { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Product = await _productService.GetProductAsync(id);

        if (Product == null)
        {
            return NotFound();
        }

        return Page();
    }
}

URL: /products/details/123id = 123

Useita käsittelijöitä

public class ContactModel : PageModel
{
    [BindProperty]
    public ContactForm Form { get; set; } = new();

    public string Message { get; set; } = "";

    // Default GET handler
    public void OnGet()
    {
    }

    // Default POST handler
    public IActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Send email
        Message = "Message sent!";
        return Page();
    }

    // Named handler: POST with ?handler=subscribe
    public IActionResult OnPostSubscribe(string email)
    {
        // Subscribe to newsletter
        Message = $"Subscribed: {email}";
        return Page();
    }

    // Named handler: POST with ?handler=unsubscribe
    public IActionResult OnPostUnsubscribe(string email)
    {
        // Unsubscribe
        Message = $"Unsubscribed: {email}";
        return Page();
    }
}

public class ContactForm
{
    [Required]
    public string Name { get; set; } = "";

    [Required, EmailAddress]
    public string Email { get; set; } = "";

    [Required]
    public string Message { get; set; } = "";
}

Käyttö Razorissa:

@page
@model ContactModel

<!-- Default handler (OnPost) -->
<form method="post">
    <input asp-for="Form.Name" />
    <input asp-for="Form.Email" />
    <textarea asp-for="Form.Message"></textarea>
    <button type="submit">Send</button>
</form>

<!-- Named handler (OnPostSubscribe) -->
<form method="post" asp-page-handler="Subscribe">
    <input type="email" name="email" />
    <button type="submit">Subscribe</button>
</form>

<!-- Named handler (OnPostUnsubscribe) -->
<form method="post" asp-page-handler="Unsubscribe">
    <input type="email" name="email" />
    <button type="submit">Unsubscribe</button>
</form>

Sivun suotimet

// Global page filter
public class LoggingPageFilter : IPageFilter
{
    private readonly ILogger<LoggingPageFilter> _logger;

    public LoggingPageFilter(ILogger<LoggingPageFilter> logger)
    {
        _logger = logger;
    }

    public void OnPageHandlerSelected(PageHandlerSelectedContext context)
    {
        _logger.LogInformation(
            "Page handler selected: {HandlerMethod}",
            context.HandlerMethod?.Name);
    }

    public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
    {
        _logger.LogInformation("Page handler executing");
    }

    public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
    {
        _logger.LogInformation(
            "Page handler executed, result: {Result}",
            context.Result?.GetType().Name);
    }
}

// Register globally
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages(options =>
{
    options.Conventions.ConfigureFilter(new LoggingPageFilter(
        LoggerFactory.Create(b => b.AddConsole()).CreateLogger<LoggingPageFilter>()
    ));
});

var app = builder.Build();

MVC: Täydellinen kehys

MVC (Model-View-Controller) tarjoaa strukturoidun kuvion monimutkaisten sovellusten rakentamiseen ja erottaa huolet selkeästi toisistaan.

Rakenne

Controllers/
  HomeController.cs
  ProductsController.cs
  ApiController.cs
Models/
  Product.cs
  CreateProductViewModel.cs
Views/
  Home/
    Index.cshtml
    About.cshtml
  Products/
    List.cshtml
    Details.cshtml
  Shared/
    _Layout.cshtml
    _ValidationScriptsPartial.cshtml

MVC-pyynnön käsittelyvirta

sequenceDiagram
    participant Client
    participant Routing
    participant Controller as Controller Factory
    participant Filters
    participant ModelBinding as Model Binding
    participant Action as Action Method
    participant Result as Action Result
    participant View

    Client->>Routing: GET /products/details/123
    Routing->>Controller: Create controller instance
    Controller->>Filters: Authorization filters

    alt Not authorized
        Filters-->>Client: 401/403
    end

    Filters->>Filters: Resource filters
    Filters->>ModelBinding: Bind parameters

    Note over ModelBinding: Bind from:<br/>- Route values<br/>- Query string<br/>- Form data<br/>- Body<br/>- Services

    ModelBinding->>Filters: Action filters (OnActionExecuting)
    Filters->>Action: Invoke action method
    Action->>Action: Execute business logic
    Action->>Result: Return IActionResult
    Result->>Filters: Action filters (OnActionExecuted)
    Filters->>Filters: Result filters (OnResultExecuting)
    Filters->>View: Execute result (render view)
    View->>Filters: Result filters (OnResultExecuted)
    Filters->>Filters: Resource filters (OnResourceExecuted)
    Filters->>Client: HTML/JSON Response

Perusohjain

public class ProductsController : Controller
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(
        IProductService productService,
        ILogger<ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

    // GET: /products
    public async Task<IActionResult> Index()
    {
        var products = await _productService.GetAllProductsAsync();
        return View(products);
    }

    // GET: /products/details/5
    public async Task<IActionResult> Details(int id)
    {
        var product = await _productService.GetProductAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        return View(product);
    }

    // GET: /products/create
    public IActionResult Create()
    {
        return View();
    }

    // POST: /products/create
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(CreateProductViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var product = await _productService.CreateProductAsync(model);

        return RedirectToAction(nameof(Details), new { id = product.Id });
    }

    // DELETE: /products/delete/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Delete(int id)
    {
        await _productService.DeleteProductAsync(id);

        return RedirectToAction(nameof(Index));
    }
}

Toimintojen tulokset

public class ResultsController : Controller
{
    // View result
    public IActionResult Index()
    {
        return View(); // Views/Results/Index.cshtml
    }

    // View with model
    public IActionResult Details(int id)
    {
        var model = new Product { Id = id, Name = "Product" };
        return View(model);
    }

    // Specific view
    public IActionResult Custom()
    {
        return View("CustomViewName");
    }

    // JSON result
    public IActionResult GetJson()
    {
        return Json(new { id = 1, name = "Product" });
    }

    // Redirect
    public IActionResult RedirectSample()
    {
        return RedirectToAction("Index", "Home");
    }

    // Redirect to route
    public IActionResult RedirectToRouteSample()
    {
        return RedirectToRoute("Default", new { controller = "Home", action = "Index" });
    }

    // Content result
    public IActionResult GetText()
    {
        return Content("Plain text response", "text/plain");
    }

    // File result
    public IActionResult DownloadFile()
    {
        var bytes = System.IO.File.ReadAllBytes("file.pdf");
        return File(bytes, "application/pdf", "download.pdf");
    }

    // Status code result
    public IActionResult StatusCodeSample()
    {
        return StatusCode(503); // Service Unavailable
    }

    // Not found
    public IActionResult NotFoundSample()
    {
        return NotFound(); // 404
    }

    // Bad request
    public IActionResult BadRequestSample()
    {
        return BadRequest("Invalid input"); // 400
    }

    // Unauthorized
    public IActionResult UnauthorizedSample()
    {
        return Unauthorized(); // 401
    }
}

Mallin sidonta

public class BindingController : Controller
{
    // Simple parameter from route
    // Route: products/{id}
    public IActionResult Get(int id)
    {
        return Ok(new { id });
    }

    // Multiple parameters from route and query
    // Route: products/{id}?includeDetails=true
    public IActionResult GetWithDetails(int id, bool includeDetails = false)
    {
        return Ok(new { id, includeDetails });
    }

    // Complex type from form
    [HttpPost]
    public IActionResult CreateFromForm([FromForm] Product product)
    {
        return Ok(product);
    }

    // Complex type from body (JSON)
    [HttpPost]
    public IActionResult CreateFromBody([FromBody] Product product)
    {
        return Ok(product);
    }

    // From header
    public IActionResult HeaderSample([FromHeader(Name = "X-API-Key")] string apiKey)
    {
        return Ok(new { apiKey });
    }

    // From services
    public IActionResult ServiceSample([FromServices] IProductService productService)
    {
        var count = productService.GetCount();
        return Ok(new { count });
    }

    // Custom binding
    public IActionResult CustomBinding([ModelBinder(typeof(CustomBinder))] CustomModel model)
    {
        return Ok(model);
    }
}

// Custom model binder
public class CustomBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue("custom").FirstValue;

        var model = new CustomModel { Value = value?.ToUpper() ?? "" };

        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

public class CustomModel
{
    public string Value { get; set; } = "";
}

Toimintasuodattimet

Suodattimet suorittavat koodin eri kohdista pyyntöputkessa:

graph TD
    A[Request] --> B[Authorization Filters]
    B --> C{Authorized?}
    C -->|No| D[Challenge Result]
    C -->|Yes| E[Resource Filters<br/>OnResourceExecuting]
    E --> F[Model Binding]
    F --> G[Action Filters<br/>OnActionExecuting]
    G --> H[Action Method]
    H --> I[Action Filters<br/>OnActionExecuted]
    I --> J[Exception Filters<br/>if exception]
    J --> K[Result Filters<br/>OnResultExecuting]
    K --> L[Action Result]
    L --> M[Result Filters<br/>OnResultExecuted]
    M --> N[Resource Filters<br/>OnResourceExecuted]
    N --> O[Response]
    D --> O

    style H stroke:#ef4444,stroke-width:3px
    style L stroke:#10b981,stroke-width:3px

Suodatintyypit:

// 1. Authorization filter
public class CustomAuthorizeFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity?.IsAuthenticated ?? true)
        {
            context.Result = new UnauthorizedResult();
        }
    }
}

// 2. Resource filter
public class CacheResourceFilter : IResourceFilter
{
    private static readonly Dictionary<string, object> _cache = new();

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var cacheKey = context.HttpContext.Request.Path;

        if (_cache.TryGetValue(cacheKey, out var cachedResult))
        {
            context.Result = (IActionResult)cachedResult;
            // Short-circuit the pipeline
        }
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        var cacheKey = context.HttpContext.Request.Path;

        if (context.Result != null)
        {
            _cache[cacheKey] = context.Result;
        }
    }
}

// 3. Action filter
public class LogActionFilter : IActionFilter
{
    private readonly ILogger<LogActionFilter> _logger;

    public LogActionFilter(ILogger<LogActionFilter> logger)
    {
        _logger = logger;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _logger.LogInformation(
            "Executing action {Action} with arguments: {Arguments}",
            context.ActionDescriptor.DisplayName,
            string.Join(", ", context.ActionArguments.Select(kvp => $"{kvp.Key}={kvp.Value}")));
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation(
            "Executed action {Action}, result: {Result}",
            context.ActionDescriptor.DisplayName,
            context.Result?.GetType().Name);
    }
}

// 4. Exception filter
public class CustomExceptionFilter : IExceptionFilter
{
    public void OnException(ExceptionContext context)
    {
        if (context.Exception is ArgumentException)
        {
            context.Result = new BadRequestObjectResult(new
            {
                error = context.Exception.Message
            });

            context.ExceptionHandled = true;
        }
    }
}

// 5. Result filter
public class HeaderResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        context.HttpContext.Response.Headers["X-Custom-Header"] = "Value";
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // After result execution
    }
}

// Register filters
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    // Global filters
    options.Filters.Add<LogActionFilter>();
    options.Filters.Add<CustomExceptionFilter>();
});

// Or apply to specific controllers/actions
[ServiceFilter(typeof(LogActionFilter))]
public class ProductsController : Controller
{
    [TypeFilter(typeof(CacheResourceFilter))]
    public IActionResult Index()
    {
        return View();
    }
}

API-ohjaimet

[ApiController]
[Route("api/[controller]")]
public class ProductsApiController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsApiController(IProductService productService)
    {
        _productService = productService;
    }

    // GET: api/products
    [HttpGet]
    [ProducesResponseType(typeof(IEnumerable<Product>), 200)]
    public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
    {
        var products = await _productService.GetAllProductsAsync();
        return Ok(products);
    }

    // GET: api/products/5
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(Product), 200)]
    [ProducesResponseType(404)]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        var product = await _productService.GetProductAsync(id);

        if (product == null)
        {
            return NotFound();
        }

        return Ok(product);
    }

    // POST: api/products
    [HttpPost]
    [ProducesResponseType(typeof(Product), 201)]
    [ProducesResponseType(400)]
    public async Task<ActionResult<Product>> CreateProduct(CreateProductDto dto)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var product = await _productService.CreateProductAsync(dto);

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    // PUT: api/products/5
    [HttpPut("{id}")]
    [ProducesResponseType(204)]
    [ProducesResponseType(400)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> UpdateProduct(int id, UpdateProductDto dto)
    {
        if (id != dto.Id)
        {
            return BadRequest();
        }

        var exists = await _productService.ExistsAsync(id);
        if (!exists)
        {
            return NotFound();
        }

        await _productService.UpdateProductAsync(dto);

        return NoContent();
    }

    // DELETE: api/products/5
    [HttpDelete("{id}")]
    [ProducesResponseType(204)]
    [ProducesResponseType(404)]
    public async Task<IActionResult> DeleteProduct(int id)
    {
        var exists = await _productService.ExistsAsync(id);
        if (!exists)
        {
            return NotFound();
        }

        await _productService.DeleteProductAsync(id);

        return NoContent();
    }
}

Kolmen mallin vertaaminen

graph TB
    subgraph "Minimal APIs"
        MA1[Direct Route Registration]
        MA2[Lambda Handlers]
        MA3[Endpoint Filters]
        MA4[Simple Binding]

        MA1 --> MA2
        MA2 --> MA3
        MA3 --> MA4
    end

    subgraph "Razor Pages"
        RP1[Page-Based Routing]
        RP2[PageModel Handlers]
        RP3[Page Filters]
        RP4[Razor View Engine]

        RP1 --> RP2
        RP2 --> RP3
        RP3 --> RP4
    end

    subgraph "MVC"
        MVC1[Controller-Based Routing]
        MVC2[Action Methods]
        MVC3[Multiple Filter Types]
        MVC4[View/JSON Results]

        MVC1 --> MVC2
        MVC2 --> MVC3
        MVC3 --> MVC4
    end

    style MA1 stroke:#10b981,stroke-width:3px
    style RP1 stroke:#ef4444,stroke-width:3px
    style MVC1 stroke:#6366f1,stroke-width:3px

Ominaisuus Minimirajapinnat Razor-sivut MVC-sivut |---------|--------------|-------------|-----| | Monimutkaisuus Keskikokoinen, korkea | Keittolevy Minimaalinen ja maltillinen. Merkityksellistä. | Paras API-rajapinnat, mikropalvelut Sivupainotteiset web-sovellukset Monimutkaiset sovellukset | Rottaus "Inline-yleissopimus + ominaisuudet " Convention + attribuutit" | Huolien erottaminen Keskikokoinen, korkea | Testattavuus Hyvä on, hyvä on erinomainen. | Suorituskyky Nopein nopein | Oppimiskäyrä Helpointa ja maltillista

Tärkeimmät takeawayt

  • Minimirajapinnat tarjota kevyen, matalan seremonian tavan rakentaa HTTP-rajapintoja
  • Razor-sivut yksinkertaistaa sivupainotteista verkkokehitystä sivukeskeisellä mallilla
  • MVC tarjoaa täydet puitteet, joissa huolet on selkeästi erotettu toisistaan
  • Kaikki kolme mallia käyttävät samaa reititys- ja päätetapahtumainfrastruktuuria
  • Parametrin sidonta toimii samalla tavalla kaikissa malleissa
  • Suodattimet aiheuttavat laaja-alaisia huolia eri putkivaiheissa
  • Valitse malli sovelluksesi monimutkaisuuden ja vaatimusten perusteella
  • Voit sekoittaa mallit samaan sovellukseen

Jokainen ohjelmamalli tarjoaa erilaisen kehittäjäkokemuksen samalla kun se hyödyntää samaa tehokasta ASP.NET Core -putkea. Kaikkien kolmen ymmärtäminen auttaa valitsemaan oikean työkalun kuhunkin skenaarioon.


Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.