Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
publicize sqllite datetime column conversion (#945)
- Loading branch information
Showing
6 changed files
with
338 additions
and
1 deletion.
There are no files selected for viewing
File renamed without changes.
141 changes: 141 additions & 0 deletions
141
src/Whipstaff.Entityframework.Relational/ModelBuilderHelpers.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
// Copyright (c) 2020 DHGMS Solutions and Contributors. All rights reserved. | ||
// DHGMS Solutions and Contributors licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Metadata; | ||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||
|
||
namespace Whipstaff.EntityFramework | ||
{ | ||
/// <summary> | ||
/// Entity Framework Model Builder Helpers for SQL Lite. | ||
/// </summary> | ||
public static class ModelBuilderHelpers | ||
{ | ||
/// <summary> | ||
/// Converts all DBSet entities that contain a DateTimeOffset column to use a long database type within SQL Lite. | ||
/// Is used to workaround a limitation in SQL lite where you can't store as a DateTimeOffset and the workaround | ||
/// is to use a string or DateTime and lose the precision. Instead, so you don't need to adjust your model to cater | ||
/// for SQL lite, you can retain the ability of databases that do support it, but use SQL lite for testing. | ||
/// The caveat is that SQL lite loses timezone precision as it converts everything to UTC, but then you should | ||
/// probably be storing the data as UTC anyway. | ||
/// </summary> | ||
/// <param name="modelBuilder">Entity Framework Model Builder being configured.</param> | ||
public static void ConvertAllDateTimeOffSetPropertiesOnModelBuilderToLong(ModelBuilder modelBuilder) | ||
{ | ||
if (modelBuilder == null) | ||
{ | ||
throw new ArgumentNullException(nameof(modelBuilder)); | ||
} | ||
|
||
foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()) | ||
{ | ||
InternalConvertAllDateTimeOffSetPropertiesOnMutableEntityToLong( | ||
modelBuilder, | ||
mutableEntityType); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Converts all properties on a DBSet entity that are a DateTimeOffset column to use a long database type within SQL Lite. | ||
/// Is used to workaround a limitation in SQL lite where you can't store as a DateTimeOffset and the workaround | ||
/// is to use a string or DateTime and lose the precision. Instead, so you don't need to adjust your model to cater | ||
/// for SQL lite, you can retain the ability of databases that do support it, but use SQL lite for testing. | ||
/// The caveat is that SQL lite loses timezone precision as it converts everything to UTC, but then you should | ||
/// probably be storing the data as UTC anyway. | ||
/// </summary> | ||
/// <param name="modelBuilder">Entity Framework Model Builder being configured.</param> | ||
/// <param name="mutableEntityType">Mutable Entity Type Representing the DBSet to check.</param> | ||
public static void ConvertAllDateTimeOffSetPropertiesOnMutableEntityToLong( | ||
ModelBuilder modelBuilder, | ||
IMutableEntityType mutableEntityType) | ||
{ | ||
if (modelBuilder == null) | ||
{ | ||
throw new ArgumentNullException(nameof(modelBuilder)); | ||
} | ||
|
||
if (mutableEntityType == null) | ||
{ | ||
throw new ArgumentNullException(nameof(mutableEntityType)); | ||
} | ||
|
||
InternalConvertAllDateTimeOffSetPropertiesOnMutableEntityToLong( | ||
modelBuilder, | ||
mutableEntityType); | ||
} | ||
|
||
/// <summary> | ||
/// Converts a property properties on a DBSet entity that are a DateTimeOffset column to use a long database type within SQL Lite. | ||
/// Is used to workaround a limitation in SQL lite where you can't store as a DateTimeOffset and the workaround | ||
/// is to use a string or DateTime and lose the precision. Instead, so you don't need to adjust your model to cater | ||
/// for SQL lite, you can retain the ability of databases that do support it, but use SQL lite for testing. | ||
/// The caveat is that SQL lite loses timezone precision as it converts everything to UTC, but then you should | ||
/// probably be storing the data as UTC anyway. | ||
/// </summary> | ||
/// <param name="modelBuilder">Entity Framework Model Builder being configured.</param> | ||
/// <param name="entityClrType">The CLR type of the Entity represented as a DBSet.</param> | ||
/// <param name="propertyName">The name of the property that's to be converted from DateTimeOffset.</param> | ||
public static void ConvertDateTimeOffSetPropertyToLong( | ||
ModelBuilder modelBuilder, | ||
Type entityClrType, | ||
string propertyName) | ||
{ | ||
if (modelBuilder == null) | ||
{ | ||
throw new ArgumentNullException(nameof(modelBuilder)); | ||
} | ||
|
||
if (entityClrType == null) | ||
{ | ||
throw new ArgumentNullException(nameof(modelBuilder)); | ||
} | ||
|
||
if (string.IsNullOrWhiteSpace(propertyName)) | ||
{ | ||
throw new ArgumentNullException(nameof(propertyName)); | ||
} | ||
|
||
InternalConvertDateTimeOffSetPropertyToLong( | ||
modelBuilder, | ||
entityClrType, | ||
propertyName); | ||
} | ||
|
||
/// <summary> | ||
/// Gets a value convertor for converting a date time offset to unix time milliseconds as a long. | ||
/// </summary> | ||
/// <returns>Value convertor.</returns> | ||
public static ValueConverter<DateTimeOffset, long> GetDateTimeOffSetToUnixTimeMillisecondsLongValueConverter() => | ||
new ValueConverter<DateTimeOffset, long>( | ||
offset => offset.ToUnixTimeMilliseconds(), | ||
milliseconds => DateTimeOffset.FromUnixTimeMilliseconds(milliseconds)); | ||
|
||
private static void InternalConvertAllDateTimeOffSetPropertiesOnMutableEntityToLong(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) | ||
{ | ||
foreach (var p in mutableEntityType.GetProperties()) | ||
{ | ||
if (p.ClrType == typeof(DateTimeOffset)) | ||
{ | ||
ConvertDateTimeOffSetPropertyToLong( | ||
modelBuilder, | ||
mutableEntityType.ClrType, | ||
p.Name); | ||
} | ||
} | ||
} | ||
|
||
private static void InternalConvertDateTimeOffSetPropertyToLong( | ||
ModelBuilder modelBuilder, | ||
Type entityClrType, | ||
string propertyName) | ||
{ | ||
modelBuilder.Entity(entityClrType) | ||
.Property(propertyName) | ||
.HasColumnType("INTEGER") | ||
.HasConversion(GetDateTimeOffSetToUnixTimeMillisecondsLongValueConverter()); | ||
} | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/Whipstaff.Entityframework.Relational/Whipstaff.Entityframework.Relational.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" /> | ||
</ItemGroup> | ||
|
||
</Project> |
177 changes: 177 additions & 0 deletions
177
src/Whipstaff.UnitTests/EntityFramework/Relational/SqlLiteDateTimeOffsetOrdering.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// Copyright (c) 2020 DHGMS Solutions and Contributors. All rights reserved. | ||
// DHGMS Solutions and Contributors licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for full license information. | ||
|
||
using System; | ||
using System.Data.Common; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using System.Threading.Tasks; | ||
using Microsoft.Data.Sqlite; | ||
using Microsoft.EntityFrameworkCore; | ||
using Microsoft.EntityFrameworkCore.Internal; | ||
using Microsoft.EntityFrameworkCore.Metadata.Conventions; | ||
using Whipstaff.EntityFramework; | ||
using Xunit; | ||
|
||
namespace Whipstaff.UnitTests.EntityFramework.Relational | ||
{ | ||
/// <summary> | ||
/// Unit Tests for SQL Lite DateTimeOffset ordering. | ||
/// </summary> | ||
public static class SqlLiteDateTimeOffsetOrdering | ||
{ | ||
/// <summary> | ||
/// Represents a test entity. | ||
/// </summary> | ||
public sealed class TestEntity | ||
{ | ||
/// <summary> | ||
/// Gets or sets the Id. | ||
/// </summary> | ||
public int Id { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the date time with offset. | ||
/// </summary> | ||
public DateTimeOffset DateTimeOffset { get; set; } | ||
} | ||
|
||
/// <summary> | ||
/// Represents the db context common functionality for testing. | ||
/// </summary> | ||
public class BaseTestDbContext : DbContext | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="BaseTestDbContext"/> class. | ||
/// </summary> | ||
/// <param name="options">Database context options.</param> | ||
public BaseTestDbContext(DbContextOptions options) | ||
: base(options) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the test entity db set. | ||
/// </summary> | ||
public DbSet<TestEntity> TestEntity => Set<TestEntity>(); | ||
} | ||
|
||
/// <summary> | ||
/// Represents a test db context that has the database model specific options injected into the constructor. | ||
/// </summary> | ||
public sealed class TestWithContextOptionsDbContext : BaseTestDbContext | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="TestWithContextOptionsDbContext"/> class. | ||
/// </summary> | ||
/// <param name="options">Database context options.</param> | ||
public TestWithContextOptionsDbContext(DbContextOptions options) | ||
: base(options) | ||
{ | ||
// TODO: check the extension the model is defined in. | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Represents a test db context that sets the database model up via the OnModelCreating override method. | ||
/// </summary> | ||
public sealed class TestWithOnModelCreatingDbContext : BaseTestDbContext | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="TestWithOnModelCreatingDbContext"/> class. | ||
/// </summary> | ||
/// <param name="options">Database context options.</param> | ||
public TestWithOnModelCreatingDbContext(DbContextOptions options) | ||
: base(options) | ||
{ | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override void OnModelCreating(ModelBuilder modelBuilder) | ||
{ | ||
base.OnModelCreating(modelBuilder); | ||
|
||
ModelBuilderHelpers.ConvertAllDateTimeOffSetPropertiesOnModelBuilderToLong(modelBuilder); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Unit Tests for the OrderBy method. | ||
/// </summary> | ||
public sealed class OrderByMethod | ||
{ | ||
/// <summary> | ||
/// Test to ensure data is returned. | ||
/// </summary> | ||
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> | ||
[Fact] | ||
public async Task ReturnsData() | ||
{ | ||
var dbContextOptionsBuilder = new DbContextOptionsBuilder(); | ||
using (var connection = CreateInMemoryDatabase()) | ||
{ | ||
_ = dbContextOptionsBuilder.UseSqlite(connection); | ||
|
||
using (var dbContext = new TestWithOnModelCreatingDbContext(dbContextOptionsBuilder.Options)) | ||
{ | ||
_ = dbContext.Database.EnsureCreated(); | ||
|
||
var result = await dbContext.TestEntity | ||
.Where(GetSelector()) | ||
.ToArrayAsync() | ||
.ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Test to ensure data is returned. | ||
/// </summary> | ||
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns> | ||
[Fact] | ||
public async Task ReturnsData2() | ||
{ | ||
var dbContextOptionsBuilder = new DbContextOptionsBuilder(); | ||
|
||
using (var connection = CreateInMemoryDatabase()) | ||
{ | ||
_ = dbContextOptionsBuilder.UseSqlite(connection); | ||
|
||
var modelBuilder = SqliteConventionSetBuilder.CreateModelBuilder(); | ||
_ = modelBuilder.Entity<TestEntity>(); | ||
|
||
ModelBuilderHelpers.ConvertAllDateTimeOffSetPropertiesOnModelBuilderToLong(modelBuilder); | ||
|
||
var model = modelBuilder.FinalizeModel(); | ||
|
||
dbContextOptionsBuilder = dbContextOptionsBuilder.UseModel(model); | ||
|
||
using (var dbContext = new TestWithContextOptionsDbContext(dbContextOptionsBuilder.Options)) | ||
{ | ||
_ = dbContext.Database.EnsureCreated(); | ||
|
||
var result = await dbContext.TestEntity | ||
.Where(GetSelector()) | ||
.ToArrayAsync() | ||
.ConfigureAwait(false); | ||
} | ||
} | ||
} | ||
|
||
private static DbConnection CreateInMemoryDatabase() | ||
{ | ||
var connection = new SqliteConnection("Filename=:memory:"); | ||
|
||
connection.Open(); | ||
|
||
return connection; | ||
} | ||
|
||
private static Expression<Func<TestEntity, bool>> GetSelector() | ||
{ | ||
return entity => entity.Id > 0; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters