Working with Noda Time, .Net and Entity Framework (EF core)

Previous Article

What Is Noda Time?

Noda Time is an open-source date and time library for .NET applications. It is designed to provide a more robust and precise alternative to the built-in date and time types in the .NET Framework. Noda Time is particularly useful when dealing with complex time zone calculations, intervals, and different calendar systems.

What Is System.datetime?

System.DateTime is a fundamental type in the .NET Framework and .NET Core (and later) that represents a point in time. It's part of the base class library and is widely used for handling dates and times in .NET applications.

What is the main issue with DateTime?

Noda Time exists for .NET for the same reason that Joda Time exists for Java: the built-in libraries for handling dates and times are inadequate. Both platforms provide far too few types to represent date and time values in a way which encourages the developer to really consider what kind of data they're dealing with which in turn makes it hard to treat the data consistently.

What is Entity Framework?

Entity Framework (EF) is an object-relational mapper that enables .NET developers to work with relational data using domain-specific objects.

Implementing .Net EF core application with NodaTime

Step 1. Create a .net core WebAPI project as 'PersonDetails_NodaTime'.

Step 2. Include the class library project NodaTime_classlibrary that is created in previous article 'Creating Unit Test project for NodaTime (NodaTime classLibrary project)'. 

NodaTime_classlibrary

Step 3. Install the NodaTime package/library from the Manage NuGet package or enter the below command in the package manager console. This is the main library for NodaTime.

Install-Package NodaTime

Install the NodaTime Serialization package/library as below in the package manager console. This library is useful when serializing the NodaTime type.

Install-Package NodaTime.Serialization.JsonNet

Install the NodaTime Testing library for building the Unit test project.

Install-Package NodaTime.Testing

Install below packages for EntityFrameWorkCore :

  1. Microsoft.EntityFrameworkCore
  2. Microsoft.EntityFrameworkCore.Design
  3. Microsoft.EntityFrameworkCore.Tools
  4. Microsoft.EntityFrameworkCore.SqlServer

Step 4. Add project reference of NodaTime_classlibrary project to WebApi project.

add project reference of NodaTime_classlibrary project to WebApi project.

Step 5. Add connection string for DB and TimeZoneID at appsettings.json file of WebApi project.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "PersonDB": "Data Source=**********\\sqlexpress;Initial Catalog=Persons;trusted_connection=true;TrustServerCertificate=True;"
  },
  "TimeZoneID": "America/New_York"
}

Step 6. Add below code into program.cs file of WebApi project which contains details about the connection string, timezoneId, and other details.

using Microsoft.EntityFrameworkCore;
using NodaTime;
using NodaTime.Serialization.SystemTextJson;
using System.Text.Json;
using PersonDetails_NodaTime.DBContexts;
using PersonDetails_NodaTime.Repository;
using TimeManagement = NodaTime_ClassLibrary;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);        
        options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.WriteIndented = true;

    });

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var conn = builder.Configuration.GetConnectionString("PersonDB");

var objBuilder = new ConfigurationBuilder()
          .SetBasePath(Directory.GetCurrentDirectory())
          .AddJsonFile("appSettings.json", optional: true, reloadOnChange: true);
IConfiguration configuration = objBuilder.Build();

string timeZoneId = configuration.GetValue<string>("TimeZoneID");
builder.Services.AddSingleton<TimeManagement.IDateTimeProvider>(new TimeManagement.DateTimeProvider(DateTimeZoneProviders.Tzdb[timeZoneId]));

builder.Services.AddDbContext<PersonContext>(options =>
{
    options.UseSqlServer(conn);
});
builder.Services.AddTransient<IPersonRepository, PersonRepository>();

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();

Step 7. Create a folder as Models and Person Entiry and add below details which includes NodaTime types.

 public class Person
 {
     public int Id { get; set; }
     public string FirstName { get; set; }
     public string LastName { get; set; }
     public string SSN { get; set; }      
     public Instant InsertDte { get; set; }
     public LocalDate DOB { get; set; }
     public LocalTime WorkStartsAt { get; set; }
     public ZonedDateTime CurrentTime { get; set; }
     public OffsetDateTime CurrentTimeOffser { get; set; }
     public LocalDateTime CurrentTimeOnser { get; set; }
 }

