Search code examples
.netasp.net-mvcentity-frameworkforeign-keysmodelstate

How to get ModelState to Validate Data Submitted Through a View Model With Foreign Keys?


Note: Please ignore the fact that the passwords aren't stored securely; this project is just for my own learning purposes, and I will address this issue in the future.

I have three models, Account, AccountType, and Person:

public class Account
{
    [Key]
    public int AccountID { get; set; }

    [ForeignKey("AccountType")]
    public int AccountTypeID { get; set; }

    [ForeignKey("Person")]
    public int PersonID { get; set; }

    [Required]
    public string Username  { get; set; }

    [Required]
    public string Password { get; set; }

    [Required]
    public DateOnly DateCreated { get; set; } = DateOnly.FromDateTime(DateTime.Now);

    [Required]
    public DateOnly DateModified { get; set; } = DateOnly.FromDateTime(DateTime.Now);

    public virtual AccountType AccountType { get; set; }
    public virtual Person Person { get; set; }
}

public class AccountType
{
    [Key]
    public int AccountTypeID { get; set; }

    [Required]
    public string AccountTypeName { get; set; }
}


public class Person
{
    [Key]
    public int PersonID { get; set; }
    
    [Required]
    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [Required(AllowEmptyStrings = true)]
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    [DisplayName("Middle Name")]
    public string MiddleName { get; set; }
    
    [Required]
    [DisplayName("Last Name")]
    public string LastName { get; set; }
    
    [Required]
    [DisplayName("Email")]
    public string Email { get; set; }

    [Required(AllowEmptyStrings = true)]
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    [DisplayName("Phone")]
    public string Phone { get; set; }
    
    [Required]
    public DateOnly CreatedDate { get; set; } = DateOnly.FromDateTime(DateTime.Now);
    
    [Required]
    public DateOnly ModifiedDate { get; set; } = DateOnly.FromDateTime(DateTime.Now);
}

Since I want to use all three of these models in one view, I created a Register view model for them:

public class Register
{
    public List<SelectListItem> AccountTypes { get; set; }
    public Account Account { get; set; }
    public Person Person { get; set; }
}

Here is the relevant controller's GET action method, which serves the view:

public IActionResult Register()
{
    var Register = new Register();
    Register.AccountTypes = _db.AccountTypes.Select(accType => new SelectListItem
    {
        Value = accType.AccountTypeID.ToString(),
        Text = accType.AccountTypeName
    }).ToList();
    return View(Register);
}

And here is the Register view itself:

@model Register

<form method="post">
    <div class="row mb-2">
        <div class="col-12">
            <select asp-for="Account.AccountTypeID" 
                    asp-items="Model.AccountTypes" 
                    class="form-select" 
                    aria-label="Default select example"
            ></select>
        </div>
    </div>
    <div class="row mb-2">
        <div class="col-12">
            <label asp-for="Account.Username" class="d-block"></label>
            <input asp-for="Account.Username" class="w-100" />
            <span asp-validation-for="Account.Username" class="text-danger"></span>
        </div>
        <div class="col-12">
            <label asp-for="Account.Password" class="d-block"></label>
            <input asp-for="Account.Password" class="w-100" />
            <span asp-validation-for="Account.Password" class="text-danger"></span>
        </div>
    </div>

    <partial name="/Views/Person/_Create.cshtml" for="Person" />

    <button type="submit" class="btn btn-primary">Create</button>
</form>

I'm now at the stage of coding the controller's POST action method, with the goal of submitting the bound fields from the view into a database, and I'm running into some trouble:

[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Register(Register Register)
{
    if (ModelState.IsValid)
    {
        _db.Persons.Add(Register.Person);
        _db.Accounts.Add(Register.Account);
        _db.SaveChanges();
    }
    return View(Register);
}

When I place a marker at the ModelState.IsValid line, I see that it's false. I understand that it's because 3 model fields have null / invalid values:

  1. AccountTypes
  2. Account.AccountType
  3. Account.Person

I need AccountTypes in the Register model to display the list of accounts types in the view's dropdown / select field. Then, Account.AccountType and Account.Person are also necessary since they establish the relationships between the tables for Entity. So, I guess that I can't remove these fields from the model.

I've considered somehow excluding these particular fields from the validation process while keeping them in the model, but I'm not exactly sure how to go about that. Moreoever, I've got a nagging feeling that there may be a much better way of handling this entire process that I'm unaware of.

So, what's the most proper way of getting ModelState to accept the POST register request?


Solution

  • Apparently you are using net6. You have two choices, make this properties nullable (and maybe some more)

     public virtual AccountType? AccountType { get; set; }
     public virtual Person? Person { get; set; }
    

    or to prevent this in the future for another classes you can remove Nullable from project config

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <!--<Nullable>enable</Nullable>-->
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>