Wednesday, July 6, 2022

.NET 7 Preview 5: Using [AsParameters] Attribute for Parameter Binding in Minimal APIs

In this post let's go through this new attribute [AsParameters] that got introduced in .NET 7 Preview 5. This attribute is especially for Minimal APIs in ASP.NET Core.

Let's consider the following endpoint using Minimal APIs.

app.MapGet("ToDos"async (ToDoDbContext dbContextint offsetint limit, ILogger<Program> logger) =>
{
    logger.LogInformation("Getting ToDos with Offset: '{Offset}', Limit: '{Limit}'",
        offset,
        limit);
 
    List<ToDo> todos = await dbContext.ToDos
        .OrderBy(x => x.Id)
        .Skip(offset)
        .Take(limit)
        .ToListAsync();
 
    return TypedResults.Ok(todos);
});

With the new [AsParameters] attribute, we can move all the parameters into a POCO type.

app.MapGet("ToDos/WithRequest"async ([AsParameters] GetRequest request) =>
{
    request.Logger.LogInformation("Getting ToDos with Offset: '{Offset}', Limit: '{Limit}'",
        request.Offset,
        request.Limit);
 
    List<ToDo> todos = await request.DbContext.ToDos
        .OrderBy(x => x.Id)
        .Skip(request.Offset)
        .Take(request.Limit)
        .ToListAsync();
 
    return TypedResults.Ok(todos);
});
 
record struct GetRequest(ToDoDbContext DbContext    int Offset    int Limit, 
    ILogger<GetRequest> Logger);

Here I have used a record struct, but classes are also supported. The recommendation is to use record struct to avoid additional memory allocation.

And on top of that, we can apply other binding attributes (FromHeaderFromQuery, FromServices, etc.), something like below.

record struct GetRequest(ToDoDbContext DbContext,
    int Offset,
    int Limit,
    [FromHeader(Name = "X-OrganizationId")] string? OrganizationId,
    ILogger<GetRequest> Logger);

Here the OrganizationId will get picked from HTTP Request Header (if present). 

The following rules are applied during the parameter binding.

Structs

  • A declared public parameterless constructor will always be used if present.
  • A public parameterized constructor will be use if a single constructor is present and all arguments have a matching (case-insensitive) public property.
    • If a constructor parameter does not match with a property, InvalidOperationException will be thrown if binding is attempted.
  • Since struct always has a default constructor, the default constructor will be used if it is the only one present or more than one parameterized constructor is present.
  • When binding using a parameterless constructor all public settable properties will be bound.
Classes
  • A public parameterless constructor will be used if present.
  • A public parameterized constructor will be used if a single constructor is present and all arguments have a matching (case-insensitive) public property.
    • If a constructor parameter does not match with a property, InvalidOperationException will be thrown if binding is attempted.
  • Throw InvalidOperationException when more than one parameter is declared and the parameterless constructor is not present.
  • Throw InvalidOperationException if a suitable constructor cannot be found.

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment