.NET 5.0 Web API won't work with record featuring required properties
Asked Answered
G

6

13

I'm using a C# 9.0 record type as a binding model for a .NET 5.0 Web API project. Some of the properties are required.

I'm using the record positional syntax, but am receiving errors.

public record Mail(
    System.Guid? Id,
    [property: Required]
    string From,
    [property: Required]
    string[] Tos,
    [property: Required]
    string Subject,
    string[]? Ccs,
    string[]? Bccs,
    [property: Required]
    Content[] Contents,
    Attachment[]? Attachments
);

This is then exposed as the binding model for my Index action:

public async Task<ActionResult> Index(Service.Models.Mail mailRequest)
{
    …
}

Whenever I try to make a request, however, I receive the following error:

Record type 'Service.Models.Mail' has validation metadata defined on property 'Contents' that will be ignored. 'Contents' is a parameter in the record primary constructor and validation metadata must be associated with the constructor parameter.

I tried removing the attribute on the Contents property, but it then fails for the next (prior) property. I tried using [param: …] instead of [property: …], as well as mixing them, but keep getting the same kind of error.

I looked around the web, and haven't found any suggestion of handling annotations differently for C# 9 records. I did my best, but I'm out of ideas—outside of converting my records to POCOs.

Genera answered 28/1, 2021 at 9:41 Comment(11)
This is an interesting error. I would have expected specifying the [param: …] attribute target to cover it. Out of curiosity, have you tried assigning these as explicit properties on your record, instead of as implicit properties using the positional constructor syntax? I'm not sure if it would make a difference, but given that the error highlights the issue with the constructor, it may be useful to see how that impacts the error. I also assume that you tried this without any attribute target (i.e., removing property: entirely)?Electioneer
@JeremyCaney Using implicit properties decorated with the attributes was what worked for me.Genera
Huh! That’s curious. That seems like a bit of a bug in how the the annotations are handled with records, likely due to the fact that the constructor arguments double as property definitions. Glad to hear you got it resolved.Electioneer
@Genera in a record, all positional properties are required by default. They become part of the constructor, so all of them need to have a value. Putting Required on a property isn't meaningful as there's no way to change a property's value once set. If you want a property to be non-null, you'll have to perform validation on the constructor parameter itselfReedbuck
@PanagiotisKanavos Swashbuckle doesn't recognize them as required then, which was one of my goals.Genera
@Genera that's a different question entirely.Reedbuck
@serge_portima: I’ve deleted the wiki answer since it misrepresented your solution. I’m glad you were able to figure it out, and appreciate you posting the corrected version.Electioneer
@PanagiotisKanavos: It’s also worth noting that it doesn’t really matter which library the OP was targeting with the annotation, since the question was really about how to apply an attribute to the record syntax. It could have been any attribute, for any other purpose, and the question about how to target properties when using the positional syntax would still stand.Electioneer
I have the same issue with the [StringLength] attribute so implicitly [Required] for parameters is just an edge case Record type 'AuthCodeRequest' has validation metadata defined on property 'AuthCode' that will be ignored. 'AuthCode' is a parameter in the record primary constructor and validation metadata must be associated with the constructor parameter. But in my case replacing property with param is working. I don't need swagger though but validation works properly nowSpacious
I think Required just doesn't work, but other attributes such as Range work. Using [property: Range(1,2)] or [property: Required] will fail with the error in the comment above. Using [field: Range(1,2)] or [field: Required] has no impact (as expected). Using [Range(1,2)] or [param: Range(1,2)] works as expected (i.e. the attribute is implicitly required and the range is validated), But using [Required] or [param: Required] has no impact.Birdlime
I've also tried playing with the nullability of the field. With <Nullable>enable</Nullable> in the .csproj, there is a difference of validation for regular classes i.e. properties with get; set; : if declaring the property as string? you get a non-required field, and if declared as string you get a required field. But this doesn't work with records constructed with positional parameters.Birdlime
G
3

I gave up using Positional constructor, and with the verbose full declaration of the properties, it works.

public record Mail
{
    public System.Guid? Id { get; init; }

    [Required]
    public string From { get; init; }

    [Required]
    public string[] Tos { get; init; }

    [Required]
    public string Subject { get; init; }

    public string[]? Ccs { get; init; }

    public string[]? Bccs { get; init; }

    [Required]
    public Content[] Contents { get; init; }

    public Attachment[]? Attachments { get; init; }

    public Status? Status { get; init; }

    public Mail(Guid? id, string @from, string[] tos, string subject, string[]? ccs, string[]? bccs, Content[] contents, Attachment[]? attachments, Status status)
    {
        Id = id;
        From = @from;
        Tos = tos;
        Subject = subject;
        Ccs = ccs;
        Bccs = bccs;
        Contents = contents;
        Attachments = attachments;
        Status = status;
    }
}
Genera answered 8/2, 2021 at 9:24 Comment(1)
For me even this is not working. I need to use class insteadAnesthetic
U
3

I found something similar on ASP.NET Core Razor pages getting:

InvalidOperationException: Record type 'WebApplication1.Pages.LoginModelNRTB+InputModel' has validation metadata defined on property 'PasswordB' that will be ignored. 'PasswordB' is a parameter in the record primary constructor and validation metadata must be associated with the constructor parameter.

from

Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata.ThrowIfRecordTypeHasValidationOnProperties()

After some digging, I found: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ModelBinding/Validation/DefaultComplexObjectValidationStrategy.cs

So maybe as you've done, the verbose declaration is the way forward.

Positional record attributes in ASP.NET Core background

How do I target attributes for a record class? more background

Unbeaten answered 9/2, 2021 at 17:26 Comment(0)
B
2

Using FluentValidation and keeping properties with the full declaration seems to work perfectly in my case. I highly recommend trying this highly polished alternative validation library instead of using the pretty old standard data annotations

    public record LoginViewModel
    {
        public string Mail { get; init; }
        public string Password { get; init; }
        public bool IsPersistent { get; init; }
    }

    public class LoginValidator : AbstractValidator<LoginViewModel>
    {
        public LoginValidator()
        {
            RuleFor(l => l.Mail).NotEmpty().EmailAddress();
            RuleFor(l => l.Password).NotEmpty();
        }
    }
Burgonet answered 15/9, 2021 at 3:4 Comment(0)
I
2

Try using only [Required] (instead of [property: Required]), for some reason worked for me

Importunate answered 25/1, 2022 at 13:46 Comment(1)
That's completely expected and unsurprising, and is consistent with what was already posted as the top-voted answer from the OP. The question is how one would apply attributes to just the property (or, for that matter, just the method parameter) using the positional syntax. The expected method would be to use [property: Required], but that doesn't seem to work. Applying [Required] sidesteps the requirement entirely.Electioneer
H
1

For me it started to work by adding the [ApiController] attribute to the controller.

Hilleary answered 19/5, 2022 at 10:46 Comment(0)
B
0

Although not ideal, creating a parameterless constructor gets around the issue in my scenario:

public record Mail(
    System.Guid? Id,
    [property: Required]
    string From,
    [property: Required]
    string[] Tos,
    [property: Required]
    string Subject,
    string[]? Ccs,
    string[]? Bccs,
    [property: Required]
    Content[] Contents,
    Attachment[]? Attachments
)
{
    public Mail() : this(null, string.Empty, Array.Empty<string>(), string.Empty, null, null, Array.Empty<Content>(), null) { }
}

This stops the validation metadata error and still validates.

Belita answered 16/3, 2023 at 11:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.