EF [Required] annotation on Model's property but skip validation -- override ModelValidationState?

asp.net-core asp.net-core-2.0 c# entity-framework-core

Question

In Asp.Net Core 2.0 & EF Core, how can I override the validation attributes for a property? If I add the [Required] annotation to the property, two things happen:

  1. EF marks the database column as not-null --> Good. I want this.
  2. In the controller, following a POST, ModelState reports this property as failing validation (and thus ModelState.IsValid returns false) --> Bad. This property is excluded from the form in HTML, as I'm adding just the FK value in the controller.

I have a model with an "Author" required property that maps to an IdentityUser; in the database, that column should be not-null, but I don't want to pass the user-id between the client and server. Instead, when a POST is received, I just grab the userid from the context and set an FK property on the model.

So, my desired result is some data annotation specifying that the property is required for database purposes but optional/skipped for validation purposes as far as ModelState is concerned.


The Model

public class ArticleModel
{
    [Required]
    public string Title {get;set}
    [Required]
    public string Body {get;set;}

    [Required]
    public IdentityUser Author { get; set; }

    [ForeignKey("Author")]
    public string AuthorId { get; set; }
}

The View (The view doesn't know or care about Author)

<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Body" class="control-label"></label>
                <input asp-for="Body" class="form-control" />
                <span asp-validation-for="Body" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Title" class="control-label"></label>
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </form>
    </div>
</div>

The Controller's Create method
(ModelState.IsValid is always false, due to the Invalid state of the Author)

    [HttpPost]
    [ValidateAntiForgeryToken]
    [Authorize]
    public IActionResult Create([Bind("Body,Title")] ArticleModel articleModel)
    {
        articleModel.AuthorId = User.FindFirstValue(ClaimTypes.NameIdentifier);

        if (ModelState.IsValid)
        {
            DoStuff();
            return RedirectToAction(nameof(Index));
        }
        return View(articleModel);
    }

One Fix (ugly, brittle, and is repeated on several handlers)
check that all the desired fields are valid, and ignore the rest... replace
if (ModelState.IsValid)
with
if (ModelState.Where(k => k.Key != "Author").All(v => v.Value.ValidationState == ModelValidationState.Valid))


A better(?) fix (proper - with DTOs)
I was trying to use MS's default scaffolding here, which takes a model and just feeds it to views. In the past, I've usually done this manually with DTOs, where this would all be a non-issue. Is that still the better path in the Asp.Net Core era?


The fix I thought I'd find
in the model....

[Required]
[Validation(Mode=ValidationModes.Manual)]
public IdentityUser Author {get;set;}
1
1
6/22/2018 7:42:17 AM

Accepted Answer

There's a number of issues with using your entity classes directly to bind to POSTs and such. A view needing different validation rules is of course one of them, but you also have to deal with overposting, potential context conflicts, etc. Conflating the entity class with a "model" is an original sin of ASP.NET MVC, and unfortunately, Microsoft has done no better job at dissuading this conflation with ASP.NET Core. Simply, an entity class is not a model. An entity class serves one simple purpose, representing a database table row as an object. Period. Using it for anything other than that will invariably cause problems.

Long and short, yes, you should utilize view models, DTOs, etc. Whatever you call them, they're essentially the same thing. You're creating a class for the purpose of a view or a return from a particular method, in order to decouple your database representation from that. You entity class should contain only what logic is necessary for your database. Your view model/DTO should contain only what logic is necessary for what they're feeding. Then, you map one to the other. Not only does this solve your issue here, and other issues as well, but it also serves to remove dependencies from your code, which is always a good thing.

1
6/22/2018 1:03:33 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow