Value from HTML date picker is not interpreted in ASP.NET Core MVC controller
Context
- ASP.NET Core MVC web application targeting .NET 6.0
- Entity Framework Core 6.0.6
- SQL Server Local DB
- The forms contain HTML date picker.
Goal
- Being able to smoothly work with the new
DateOnly
type. SQL Server has a correspondingdate
type. - I don't want to use any annotation at model level. Data shall be treated automatically/globally like common types.
- Value from DB should be displayed accordingly in the date picker upon loading forms. ✔
- Value from date picker should be retrieved correctly by the targeted action. ❌
So ultimately what I want is the Student.DateOfBirth
of type DateOnly
to be properly populated when it reaches the action in the controller instead on being null
.
Many thanks to anyone being able to help.
Actions performed
1. Add DateOnly
properties in a few models
For instance:
namespace WebApp1.Models;
public class Student
{
public Guid StudentId { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? DateOfBirth { get; set; }
}
2. Setup EF
2.1. Write Entity Configs
For the Student
entity
namespace WebApp1.Persistence.EntityConfigs;
public class StudentConfig : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.HasKey(s => s.StudentId);
builder
.Property(s => s.FirstName)
.HasMaxLength(50)
.IsRequired();
builder
.Property(s => s.LastName)
.HasMaxLength(50)
.IsRequired();
// Seeding
builder.HasData(
new Student { StudentId = Guid.NewGuid(), DateOfBirth = DateOnly.FromDateTime(DateTime.Today), FirstName = "Uchi", LastName = "Testing" },
new Student { StudentId = Guid.NewGuid(), DateOfBirth = DateOnly.FromDateTime(DateTime.Today), FirstName = "Armin", LastName = "VanSisharp" },
new Student { StudentId = Guid.NewGuid(), DateOfBirth = DateOnly.FromDateTime(DateTime.Today), FirstName = "Jack", LastName = "O'Voltrayed" }
);
}
}
2.2. Write a ValueConverter<DateOnly,DateTime>
namespace WebApp1.Persistence.Conventions;
public class DateOnlyConverter : ValueConverter<DateOnly, DateTime>
{
public DateOnlyConverter() : base(
d => d.ToDateTime(TimeOnly.MinValue),
d => DateOnly.FromDateTime(d)
)
{ }
}
2.3. Add usual config
Setup of the connection string in appsettings.json
which works fine.
In program.cs
:
builder.Services.AddTransient<ApplicationContext>();
builder.Services.AddDbContext<ApplicationContext>(
options => options
.UseSqlServer(builder.Configuration.GetConnectionString("Dev")
));
3. Write DateOnlyJsonConverter
namespace WebApplication1.Converters;
public class DateOnlyJsonConverter : JsonConverter<DateOnly>
{
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateOnly.ParseExact(reader.GetString()!, "yyyy-MM-dd", CultureInfo.InvariantCulture);
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
}
4. Add the JSON converter in JsonOptions
In Program.cs
:
builder.Services.AddControllersWithViews()
.AddJsonOptions(options =>
options.JsonSerializerOptions
.Converters.Add(new DateOnlyJsonConverter())
);
5. Wrote a Tag Helper for date picker
public class DatePickerTagHelper : TagHelper
{
protected string dateFormat = "yyyy-MM-dd";
public ModelExpression AspFor { get; set; }
public DateOnly? Date { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
base.Process(context, output);
output.TagName = "input";
if (!string.IsNullOrEmpty(AspFor.Name))
{
output.Attributes.Add("id", AspFor.Name);
output.Attributes.Add("name", AspFor.Name);
}
output.Attributes.Add("type", "date");
if (Date.HasValue)
output.Attributes
.SetAttribute("value", GetDateToString());
//output.Attributes.Add("value", "2022-01-01");
output.TagMode = TagMode.SelfClosing;
}
protected string GetDateToString() => (Date.HasValue)
? Date.Value.ToString(dateFormat)
: DateOnly.FromDateTime(DateTime.Today).ToString(dateFormat);
}
6. Wrote a form view
@using WebApp1.Models;
@using WebApplication1.Helpers;
@model Student
<h2>Edit @(Model.StudentId == Guid.Empty ? "New Student":$"Student {@Model.FirstName} {@Model.LastName}")</h2>
<form asp-action="UpsertStudent" asp-controller="Student" method="post">
<input asp-for="StudentId" type="hidden" />
<label asp-for="FirstName"></label>
<input asp-for="FirstName" />
<label asp-for="LastName"></label>
<input asp-for="LastName" />
<label asp-for="DateOfBirth"></label>
<date-picker date=@Model.DateOfBirth asp-for="DateOfBirth"/>
<button type="reset">Reset</button>
<submit-button text="Submit Me"/>
</form>
7. [P.S.] Applied newly made convention to EF
I forgot that one.
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
configurationBuilder
.Properties<DateOnly>()
.HaveConversion<DateOnlyConverter>()
.HaveColumnType(SqlDbType.Date.ToString());
}
Behaviour
- Data is properly retrieved from DB and displayed upon form/page load. The date picker is set to the same date as persisted in the DB ✔
- Data is transferred accordingly in the
HttpContext.Requests.Form
which contains any updated value from the form ✔
- Date is set to
null
in the action ❌
ModelState.IsValid
returnstrue
❓
Documentation
- Many articles and posts here and there I do not even remember all of'em
- Official docs