Search code examples
c#asp.net-mvc

ASP.NET MVC data annotations: initial data is null, but required input value on submit form


I have the situation that is the Notes column inside of the table is NULL. This data is imported automatically.

I have page where user open to edit that record including the Notes field, but the Notes field is required value when user submits the form. I know the validation for Notes field can be down at the client-side using Javascript, but I like validate using ASP.NET MVC data annotations if possible.

Here is the model class (simplified):

public class DetailModel
{
    // Some other properties here....
    public string? Notes { get; set; }
}

From the controller, it calls the method shown here below and the edit page works fine:

public async Task<ActionResult?> Detail(string? id)
{
   DetailModel? Detail = await _StoredProcedures.GetDetail(id);
   return View(Detail);
}


public async Task<DetailModel?> GetDetail(string? id)
{
    List<DetailModel> Info = await _context.Database.SqlQuery<DetailModel>($@"EXECUTE [dbo].[p_DetailView] @Method = 'Info', @RecID = {id}").ToListAsync();
    return Info.FirstOrDefault();
}

Now if I try to validate using the data annotations as shown here, it will break:

public class DetailModel
{
    // Some other properties here....
        
    [Required]
    public string? Notes { get; set; }
}

Error message:

SqlNullValueException: Data is Null. This method or property cannot be called on Null values.
Microsoft.Data.SqlClient.SqlBuffer.ThrowIfNull()
InvalidOperationException: An error occurred while reading a database value for property 'DetailModel.Notes'. The expected type was 'System.String' but the actual value was null.

Is there a way to overcome this? Given the initial data is NULL, but enforce on the form submit to require value using data annotations?


Solution

  • You may want to have a mapper, a DTO and a ViewModel. In this way your DTO, which you use to exchange data with database, may be:

    public class DetailDto
    {
        public string? Notes { get; set; }
    }
    

    Your View Model, which you use for your form, may be:

    public class DetailModel
    {
        [Required]
        public string Notes { get; set; }
    }
    

    Then you can do mapping from one shape to another when you need:

    DetailDto dto = await _StoredProcedures.GetDetail(id);
    DetailModel detailModel = DtoMapper.Map(dto);
    

    You can start with simple static mapper and later consider more advanced technics:

    public static class DtoMapper
    {
        public static DetailModel Map(DetailDto dto)
        {
            return new DetailModel
            {
                 Notes = dto.Notes;
            };
        }
        
        public static DetailDto Map(DetailModel model)
        {
            return new DetailDto
            {
                Notes = model.Notes;
            };
        }    
    }
    

    With this, when you receive DetailModel from the form you can use the power of data annotations along with the aforementioned if (ModelState.IsValid). If the data passes the validation you can map it back from DetailModel to DetailDto, and save to database.