UchiTesting
UchiTesting

Reputation: 159

Value from HTML date picker is not interpreted in ASP.NET Core MVC controller

Value from HTML date picker is not interpreted in ASP.NET Core MVC controller

Context

Goal

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 [email protected] 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

enter image description here

enter image description here

enter image description here

enter image description here

Documentation

Upvotes: 0

Views: 1420

Answers (2)

Ruikai Feng
Ruikai Feng

Reputation: 11546

The First question: When you send the request,The data would not be serialized if it was in Request.Form,so your json related settings would not work.You could create a ViewModel and map your targetmodel with automapper

The Second question: Because the property is nullable,if you want to keep it nullable,you need to add the [required]attribute on it,for more details,you could read this document: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-6.0#non-nullable-reference-types-and-the-required-attribute

I tried as below:

Model:

 public class Student
    {
        public Guid StudentId { get; set; }
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        public DateOnly? DateOfBirth { get; set; }
    }
    public class StudentVM
    {
        public Guid? StudentId { get; set; }
        
        public string? FirstName { get; set; }
        public string? LastName { get; set; }
        
        public DateTime DateOfBirth { get; set; }
    }

Create Map

public class AutoMapperProfiles : Profile
    {
        public AutoMapperProfiles()
        {

            CreateMap<StudentVM, Student>().ForMember(des=>des.DateOfBirth,o=>o.MapFrom(src=>DateOnly.FromDateTime(src.DateOfBirth))).ReverseMap();  
        }
    }

Set in Program.cs:

builder.Services.AddAutoMapper(typeof(Program));

Result: enter image description here

Upvotes: 0

Buka
Buka

Reputation: 59

As I see you writed converter from json string while request is sending via formdata.

Actually it looks like a bug in asp net core, so simplest way for me is just using DateTime and then convert it to dateonly. Or you can try to write custom model binder for dateonly

P.S. as I understand issue with automatic binding already opened https://github.com/dotnet/aspnetcore/issues/34591

Upvotes: 0

Related Questions