10

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.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
serge_portima
  • 111
  • 1
  • 10
  • 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)? – Jeremy Caney Feb 05 '21 at 21:53
  • 1
    @JeremyCaney Using implicit properties decorated with the attributes was what worked for me. – serge_portima Feb 08 '21 at 07:58
  • 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. – Jeremy Caney Feb 08 '21 at 08:03
  • @serge_portima 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 itself* – Panagiotis Kanavos Feb 08 '21 at 08:19
  • @PanagiotisKanavos Swashbuckle doesn't recognize them as required then, which was one of my goals. – serge_portima Feb 08 '21 at 09:19
  • @serge_portima that's a different question entirely. – Panagiotis Kanavos Feb 08 '21 at 09:33
  • @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. – Jeremy Caney Feb 08 '21 at 18:49
  • @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. – Jeremy Caney Feb 08 '21 at 19:04
  • 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 now – SerjG Mar 04 '22 at 02:06

6 Answers6

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;
    }
}
serge_portima
  • 111
  • 1
  • 10
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();
        }
    }
alereca
  • 76
  • 1
  • 4
1

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

Dave Mateer
  • 6,588
  • 15
  • 76
  • 125
1

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

sagimar
  • 11
  • 1
  • 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. – Jeremy Caney Jan 26 '22 at 00:11
1

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

Mohsen
  • 4,000
  • 8
  • 42
  • 73
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.

Gez
  • 26
  • 5