Search code examples
c#asp.net-corerecordnullable-reference-types

Positional record attributes in ASP.NET Core


Thank you to the reviewers for finding the duplicate: How do I target attributes for a record class?

and .NET 5.0 Web API won't work with record featuring required properties is related too.

I'm trying to understand why 3. Positional record attributes don't work below.

It is a new ASP.NET Core Razor Pages Web App with source here https://github.com/djhmateer/record-test

https://daveabrock.com/2020/11/18/simplify-api-models-with-records seems to have got it working with an API.

public class LoginModelNRTB : PageModel
{
    [BindProperty]
    // there will never be an InputModel on get
    // but if we set to nullable InputModel then the cshtml will produce dereference warnings
    // https://stackoverflow.com/a/54973095/26086
    // so this will get rid of the warnings as we are happy we will never get dereferences on the front
    // ie we are happy the underlying framework will not produce null reference exceptions
    public InputModel Input { get; set; } = null!;


    // 1. Original Class which works
    //public class InputModel
    //{
    //    // don't need required as Email property is non nullable
    //    //[Required]
    //    // makes sure a regex fires to be in the correct email address form
    //    [EmailAddress]
    //    // there should always be an email posted, but maybe null if js validator doesn't fire
    //    // we are happy the underlying framework handles it, 
    //    public string Email { get; set; } = null!;

    //    // When the property is called Password we don't need a [DataType(DataType.Password)]
    //    //[DataType(DataType.Password)]
    //    public string Password { get; set; } = null!;

    //    [Display(Name = "Remember me?")]
    //    public bool RememberMe { get; set; }
    //}

    // 2. Record which works
    //public record InputModel
    //{
    //    [EmailAddress]
    //    public string Email { get; init; } = null!;

    //    [DataType(DataType.Password)]
    //    public string PasswordB { get; init; } = null!;

    //    [Display(Name = "Remember me?")]
    //    public bool RememberMe { get; init; }
    //}

    // 3. Positional record attributes not being picked up
    public record InputModel(
        string Email,
        [DataType(DataType.Password)] string PasswordB,
        [Display(Name = "Remember me?")] bool RememberMe);


    public void OnGet() { }

    public IActionResult OnPost()
    {
        if (ModelState.IsValid)
        {
            // Input property of type InputModel is bound because of the [BindProperty] attribute
            Log.Information($"Success! {Input}");
            return LocalRedirect("/");
        }

        Log.Information($"Failure on ModelState validation {Input}");
        return Page();
    }
}


Solution

  • From the proposal specification:

    Attributes can be applied to the synthesized auto-property and its backing field by using property: or field: targets for attributes syntactically applied to the corresponding record parameter.

    Try next:

    public record InputModel(
        string Email,
        [property:DataType(DataType.Password)] string PasswordB,
        [property:Display(Name = "Remember me?")] bool RememberMe);
    

    Based on sharplab.io decompilation, the attributes should be emitted for generated properties.

    Without this target the attribute gets emitted as attribute on corresponding record constructor parameter.