Search code examples
c#validationasp.net-core-mvc.net-8.0

ASP.NET Core 8 MVC - view model validations


I have an ASP.NET Core 8 MVC application. Currently I'm trying to have an admin have the ability to create new clients for their e-commerce site. They have a simple form looking like this:

NewClient.cshtml:

@using Ecommerce.Models
@model NewClientViewModel
@using Microsoft.AspNetCore.Identity
@using Ecommerce.ViewModel

<h1>New Client creation</h1>
<div>
    <form asp-action="NewClient" asp-controller="Admin" method="post">
        <div>
            <label asp-for="Email">Email</label>
            <input asp-for="Email" class="form-control" />
            <span asp-validation-for="Email" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="FirstName">First Name</label>
            <input asp-for="FirstName" class="form-control" />
            <span asp-validation-for="FirstName" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="LastName">Last Name</label>
            <input asp-for="LastName" class="form-control" />
            <span asp-validation-for="LastName" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Password">Password</label>
            <input asp-for="Password" class="form-control" />
            <span asp-validation-for="Password" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="BranchName">Branch Name</label>
            <input asp-for="BranchName" class="form-control" />
            <span asp-validation-for="BranchName" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="PhoneNumber">Phone Number</label>
            <input asp-for="PhoneNumber" class="form-control" />
            <span asp-validation-for="PhoneNumber" class="text-danger"></span>
        </div>
        <div>
            <label asp-for="Address">Address</label>
            <input asp-for="Address" class="form-control" />
            <span asp-validation-for="Address" class="text-danger"></span>
        </div>
        <div class="form-check">
            <input  asp-for="CheckedBox" class="form-check-input" type="checkbox" id="flexCheckDefault">
            <label class="form-check-label" for="flexCheckDefault">
                If Address same as Shipping Address please check box here
            </label>
        </div>
        <div>
            <label asp-for="ShippingAddress">Shipping Address</label>
            <input asp-for="ShippingAddress" class="form-control" />
            <span asp-validation-for="ShippingAddress" class="text-danger"></span>
        </div>
        <div class="form-group">
            <button type="submit" class="btn btn-primary">Create Client</button>
        </div>
    </form>
</div>

On the checkbox, if the address is the same as the shipping address they should check box to 'true'. Here is the model, view model, and the controller that handles the post request:

NewClientViewModel:

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

[Required]
[Display(Name = "First Name")]
public string FirstName { get; set; }

[Required]
[Display(Name = "Last Name")]
public string LastName { get; set;}

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

[Required]
[Display(Name = "Branch Name")]
public string BranchName { get; set; }

[Required]
[Display(Name = "Phone Number")]
public string PhoneNumber { get; set; }

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

[AllowNull]
[Display(Name = "Shipping Address")]
public string? ShippingAddress { get; set; }

public bool CheckedBox { get; set; } = false;

Client.cs:

public class Client
{
    public int Id { get; set; }
    public string BranchName { get; set; }
    public string PhoneNumber { get; set; }
    public string Address { get; set; }

    [AllowNull] // <-- added this to test out if this attribute would work. It didn't make a difference
    // By adding the question after the type it should allow null to my understanding
    public string? ShippingAddress { get; set; }
}

AdminController:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> NewClient(NewClientViewModel client)
{
    if (!ModelState.IsValid)
        return View();
    
    User newClientUser = new User
    {
        UserName = client.Email,
        Email = client.Email,
        FirstName = client.FirstName,
        LastName = client.LastName
    };

    Client newClient = new Client();

    // So if the user checks box true we enter this if check
    // to set shipping address null. Otherwise do the opposite
    if (client.CheckedBox)
    {
        newClient.BranchName = client.BranchName;
        newClient.PhoneNumber = client.PhoneNumber;
        newClient.Address = client.Address;
        newClient.ShippingAddress = null;
    }
    else
    {
        newClient.PhoneNumber = client.PhoneNumber;
        newClient.Address = client.Address;
        newClient.ShippingAddress = client.ShippingAddress;
    }

    IdentityResult result = await _userManager.CreateAsync(newClientUser, client.Password);

    if (!result.Succeeded)
    {
        _logger.LogInformation("Failed attempt");

        ModelState.AddModelError(string.Empty, "Hmm something went wrong!");
        return View();
    }

    await _userManager.AddToRoleAsync(newClientUser, Roles.Customer.ToString());

    // This line throws a null ref exception
    await _dbContext.Clients.AddAsync(newClient);
    await _dbContext.SaveChangesAsync();

    return RedirectToAction("Dashboard", "Admin");
}

My question is why am I getting the null ref if I made the client model and the view model to allow null. I also verified in the DB that it will accept null if nothing is provided. What am I overlooking that it is failing to save a new client into the DB? I can provide explicitly newClient.ShippingAddress = null; to newClient.ShippingAddress = client.ShippingAddress;, however, I feel that it would be redundant if I'm supposedly saying in the model to accept null.


Solution

  • I have found my issue. This is more of a DBSet issue than a new entity instantiation issue. In my DBContext I had this line of code

    public class EcommerceDBContext : IdentityDbContext<User, IdentityRole<int>, int>
    {
        public EcommerceDBContext(DbContextOptions<EcommerceDBContext> options) : base(options) {}
    
        public DbSet<User> Users { get; set; }
        public DbSet<Client> Clients { get; } // Only had get and not set
    }
    

    Was preventing me setting or creating a new entity to save in the DB. In the Clients table, switching from { get; } to { get; set; } fixed my issue. Always make sure that DB Context file is set properly.