Tutorial on DateTime JSON Serialization between Client and Server

September 13, 2017

When it comes to software development, most developers will encounter some difficulty dealing with date and time. That difficulty could manifest itself in a variety of areas such as time zones, precision, database storage types, etc. One particular area that is frustrating to deal with is the serialization of the C# DateTime object from the server to the client. The DateTime object is converted into a string inside DTOs when serialized into JSON. When it reaches the client, it is still, in fact, a string instead of the Javascript date object. If I want to use any of the Javascript Date functions, I have to be diligent and remember to convert that string into the Javascript Date manually. 

There are several ways to go about getting the Javascript Date object instead of a string. This tutorial demonstrates how a server side date wrapper combined with a model updater service would work with serialization to produce the Javascript Date object when expected. At the end of the tutorial, you should have a project with a fully functional model updater service that will take care of your Javascript Date initialization for you.

  • Prereq: Everything you need for this tutorial comes with an install of Visual Studio 2017 and the templates mentioned in the first section

Download/Setup Angular SPA Template

This blog makes use of the Angular dotnet SPA template that can be found here: 

https://blogs.msdn.microsoft.com/webdev/2017/02/14/building-single-page-applications-on-asp-net-core-with-javascriptservices/

1. Install the SPA templates.

dotnet new --install Microsoft.AspNetCore.SpaTemplates::*

2. Create a new instance of the Angular template.

dotnet new angular

3. Run the npm install and dotnet restore commands on the project.

dotnet restore 
npm install

JSON Serialization Options

1. Set the Json serializations options in the Startup.cs file. This changes the serialization so that property name case isn't modified from whats on the server side.

public void ConfigureServices(IServiceCollection services)
{
	// Add framework services.
	services.AddMvc().AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver());
}

Client Side Models

The two client side models that will be added are the DateWrapper and a test DTO called MyDto.

1. Create a models folder with a dateWrapper.ts and a myDto.ts file inside.

Models Folder With Contents
Models Folder with Contents

2. Update dateWrapper.ts with the following code.

export interface IDateWrapper {
    Date: Date;
    IamADateWrapperClass: boolean;
}
export class DateWrapper implements IDateWrapper {

    public Date: Date = new Date();
    public IamADateWrapperClass: boolean;

    constructor(wrapper: IDateWrapper = {
        Date: new Date(),
        IamADateWrapperClass: true
    }) {
        this.Date = wrapper.Date;
    }
}

The Date property will hold the date that comes across from the server. The IamADateWraooerClass property is a marker that is used to signify that this is a serialized DateWrapper. Later in this model updater service, this property will be the flag for getting the date into the Javascript Date object.

3. Update myDto.ts with the following code.

import { DateWrapper } from './dateWrapper';
export class MyDto {
    public Id: number;
    public Date: Date;
}

Server Side Models

The models on the server side are compliments to the client side models that have already been created with one exception. There will be a MyDto with the new DateWrapper and one without. This will be used to demonstrate the drawbacks of working without this wrapper.

1. Create a Models folder and place a DateWrapper.cs, a MyDtoWithWrapper.cs, and MyDtoWithoutWrapper.cs files inside.

Models Folder With Contents Server
Models Folder with Contents

2. Update DateWrapper.cs with the following code.

public class DateWrapper
{
	public DateWrapper()
	{

	}
	public DateWrapper(DateTime source, DateTimeKind kind = DateTimeKind.Utc)
	{
		Date = DateTime.SpecifyKind(source, kind);
	}

	public bool IamADateWrapperClass { get; set; } = true;
	public DateTime Date { get; set; }

	#region Operators
	public static implicit operator DateWrapper(DateTime source)
	{
		var result = new DateWrapper(source);
		return result;
	}

	public static implicit operator DateTime(DateWrapper source)
	{
		return source.Date;
	}
	#endregion 
}

3. Update MyDtoWithWrapper.cs with the following code.

public class MyDtoWithWrapper
{
	public int Id { get; set; }
	public DateWrapper Date { get; set; }
}

4. Update MyDtoWithoutWrapper.cs with the following code.

public class MyDtoWithoutWrapper
{
	public int Id { get; set; }
	public DateTime Date { get; set; }
}

MVC Endpoints

There are two endpoints that need to be created for the demo. 

1. Create the GetMyDtoWithWrapper endpoint inside the HomeController 

public IActionResult GetMyDtoWithWrapper()
{
	return Json(new MyDtoWithWrapper { Id = 1, Date = DateTime.UtcNow });
}

2. Create the GetMyDtoWithoutWrapper endpoint inside the HomeController 

public IActionResult GetMyDtoWithoutWrapper()
{
	return Json(new MyDtoWithoutWrapper { Id = 1, Date = DateTime.UtcNow });
}

Model Updater Service

The model updater service will take the HTTP post's response and translate any DateWrapper it finds into an actual Javascript Date instead of a string. It leaves other objects and primitive types as they are.

1. Create a services folder with a model-updater.service.ts file inside.

Services Folder And Contents1
Services Folder and Contents

2. Update the model-updater.service.ts file with the following code.

import { Injectable } from '@angular/core';
import { IDateWrapper, DateWrapper } from '../models/dateWrapper';

@Injectable()
export class ModelUpdaterService {

    constructor() {
    }

    public updateModel(source: any): any {
        if (typeof source === "object") {
            source = this.handleObject(source);
        }
        return source;
    }

    private handleObject(source: any): any {
        if (this.instanceOfDateWrapper(source)) {
            source = this.translateDateWrapper(source);
        }
        else {
            for (var prop in source) {
                if (typeof source[prop] === "object") {
                    if (this.instanceOfDateWrapper(source[prop])) {
                        source[prop] = this.translateDateWrapper(source[prop]);
                        
                    }
                    else {
                        source[prop] = this.handleObject(source[prop]);
                    }
                }
            }
        }
        return source;
    }

    private instanceOfDateWrapper(object: any): object is IDateWrapper {
        return 'IamADateWrapperClass' in object;
    }

    private translateDateWrapper(source: DateWrapper): Date {
        var result = new Date(source.Date);
        return result;
    }
}

By calling updateModel method with the model provided as a parameter, the method loops through all nested properties in the model and looks for the IamADateWrapperClass flag that was mentioned earlier to identify DateWrappers. Other properties are ignored.

App.module Update

Next, the app.module files need to be updated so the model updater service is available throughout the app.

1. Update app.module.shared.ts with the following code.

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { AppComponent } from './components/app/app.component'
import { NavMenuComponent } from './components/navmenu/navmenu.component';
import { HomeComponent } from './components/home/home.component';
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
import { CounterComponent } from './components/counter/counter.component';

import { ModelUpdaterService } from './services/model-updater.service';

export const sharedConfig: NgModule = {
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
        NavMenuComponent,
        CounterComponent,
        FetchDataComponent,
        HomeComponent
    ],
    providers: [
        ModelUpdaterService
    ],
    imports: [
        RouterModule.forRoot([
            { path: '', redirectTo: 'home', pathMatch: 'full' },
            { path: 'home', component: HomeComponent },
            { path: 'counter', component: CounterComponent },
            { path: 'fetch-data', component: FetchDataComponent },
            { path: '**', redirectTo: 'home' }
        ])
    ]
};

2. Update app.module.client.ts with the following code.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { sharedConfig } from './app.module.shared';

@NgModule({
    bootstrap: sharedConfig.bootstrap,
    declarations: sharedConfig.declarations,
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        ...sharedConfig.imports
    ],
    providers: [
        { provide: 'ORIGIN_URL', useValue: location.origin },
        ...sharedConfig.providers
    ]
})
export class AppModule {
}

3. Update app.module.server.ts with the following code.

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { sharedConfig } from './app.module.shared';

@NgModule({
    bootstrap: sharedConfig.bootstrap,
    declarations: sharedConfig.declarations,
    providers: sharedConfig.providers,
    imports: [
        ServerModule,
        ...sharedConfig.imports
    ]
})
export class AppModule {
}

Home component

Finally, the home component needs to be updated so that the changes can be demonstrated.

1. Update the home.component.ts file with the following code.

