Skip to content

Entity Framework Core 6 - DateOnly, TimeOnly

Published: at 00:00

Bevezetés

Az Entity Framework Core 6 remek támogatást nyújt ahhoz, hogy meghatározzuk az objektumaink egy-egy tulajdonsága milyen formában kerüljön tárolásra. Lehetőségünk van felülírni a konvenciókat és akár kontextus szinten is konfigurálhatjuk, hogy egy-egy típust hogyan is kezeljen.

A “régi” entity framework verzióban az első esetben trükközni kellett (elég az enum szöveges mezőbe történő mentésre gondolni). A második esetet nevezték egyedi konvenciónak (custom convention) és a Convention ősosztályból származtatott egyedi osztályba helyeztük a kódot. Természetesen egyszerűen a ModelBuilderen is beállíthattuk ezeket, de ez idővel az alkalmazás növekedésével egyre nagyobb átláthatatlanságot eredményezett.

Entity Framework Core 6 - Példa

// EmployeeDbContext.cs
public class EmployeeDbContext : DbContext
{
    public DbSet<Employee> Employees => Set<Employee>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(Configuration.ConnectionString);
}
public class Employee
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public DateOnly Birthdate { get; set; }
    public TimeOnly ShiftStartTime { get; set; }
}

// Program.cs
using var context = new EmployeeDbContext();

context.Database.EnsureDeleted();
context.Database.EnsureCreated();

await context.Employees.AddAsync(new Employee
{
    Name = "Teszt Elek",
    Birthdate = new DateOnly(1987, 01, 07),
    ShiftStartTime = new TimeOnly(10, 0)
});
await context.SaveChangesAsync();

Amennyiben ezt így megpróbálnánk futtatni egyből kapnánk a hibaüzenetet, ami azt tartalmazná, hogy a DateOnly tulajdonsággal nem tudd mit kezdeni. A feladat az lesz, hogy a Birthdate tulajdonságot egy datetime mezőbe a ShiftStartTime tulajdonságot pedig egy nvarchar mezőbe mentsük. Azt fontos megemlíteni, hogy az Sql szerver használata esetén jelentkezik ez a probléma, de a SQLite esetén már van rá támogatás.

Property alapú megoldás

Amire szükségünk lesz az a DbContext osztályunkban OnModelCreating metódus felülírása. A kapott modelBuilder paraméteren az alább látható módon kiválasztjuk a tulajdonságot.

 modelBuilder.Entity<Employee>().Property(e => e.Birthdate)

És akkor most van szükségünk a HasConversion metódusra. Ennek többféle paraméterezése is lehetséges. Az egyik, hogy megadunk két expressiont. Ebben az esetben az első paraméter meghatározza, hogy hogyan lesz az eredeti típusból a céltípus. A második paraméter, pedig a fordított irányt írja le.

Az említett példánkat alapul véve az alábbi kódot rakhatjuk össze:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Employee>().Property(e => e.Birthdate)
        .HasConversion(dateOnly => dateOnly.ToDateTime(TimeOnly.MinValue), dateTime => DateOnly.FromDateTime(dateTime));

    modelBuilder.Entity<Employee>().Property(e => e.ShiftStartTime)
        .HasConversion(timeOnly => timeOnly.ToString(), str => TimeOnly.Parse(str))
        .HasMaxLength(5);
}

A célunk ennyivel is elértük, de nézzük meg a további lehetőségeinket. Jó lenne ha a kifejezések nem lennének beágyazva a fluent konfigurációs részbe. A ValueConverter osztályt használva különválaszthatjuk az alábbi módon:

var converter = new ValueConverter<DateOnly, DateTime>(
    dateOnly => dateOnly.ToDateTime(TimeOnly.MinValue),
    dateTime => DateOnly.FromDateTime(dateTime));

modelBuilder.Entity<Employee>().Property(e => e.Birthdate)
    .HasConversion(converter);

Innen már csak egy lépés, hogy külön osztályba szervezzük az egészet.

public class DateOnlyConverter : ValueConverter<DateOnly, DateTime>
{
    public DateOnlyConverter() 
        : base(
              dateOnly => dateOnly.ToDateTime(TimeOnly.MinValue),
              dateTime => DateOnly.FromDateTime(dateTime))
    {
    }
}

// használata
modelBuilder.Entity<Employee>().Property(e => e.Birthdate)
                .HasConversion<DateOnlyConverter>();

Ugyanezt a refaktorálást végig lehet vinni a TimeOnly konvertálással is.

 public class TimeOnlyToStringConverter : ValueConverter<TimeOnly, string>
    {
        public TimeOnlyToStringConverter() 
            : base(
                  timeOnly => timeOnly.ToString(),
                  stringTime => TimeOnly.Parse(stringTime))
        {
        }
    }

Konvenció

Nézzük meg, hogyan oldható meg, hogy az egész contextusunkban a fenti konverzió legyen érvényes alapértelmezetten. Ehhez a ConfigureConventions metódus felülírása szükséges és benne a konvertáló osztály megadása az adott típus esetén.

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    base.ConfigureConventions(configurationBuilder);
    configurationBuilder.Properties<DateOnly>().HaveConversion<DateOnlyConverter>();
}

Végeredmény

Végül a DbContextünk az alábbi módon néz ki:

public class EmployeeDbContext : DbContext
{
    public DbSet<Employee> Employees => Set<Employee>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer(Configuration.ConnectionString);

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        modelBuilder.Entity<Employee>().Property(e => e.ShiftStartTime)
           .HasConversion<TimeOnlyToStringConverter>()
           .HasMaxLength(5);
    }

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        base.ConfigureConventions(configurationBuilder);
        configurationBuilder.Properties<DateOnly>().HaveConversion<DateOnlyConverter>();
    }
}

Természetesen a ValueConverter osztály használatával más/saját típusok megadott formában történő mentése is lehetséges.


Previous Post
Színes-szagos tabok - Visual Studio 2022 (17.3)
Next Post
Web API - .NET 5-ről .NET 6-ra átállás