Click here to Skip to main content
15,881,742 members
Articles / Programming Languages / Typescript
Tip/Trick

DateOnly in ASP.NET 7 with JavaScript Clients

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
16 Nov 2022CPOL4 min read 7.4K   3  
Resolving issues with date picker component in ASP.NET 7 applications.
ASP.NET 7 had most issues from ASP.NET 6 resolved. But, for Web frontend, you still need to be careful, especially with date picker components.

Introduction

DateOnly was introduced in .NET 6, however without proper serialization and binding for ASP.NET applications. To utilize DateOnly in ASP.NET 6 applications, you have to do some extra programming as discussed in "DateOnly in .NET 6 and ASP.NET Core 6". ASP.NET 7 released on 8th November, 2022 had most issues resolved. However, for Web frontend, you still need to be careful, especially with date picker components. Depending on your design approaches, you may still need Fonlow.DateOnlyJsonConverter.

Issues

HTML provides a native date input:

HTML
<input type="date" id="start" name="trip-start" 
 value="2022-10-23"  min="2022-01-01" max="2023-06-31">

The value of the input is a string representing a date in YYYY-MM-DD format, or empty. If you use the date input in a JavaScript application with binding like this one in an Angular 2+ app:

HTML
<input type="date" id="start" name="trip-start" 
 [(ngModel)]="d" min="2021-01-01" max="2023-06-30">

The object assigned to variable "d" in JavaScript is of the string type, rather than the Date type. "YYYY-MM-DD" is good for deserialization of binding in ASP.NET 7 backend, however, not straightforward for client side calculation, since you have to convert it to a Date object first.

Some Web UI libraries like Angular Material Components suites provide a beautiful date picker transforming string "YYYY-MM-DD" to a Date object which stores UTC value, and this is straightforward for client side calculation. When the date object is serialized in an AJAX call by HttpClient of Angular, the value is serialized into a ISO 8601 datetime string. For example, picking "2022-12-25" may result in "2022-12-24T14:00:00.000Z" while the local time representation of the date object is "Sun Dec 25 2022 00:00:00 GMT+1000 (Australian Eastern Standard Time)", however, ASP.NET 7 binding will fail in some cases.

Case 1: DateOnly in POST

Without the converter, ASP.NET 7 binding parses  "2022-12-24T14:00:00.000Z" and give a DateOnly.MinValue (0001-01-01) value in PostDateOnly() to the body of the Web API function and return an empty string (empty JSON string object, status 200), or give a null in PostDateONlyNullable() and return empty body (status 204) in the HTTP response.

C#
[HttpPost]
[Route("ForDateOnly")]
public DateOnly PostDateOnly([FromBody] DateOnly d)
{
    return d;
}

[HttpPost]
[Route("DateOnlyNullable")]
public DateOnly? PostDateOnlyNullable([FromBody] DateOnly? d)
{
    return d;
}

Case 2: DateOnly as a Property of a Complex Object in POST

If the expected DateOnly object is of a nested property of a complex object, ASP.NET 7 binding will fail entirely and interpret the complex object as null.

API:

C#
[HttpPost]
[Route("createPerson")]
public long CreatePerson([FromBody] Person p)
{
    Debug.WriteLine("CreatePerson: " + p.Name);
   ...
}

POST Payload:

JavaScript
{
    "name": "John Smith1668562590592",
    "givenName": "John",
    "surname": "Smith",
    "dob": "1969-12-28T00:00:00.000Z",
    "baptised": "1980-01-31T00:00:00.000Z"
}

Using Angular Material Component Suite

DatePicker of Angular Material Component Suite could be customized through multiple means. By default, the DatePicker components will give data as described above, causing the backend to get yesterday's date.

Rather than using the default NativeDateAdapter, MomentDateAdaptor is more comprehensive for cross-timezone applications.

TypeScript
import { MAT_MOMENT_DATE_ADAPTER_OPTIONS, MAT_MOMENT_DATE_FORMATS, 
         MomentDateAdapter } from '@angular/material-moment-adapter';

{ provide: DateAdapter, useClass: MomentDateAdapter, 
  deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS] },
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true, strict: true } },

Picking date 2022-12-25 will result in a date object "Sun Dec 25 2022 00:00:00 GMT+0000" and the serialized value is "2022-12-25T00:00:00.000Z".

ASP.NET 6 application codes with Fonlow.DateOnlyJsonConverter and ASP.NET 7 application codes without the converter can work perfectly with such Angular SPA.

If you have ASP.NET 6 codes following the advices in "DateOnly in .NET 6 and ASP.NET Core 6" and have just upgraded to ASP.NET 7, the legacy codes should be working right away while having the converter. The converter is compatible with the new DateOnly handing in ASP.NET 7. However, you may consider the converter is obsolete, and remove it from your ASP.NET 7 startup codes, then test respective test cases to ensure really nothing is broken.

Points of Interest

When developing Web applications dealing with date only info like date of birth, you need to consider the following:

  • HTML date input gives a string representing a date in YYYY-MM-DD.
  • JavaScript date object stores UTC datetime.
  • Some date picker components like Angular Material DatePicker give a JavaScript date object which value may vary depending on which DateAdapter to use, with respective options.
  • Whether the client calls should contain timezone info, for example, through the HTTP request headers.

You had better test some of the following cases, and also make the client and the server sit in different timezones. For example, I am in Australia UTC+10 timezone, and have a server at UTC-10 timezone, so I have a 20 hours window to test cross-timezone issues between the clients and the service.

Test Cases

Service:

C#
[HttpPost]
[Route("ForDateOnly")]
public DateOnly PostDateOnly([FromBody] DateOnly d)
{
    return d;
}

JS Client:

TypeScript
it('postDateOnly', (done) => {
    const dt = new Date(Date.parse('2018-12-25')); //JS will serialize it
                                                   //to 2018-12-25T00:00:00.000Z.
    service.postDateOnly(dt).subscribe(
        data => {
            const v: any = data; //string 2008-12-25
            expect(v).toEqual('2018-12-25');
            done();
        },
        error => {
            fail(errorResponseToString(error));
            done();
        }
    );
}
);

it('postDateOnlyWithUtc', (done) => {
    const dt = new Date(Date.parse('2018-12-25T00:00:00.000Z')); //JS will
                                  //serialize it to 2018-12-25T00:00:00.000Z.
    service.postDateOnly(dt).subscribe(
        data => {
            const v: any = data; //string 2008-12-25
            expect(v).toEqual('2018-12-25');
            done();
        },
        error => {
            fail(errorResponseToString(error));
            done();
        }
    );
}
);

it('postDateOnlyWithAusMidnight', (done) => {
    const dt = new Date(Date.parse('2018-12-24T14:00:00.000Z')); //Angular Material
                    //DatePicker by default will give this when picking 2018-12-25
    service.postDateOnly(dt).subscribe(
        data => {
            const v: any = data;
            expect(v).toEqual('2018-12-24');
            done();
        },
        error => {
            fail(errorResponseToString(error));
            done();
        }
    );
}
);

it('postDateOnlyText', (done) => {
    let obj: any = '2018-12-25';
    service.postDateOnly(obj).subscribe(
        data => {
            const v: any = data; //string 2008-12-25
            expect(v).toEqual('2018-12-25');
            done();
        },
        error => {
            fail(errorResponseToString(error));
            done();
        }
    );
}
);

it('postDateOnlyUtcText', (done) => {
    let obj: any = '2018-12-25T00:00:00.000Z';
    service.postDateOnly(obj).subscribe(
        data => {
            const v: any = data;
            expect(v).toEqual('2018-12-25');
            done();
        },
        error => {
            fail(errorResponseToString(error));
            done();
        }
    );
}
);

it('postDateOnlyAusMidnightText', (done) => {
    let obj: any = '2018-12-24T23:00:01.001Z';
    service.postDateOnly(obj).subscribe(
        data => {
            const v: any = data;
            expect(v).toEqual('2018-12-24');
            done();
        },
        error => {
            fail(errorResponseToString(error));
            done();
        }
    );
}
);

Hints

.NET 7 provides a helper class DateOnlyConverter. I would be looking forward to someone writing an article about where to utilize it.

History

  • 16th November, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
-- There are no messages in this forum --