Custom DateTime Model Binding with .NET and Angular

Want to learn how to create a custom model binder in a .NET application to bind a custom date format? Let's go through the process step by step with a demo app that uses .NET for the Web API and Angular as the UI framework.

When a client application passes a date to an API endpoint via an URL parameter, the model binding mechanism will kick in to bind the parameter value to a property. However, while binding the date, the model binding works only with the default date format such as MM-dd-yyyy (e.g., https://localhost:5001/api/date/06-22-2021).

If we want to pass a custom date format such as “ddMMyyyy”, to an API as a parameter, the model binder will throw an exception. Therefore, we need to create a custom model binder that will allow us to bind a custom date format from the API parameter to the model property.

🗒 Note » You can find the source code for the demo app on GitHub.

Prerequisites

Before we start with our app, we need to install the following software:

  • Download the latest version of Visual Studio 2019 from the Visual Studio download page; while installing, make sure you have selected the ASP.NET and web development workload.
  • Download and install the latest version of Node.js from the Nodejs download page.
  • Install Angular CLI from this page.

Using Angular CLI is not mandatory. We are using Angular CLI for this tutorial as it provides an easy way to work with Angular applications. If you do not want to use CLI, then you need to create the files for components and services manually.

What is model binding?

Model binding allows the action methods in a controller to work directly with model types, which are passed as arguments, rather than HTTP requests. Mapping between incoming request data and application models is handled by model binders.

The default model binders support most of the common .NET data types. However, we can also extend the built-in model binding functionality by implementing custom model binders as per our requirements.

Creating the ASP.NET core application

Open Visual Studio 2019, and click on “Create a new Project”. In the dialog box, select “ASP.NET Core with Angular”, and click on “Next”:

Next up will be the “Configure your new project” screen, where you can provide a name for your application, for example, CustomDateModelBinding. After clicking on “Next”, on the additional information page, select the target framework as .NET 5.0 and set the authentication type to none as shown in the image below:

This will create our project. The folder structure of the application is shown below:

The ClientApp folder contains the Angular code for our application, the Controllers folders will contain our API controllers, and the Angular components are inside the ClientApp\src\app folder. The default template contains a few Angular components; they will not affect our application, but for the sake of simplicity, we will delete the fetchdata and counter folders from the ClientApp\src\app folder. Remove also the reference for these two components from the app.module.ts file.

Creating the CustomDateTimeModelBinder class

Right-click on the CustomDateModelBinding project and select Add > New Folder. Name the folder “Models”. Again, right-click on the Models folder and select Add > Class to add a new class file. Name your class CustomDateTimeModelBinder.cs, then click “Add”, and finally, insert the following code in it:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace CustomDateModelBinding.Models
{
    public class CustomDateTimeModelBinder : IModelBinder
    {
        public static readonly Type[] SUPPORTED_DATETIME_TYPES =
            new Type[] { typeof(DateTime), typeof(DateTime?) };

        public Task BindModelAsync
            (ModelBindingContext modelBindingContext)
        {
            if (modelBindingContext == null)
            {
                throw new ArgumentNullException
                    (nameof(modelBindingContext));
            }

            if (!SUPPORTED_DATETIME_TYPES
                .Contains(modelBindingContext.ModelType)
                )
            {
                return Task.CompletedTask;
            }

            var modelName = modelBindingContext.ModelName;

            var valueProviderResult = modelBindingContext
                .ValueProvider
                .GetValue(modelName);

            if (valueProviderResult == ValueProviderResult.None)
            {
                return Task.CompletedTask;
            }

            modelBindingContext
                .ModelState
                .SetModelValue(modelName, valueProviderResult);

            var dateTimeToParse
                = valueProviderResult.FirstValue;

            if (string.IsNullOrEmpty(dateTimeToParse))
            {
                return Task.CompletedTask;
            }

            var formattedDateTime
                = ParseDateTime(dateTimeToParse);

            modelBindingContext.Result
                = ModelBindingResult.Success(formattedDateTime);

            return Task.CompletedTask;
        }

        static DateTime? ParseDateTime(string date)
        {
            var CUSTOM_DATETIME_FORMATS = new string[]
            {
            "ddMMyyyy",
            "dd-MM-yyyy-THH-mm-ss",
            "dd-MM-yyyy-HH-mm-ss",
            "dd-MM-yyyy-HH-mm",
            };

            foreach (var format in CUSTOM_DATETIME_FORMATS)
            {
                if (DateTime.TryParseExact(
                    date, format, null,
                    DateTimeStyles.None,
                    out DateTime validDate)
                   )
                {
                    return validDate;
                }
            }

            return null;
        }
    }
}

The CustomDateTimeModelBinder class will implement the IModelBinder and override the BindModelAsync method. This method will fetch the value passed in the model context and then parse it to our supported date format. We will then set the ModelBindingResult to success and return the completed task from the method.

Inside the ParseDateTime method, we will create a string array and declare the custom date formats supported by the app. We will then parse the date passed as the parameter to our custom date format.

Add a new class file called CustomDateTimeModelBinderProvider.cs and add the following code to it:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;
using System.Linq;

namespace CustomDateModelBinding.Models
{
    public class CustomDateTimeModelBinderProvider
        : IModelBinderProvider
    {
        public IModelBinder GetBinder(
            ModelBinderProviderContext context)
        {
            if (CustomDateTimeModelBinder
                .SUPPORTED_DATETIME_TYPES
                .Contains(context.Metadata.ModelType)
                )
            {
                return new BinderTypeModelBinder(
                    typeof(CustomDateTimeModelBinder));
            }

            return null;
        }
    }
}

The CustomDateTimeModelBinderProvider class will implement the IModelBinderProvider. The GetBinder method will create an instance of the IModelBinder, based on the ModelBinderProviderContext passed to it. This class is used to provide the custom binder globally in the Startup class of the app.

Adding the Date Controller

We will add a Date Controller to our application which will handle the API request from the UI layer. Right-click on the Controllers folder and select Add > New Item. An “Add New Item” dialog box will open. Select “Visual C#” from the left panel, then select “API Controller-Empty” from the templates panel, set the name to DateController.cs, and click Add. Refer to the image below.

Add the following code to the DateController.cs file:

using Microsoft.AspNetCore.Mvc;
using System;

namespace CustomDateModelBinding.Controllers
{
    [Produces("application/json")]
    [Route("api/[controller]")]
    [ApiController]
    public class DateController : ControllerBase
    {
        [HttpGet("{date}")]
        public string Get([FromRoute] DateTime date)
        {
            Console.WriteLine($"The date passed is {date}");
            return "Date parameter processed successfully";
        }
    }
}

We have added the Get method. The FromRoute attribute is used to specify that the parameter should be bound using route-data from the current request. The method will return a success message.

Configuring the Startup class

Insert the following code in the ConfigureServices method of the startup.cs file:

services.AddMvc(options =>
{
    options
        .ModelBinderProviders
        .Insert(0, new CustomDateTimeModelBinderProvider());
});

We have registered our provider class so that the model binder is available globally at the app level.

Working with the client side of the app

The code for the client side is available in the ClientApp folder. We will use Angular CLI to work with the client code. Navigate to the CustomDateModelBinding\ClientApp folder in your machine and open a command window. We will execute all our Angular CLI commands in this window.

Creating the date service

We will create an Angular service to invoke the API endpoints. Run the following command:

ng g s services\date

This command will create a folder with name services, and then, create the following two files inside it.

  • service.ts—the service class file.
  • service.spec.ts—the unit test file for service.

Open the date.service.ts file and insert the following code in it:

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Injectable({
  providedIn: "root",
})
export class DateService {
  constructor(private http: HttpClient) {}

  getDate(date: any) {
    const url = `/api/date/${date}`;
    return this.http.get<string>(url);
  }
}

We have created a function called getDate, which will invoke our API endpoint and pass the date as an API parameter.

Creating the CustomDate component

Run the following command to create the car component:

ng g c custom-date

Open src\app\custom-date\custom-date.component.ts and put the following code inside the CustomDateComponent class:

import { Component } from "@angular/core";
import { DateService } from "../services/date.service";
import { DatePipe } from "@angular/common";

@Component({
  selector: "app-custom-date",
  templateUrl: "./custom-date.component.html",
  styleUrls: ["./custom-date.component.css"],
  providers: [DatePipe],
})
export class CustomDateComponent {
  constructor(
    private dateService: DateService,
    private datePipe: DatePipe) { }

  date: string;
  formattedDate;

  fetchDate() {
    this.formattedDate = this.datePipe.transform(
      Date.now().toString(),
      "ddMMyyyy"
    );
    this.dateService
      .getDate(this.formattedDate)
      .subscribe((date) => (this.date = date));
  }
}

We have injected the DatePipe into the component. The DatePipe allows us to adjust the date string to our desired date format. After creating a function called fetchDate, we will format the current date to one of the date formats supported by our API. We will then invoke the getDate function of our DateService and pass the formatted date as a parameter.

Open src\app\custom-date\custom-date.component.html and replace what you see there with the following code:

<button class="btn btn-primary" (click)="fetchDate()">Fetch Date from backend</button>

<h4>Formattd date is:</h4>
<p>{{formattedDate}}</p>

<h4>Response from backend is:</h4>
<p>{{date}}</p>

We have added a button that will invoke the fetchDate function on click. The formatted date and the response returned from the API will be displayed on the screen.

Add the link to the Nav-menu

Open the src\app\nav-menu\nav-menu.component.html file and add the navigation link for the CustomDateComponent as shown below:

<li class="nav-item" [routerLinkActive]="['link-active']">
  <a class="nav-link text-dark" [routerLink]="['/custom-date']"
    >Custom Date</a
  >
</li>

Executing the demo

Run the code from Visual Studio. The app will be launched in the default browser. Click on the Custom Date link in the Nav-menu at the top. You can see the output as shown in the image below.

Conclusion

.NET’s default model binder does not allow binding to custom formats, so we have created a custom model binder. Our custom binder allows us to bind the custom date format passed as the API parameter. We have used Angular on the client project to demonstrate the custom date format binding.

If you now feel ready to localize your app, try Phrase. A software localization platform engineered to streamline app localization end to end, Phrase features a flexible API and CLI, as well as a beautiful web interface for effective collaboration with your translators. Check out all of Phrase’s features and try it for 14 days for free.

Rate this post
Comments
close

Untangle Continuous Localization With Ease

Get your own FREE EBOOK copy now to explore

  • advanced automation workflows
  • rapid release cycles,
  • simultaneous translation and delivery,
  • new ways of testing your localized product