Step 8. Create a folder called Repository and add IPersonRepository and PersonRepository class with below details.

IPersonRepository.cs

public interface IPersonRepository
{        
    IEnumerable<Person> GetPersons();

    Person GetPersonByID(int personID);

    void InsertPerson(Person person);

    void UpdatePerson(Person person);

    void Save();
}

PersonRepository.cs

public class PersonRepository : IPersonRepository
{
    private readonly PersonContext _dbContext;

    public PersonRepository(PersonContext dbContext)
    {
        _dbContext = dbContext;
    }

    public void Save()
    {
        _dbContext.SaveChanges();
    }

    public IEnumerable<Person> GetPersons()
    {
        return _dbContext.Persons.ToList();
    }

    public Person GetPersonByID(int personId)
    {
        return _dbContext.Persons.Find(personId);
    }

    public void InsertPerson(Person person)
    {
        _dbContext.Add(person);
        Save();
    }

    public void UpdatePerson(Person person)
    {
        _dbContext.Entry(person).State = EntityState.Modified;
        Save();
    }
}

Step 9. Create a folder called Controller and add PersonController with below details for CRUD operation

PersonController.cs

[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
    private readonly IPersonRepository _personRepository;
    private readonly TimeManagement.IDateTimeProvider _dateTimeZone;

    public PersonController(IPersonRepository personRepository, TimeManagement.IDateTimeProvider dateTimeProvider)
    {
        _personRepository = personRepository;
        _dateTimeZone = dateTimeProvider;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var persons = _personRepository.GetPersons();
        return new OkObjectResult(persons);
    }

    [HttpGet("{id}", Name = "Get")]
    public IActionResult Get(int id)
    {
        var person = _personRepository.GetPersonByID(id);
       
        var options = new JsonSerializerOptions
        {
            Converters = { NodaConverters.LocalTimeConverter, NodaConverters.LocalDateConverter, NodaConverters.InstantConverter,
                            NodaConverters.OffsetDateTimeConverter, NodaConverters.LocalDateTimeConverter, new ZonedDateTimeJsonConverter() },
            WriteIndented = true,
            PropertyNamingPolicy = default,
        };

        string json = JsonSerializer.Serialize(person, options);
        Console.WriteLine(json);

        return new OkObjectResult(person);
    }

    [HttpPost]
    public IActionResult Post([FromBody] Person person)
    {
        using (var scope = new TransactionScope())
        {
            person.InsertDte = _dateTimeZone.Now;
            person.CurrentTime = _dateTimeZone.CurrentDateTime;
            person.WorkStartsAt = _dateTimeZone.CurrentTime;
            _personRepository.InsertPerson(person);
            scope.Complete();
            return CreatedAtAction(nameof(Get), new { id = person.Id }, person);
        }
    }

    [HttpPut]
    public IActionResult Put([FromBody] Person person)
    {
        if (person != null)
        {
            using (var scope = new TransactionScope())
            {
                person.InsertDte = _dateTimeZone.Now;
                person.CurrentTime = _dateTimeZone.CurrentDateTime;
                person.WorkStartsAt = _dateTimeZone.CurrentTime;

                _personRepository.UpdatePerson(person);
                scope.Complete();
                return new OkResult();
            }
        }
        return new NoContentResult();
    }

}

Step 10. Add DbContext folder and PersonContext with below details for seeding the values for Person entiry and referring the converters to NodaType to DB types.