import { Component } from '@angular/core';
import { Http } from '@angular/http'
import 'rxjs/add/operator/toPromise';
import { ModelUpdaterService } from '../../services/model-updater.service';
import { MyDto } from '../../models/myDto';

@Component({
    selector: 'home',
    templateUrl: './home.component.html'
})
export class HomeComponent {
    validModel: MyDto;
    invalidModel: MyDto;

    constructor(
        private http: Http,
        private mus: ModelUpdaterService) {
        this.validModel = new MyDto();
        this.invalidModel = new MyDto();
    }

    getDateWithWrapper() {
        this.postToEndpoint("Home/GetMyDtoWithWrapper").then((response) => {
            if (response)
                this.validModel = this.mus.updateModel(response);
        });
    }

    getDateWithoutWrapper() {
        this.postToEndpoint("Home/GetMyDtoWithoutWrapper").then((response) => {
            if (response)
                this.invalidModel = this.mus.updateModel(response);
        });
    }

    postToEndpoint(endPoint: string): Promise<any> {
        var promise = this.http.get(endPoint)
            .toPromise()
            .then(
            function Success(response) {
                return response.json();
            },
            function Error(response) {
                console.log(response);
                alert(response)
            }
            );
        return promise;
    }

    alertISOString(model: MyDto) {
        model.Date == null ? alert('no date') : alert(model.Date.toISOString())
    }
}

The class now includes the method calls to hit our two new endpoints, an alert method for the demo, and the models for the demo.

2. Update the home.component.html file with the following code.

<h1>Date Demo</h1>
<div class="col-md-6">
    <h2>Without Wrapper</h2>
    <button type="button" (click)="getDateWithoutWrapper()">Get Date without Wrapper</button><br />
    {{invalidModel | json}}<br />
    <button type="button" (click)="alertISOString(invalidModel)">Get ISO string from No Wrapper</button><br />
</div>
<div class="col-md-6">
    <h2>With Wrapper</h2>
    <button type="button" (click)="getDateWithWrapper()">Get Date with Wrapper</button><br />
    {{validModel | json}} <br />
    <button type="button" (click)="alertISOString(validModel)">Get ISO string from Wrapper</button><br />
</div>

The html markup contains 2 buttons per endpoint. The first button will retrieve our date and the second button will try to alert the Javascript Date method toISOString() to the user.

Demo

Launch the app. The screen below should be displayed.

Demo Start Screen1
Demo Start Screen

Without Date Wrapper

1. Click the Get Date without Wrapper button. The DTO will materialize below the button in the brackets

Get Date Without Wrapper1
Get Date without Wrapper

2. Click Get ISO string from no Wrapper. The browser's console (F12 in the browser) will show an error stating that toISOString is not a function. This is because the date that was serialized on the server into a string stayed a string.

Console Error
Console Error

With Wrapper

1. Click the Get Date with Wrapper button. The DTO will materialize below the button in the brackets

Get Date With Wrapper1
Get Date with Wrapper

2. Click Get ISO string from Wrapper. The ISO string should show in an alert from the browser and no new errors should be showing in the browser's console window. This is because the model updater service changed the wrapper into a Javascript Date object for the home component.

Alert1
ISO string alert

Closing Remarks

After seeing the demo, its easy to see that this is slightly better than having to remember to reinitialize the date on the client side. The ideal way to use this solution would be to have the model updater run each time you return from the server at a framework level in order to avoid having to manually call it each time a request is sent to the server. After that is done, the developer just needs to be diligent with using DateWrapper for DateTime when defining DTOs.

Information and material in our blog posts are provided "as is" with no warranties either expressed or implied. Each post is an individual expression of our Sparkies. Should you identify any such content that is harmful, malicious, sensitive or unnecessary, please contact marketing@sparkhound.com.

Meet Sparkhound

Review our capabilities and services, meet the leadership team, see our valued partnerships, and read about the hardware we've earned.

Learn How We Work

See how our Plan/Build/Run methodology drives real client success, and gain our team's perspectives on timely tech topics.

Engage With Us

Get in touch any of our offices, or checkout our open career positions and consider joining Sparkhound's dynamic team.