diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypes.Tests.csproj b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypes.Tests.csproj index db7467f52e..bb4f594afa 100644 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypes.Tests.csproj +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypes.Tests.csproj @@ -1,37 +1,37 @@  - net7.0 + net10.0 enable false - - + + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypesTests.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypesTests.cs index 38a517fc82..8dc3382d1d 100644 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypesTests.cs +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/DateOnlyAndTimeOnlyTypesTests.cs @@ -1,45 +1,90 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using System; +using System.Linq; using Xunit; namespace DateOnlyAndTimeOnlyTypes.Tests; public class DateOnlyAndTimeOnlyTypesTest { - private readonly AppDbContext _context; - - public DateOnlyAndTimeOnlyTypesTest() + private static AppDbContext CreateSqlServerContext() { - var contextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("DateOnlyDemo") - .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + // No connection is opened: these tests only build the model and generate SQL. + var options = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Database=DateOnlyDemo;Trusted_Connection=True;TrustServerCertificate=True") .Options; - _context = new AppDbContext(contextOptions); - _context.Database.EnsureDeleted(); - _context.Database.EnsureCreated(); + return new AppDbContext(options); } [Fact] - public void GivenDbContext_WhenHavingProperConverters_ThenRightlyStoresDateOnlyTimeOnlyValues() + public void GivenDbContext_WhenSavingDateOnlyTimeOnlyValues_ThenReadsThemBack() { - var employee = new Employee - { - Name = "John Doe", - BirthDate = new DateOnly(2023, 5, 9), - WorkStartTime = new TimeOnly(8, 30), - WorkEndTime = new TimeOnly(4, 30) + var contextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase("DateOnlyDemo") + .ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning)) + .Options; + + using var context = new AppDbContext(contextOptions); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + var employee = new Employee + { + Name = "John Doe", + BirthDate = new DateOnly(2023, 5, 9), + WorkStartTime = new TimeOnly(8, 30), + WorkEndTime = new TimeOnly(16, 30) }; - _context.Employees.Add(employee); - _context.SaveChanges(); + context.Employees.Add(employee); + context.SaveChanges(); - var storedEmployee = _context.Employees.Find(employee.Id); + var storedEmployee = context.Employees.Find(employee.Id); Assert.NotNull(storedEmployee); Assert.Equal(employee.BirthDate, storedEmployee!.BirthDate); Assert.Equal(employee.WorkStartTime, storedEmployee.WorkStartTime); Assert.Equal(employee.WorkEndTime, storedEmployee.WorkEndTime); } -} \ No newline at end of file + + [Fact] + public void GivenSqlServerProvider_WhenGeneratingSchema_ThenMapsDateOnlyToDateAndTimeOnlyToTime() + { + using var context = CreateSqlServerContext(); + + var script = context.Database.GenerateCreateScript(); + + Assert.Contains("[BirthDate] date NOT NULL", script); + Assert.Contains("[WorkStartTime] time NOT NULL", script); + Assert.Contains("[WorkEndTime] time NOT NULL", script); + } + + [Fact] + public void GivenDateOnlyMember_WhenFiltering_ThenTranslatesToSql() + { + using var context = CreateSqlServerContext(); + + var sql = context.Employees + .Where(e => e.BirthDate.Year == 1990 && e.WorkStartTime.Hour < 9) + .ToQueryString(); + + Assert.Contains("DATEPART(year, [e].[BirthDate]) = 1990", sql); + Assert.Contains("DATEPART(hour, [e].[WorkStartTime]) < 9", sql); + } + + [Fact] + public void GivenLegacyDatetime2Column_WhenUsingConverter_ThenKeepsDatetime2() + { + var options = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Database=LegacyDemo;Trusted_Connection=True;TrustServerCertificate=True") + .Options; + + using var context = new LegacyAppDbContext(options); + + var script = context.Database.GenerateCreateScript(); + + Assert.Contains("[BirthDate] datetime2 NOT NULL", script); + } +} diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/LegacyAppDbContext.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/LegacyAppDbContext.cs new file mode 100644 index 0000000000..c23dec0025 --- /dev/null +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.Tests/LegacyAppDbContext.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace DateOnlyAndTimeOnlyTypes.Tests; + +// An existing database keeps BirthDate in a datetime2 column that we can't change. +public class LegacyAppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Employees => Set(); + + protected override void ConfigureConventions(ModelConfigurationBuilder builder) + { + base.ConfigureConventions(builder); + + builder.Properties() + .HaveConversion() + .HaveColumnType("datetime2"); + } +} diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/AppDbContext.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/AppDbContext.cs index 2947be63cc..9ebaec252e 100644 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/AppDbContext.cs +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/AppDbContext.cs @@ -1,34 +1,24 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; - -namespace DateOnlyAndTimeOnlyTypes; - -public class AppDbContext : DbContext +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace DateOnlyAndTimeOnlyTypes; + +public class AppDbContext : DbContext { - public DbSet Employees => Set(); - - protected override void OnConfiguring(DbContextOptionsBuilder builder) - { - if (builder.IsConfigured) - return; - - var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - builder.UseSqlServer(configuration.GetConnectionString("Default")); - } - - protected override void ConfigureConventions(ModelConfigurationBuilder builder) + public DbSet Employees => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder builder) { - base.ConfigureConventions(builder); + if (builder.IsConfigured) + return; + + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + builder.UseSqlServer(configuration.GetConnectionString("Default")); + } - builder.Properties() - .HaveConversion() - .HaveColumnType("date"); + public AppDbContext() { } - builder.Properties() - .HaveConversion(); - } - - public AppDbContext(DbContextOptions options) : base(options) { } -} + public AppDbContext(DbContextOptions options) : base(options) { } +} diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.csproj b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.csproj index 9fa0bf4ae3..689ce6ef29 100644 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.csproj +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes.csproj @@ -2,34 +2,35 @@ Library - net7.0 + net10.0 enable enable - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + - - + + - - - PreserveNewest - + + + PreserveNewest + diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyComparer.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyComparer.cs deleted file mode 100644 index 791ce6240b..0000000000 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/DateOnlyComparer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore.ChangeTracking; - -namespace DateOnlyAndTimeOnlyTypes; - -public class DateOnlyComparer : ValueComparer -{ - public DateOnlyComparer() : base( - (x, y) => x.DayNumber == y.DayNumber, - dateOnly => dateOnly.GetHashCode()) - { } -} \ No newline at end of file diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/20261017201029_RemoveDateOnlyTimeOnlyConverters.Designer.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/20261017201029_RemoveDateOnlyTimeOnlyConverters.Designer.cs new file mode 100644 index 0000000000..6bfb82f213 --- /dev/null +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/20261017201029_RemoveDateOnlyTimeOnlyConverters.Designer.cs @@ -0,0 +1,56 @@ +// +using System; +using DateOnlyAndTimeOnlyTypes; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DateOnlyAndTimeOnlyTypes.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20261017201029_RemoveDateOnlyTimeOnlyConverters")] + partial class RemoveDateOnlyTimeOnlyConverters + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DateOnlyAndTimeOnlyTypes.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("WorkEndTime") + .HasColumnType("time"); + + b.Property("WorkStartTime") + .HasColumnType("time"); + + b.HasKey("Id"); + + b.ToTable("Employees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/20261017201029_RemoveDateOnlyTimeOnlyConverters.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/20261017201029_RemoveDateOnlyTimeOnlyConverters.cs new file mode 100644 index 0000000000..7fa13b299b --- /dev/null +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/20261017201029_RemoveDateOnlyTimeOnlyConverters.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DateOnlyAndTimeOnlyTypes.Migrations +{ + /// + public partial class RemoveDateOnlyTimeOnlyConverters : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/AppDbContextModelSnapshot.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/AppDbContextModelSnapshot.cs index 53a1d61cb9..abac12b99c 100644 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/AppDbContextModelSnapshot.cs +++ b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/Migrations/AppDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("ProductVersion", "10.0.13") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -30,17 +30,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("BirthDate") + b.Property("BirthDate") .HasColumnType("date"); b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("WorkEndTime") + b.Property("WorkEndTime") .HasColumnType("time"); - b.Property("WorkStartTime") + b.Property("WorkStartTime") .HasColumnType("time"); b.HasKey("Id"); diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/TimeOnlyComparer.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/TimeOnlyComparer.cs deleted file mode 100644 index 4937209447..0000000000 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/TimeOnlyComparer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore.ChangeTracking; - -namespace DateOnlyAndTimeOnlyTypes; - -public class TimeOnlyComparer : ValueComparer -{ - public TimeOnlyComparer() : base( - (x, y) => x.Ticks == y.Ticks, - timeOnly => timeOnly.GetHashCode()) - { } -} \ No newline at end of file diff --git a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/TimeOnlyConverter.cs b/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/TimeOnlyConverter.cs deleted file mode 100644 index d967f50cb5..0000000000 --- a/dotnet-efcore/DateOnlyAndTimeOnlyTypes/DateOnlyAndTimeOnlyTypes/TimeOnlyConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace DateOnlyAndTimeOnlyTypes; - -public class TimeOnlyConverter : ValueConverter -{ - public TimeOnlyConverter() : base( - timeOnly => timeOnly.ToTimeSpan(), - timeSpan => TimeOnly.FromTimeSpan(timeSpan)) - { } -} \ No newline at end of file