PersonContext.cs

 public class PersonContext : DbContext
 {
     private readonly IConfiguration _configuration;
     private readonly TimeManagement.IDateTimeProvider _dateTimeZone;

     public PersonContext(DbContextOptions<PersonContext> options, TimeManagement.IDateTimeProvider dateTimeProvider) : base(options)
     {
         _dateTimeZone = dateTimeProvider;
     }

     public DbSet<Person> Persons { get; set; }

     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         modelBuilder.Entity<Person>().HasData(
             new Person
             {
                 Id = 1,
                 FirstName = "A_alice",
                 LastName = "SuperSecret_A",
                 SSN = "000000001",
                 InsertDte = _dateTimeZone.Now,
                 DOB = _dateTimeZone.CurrentDate, 
                 WorkStartsAt = _dateTimeZone.CurrentTime, 
                 CurrentTime = _dateTimeZone.CurrentDateTime,
                 CurrentTimeOnser = _dateTimeZone.Today,
                 CurrentTimeOffser = _dateTimeZone.CurrentDateTimeOffset,
             },
             new Person
             {
                 Id = 2,
                 FirstName = "B_alice",
                 LastName = "SuperSecret_B",
                 SSN = "000000002",
                 InsertDte = _dateTimeZone.Now,
                 DOB = _dateTimeZone.CurrentDate, 
                 WorkStartsAt = _dateTimeZone.CurrentTime, 
                 CurrentTime = _dateTimeZone.CurrentDateTime,
                 CurrentTimeOnser = _dateTimeZone.Today,
                 CurrentTimeOffser = _dateTimeZone.CurrentDateTimeOffset,
             },
             new Person
             {
                 Id = 3,
                 FirstName = "C_alice",
                 LastName = "SuperSecret_C",
                 SSN = "000000003",
                 InsertDte = _dateTimeZone.Now,
                 WorkStartsAt = _dateTimeZone.CurrentTime,
                 CurrentTime = _dateTimeZone.CurrentDateTime,
                 CurrentTimeOnser = _dateTimeZone.Today,
                 CurrentTimeOffser = _dateTimeZone.CurrentDateTimeOffset,
             }
         );

         modelBuilder.Entity<Person>()
             .Property(x => x.DOB)
             .HasConversion(new LocalDateConverter())
             .HasColumnType("date");

         modelBuilder.Entity<Person>()
            .Property(x => x.InsertDte)
            .HasConversion(new InstantConverter())
            .HasColumnType("datetime2");

         modelBuilder.Entity<Person>()
            .Property(x => x.WorkStartsAt)
            .HasConversion(new LocalTimeConverter())
            .HasColumnType("time");

         modelBuilder.Entity<Person>()
            .Property(x => x.CurrentTime)
            .HasConversion(new ZonedDateTimeConverter())
            .HasColumnType("datetimeoffset");

         modelBuilder.Entity<Person>()
         .Property(x => x.CurrentTimeOffser)
         .HasConversion(new OffsetDateTimeConverter())
         .HasColumnType("datetimeoffset");

         modelBuilder.Entity<Person>()
           .Property(x => x.CurrentTimeOnser)
           .HasConversion(new LocalDateTimeConverter());
     }
 }

Step 11. Now add and update migration to create Persons table in database and insert initial values.

  • Open the Tools menu (available in the Visual Studio toolbar)

  • Select NuGet package manager

  • Then Select Package Manager Console

Package Manager Console Description
add-migration [name] Create a new migration with the specific migration name.
remove-migration Remove the latest migration.
update-database Update the database to the latest migration.
update-database [name] Update the database to a specific migration name point.
get-migrations Lists all available migrations.
script-migration Generates an SQL script for all migrations.
drop-database Drop the database.

Run the add-migration [name] command to generate a DB migration file.

add-migration

The above command will create a Migrations folder and add a migration script with the mentioned name and Snapshot.

add-migration

Then, run the update-database command to reflect the migration change on our database side.

update-databaseupdate-database

Step 12. Once the update database runs successfully, you will verify your Database in the SQL Server like this.

verify your Database

Verity schema of the table Persons at DB.

verify schema

Run the select query to verify the seed values have been inserted.

run select query

Step 13. Build and Run the application. If Build is a success, then the swagger page will be loaded with the API's.

Build and Run the application

Step 14. Now let's try to Insert values using POST API.

Insert values using POST API

Insert values using POST API

Verify the same in DB,

Insert values using POST API

Step 15. Now let's try to Update values using PUT API.

Update values using POST API

Update values using POST API

Verify the same Update in DB,

Update values using POST API

Step 16. Now let's try to Get a record using Get{ID} API.

Get a record using Get{ID} API

Get a record using Get{ID} API

Step 17.  Now let's try to Get all records using Get API.

Get all record using Get API

Get all record using Get API

Summary

NodaTime offers a robust and flexible solution for handling date and time in .NET applications, addressing many of the limitations and ambiguities present in the standard DateTime types. It can be especially useful in scenarios where the limitations of the standard .NET DateTime types become apparent. It was developed by Jon Skeet and is actively maintained as an open-source project.


Similar Articles