Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DateOnly and TimeOnly support #129

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<PackageVersion Include="System.Data.Common" Version="4.3.0" />
<PackageVersion Include="System.Data.SqlClient" Version="4.8.5" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="Testcontainers.MsSql" Version="3.10.0" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="3.6.0" />
<!-- these are bound by Microsoft.CodeAnalysis.CSharp.*.Testing.XUnit -->
<PackageVersion Include="xunit" Version="[2.3.0]" />
Expand Down
40 changes: 40 additions & 0 deletions src/Dapper.AOT/Internal/CommandUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,46 @@ internal static T As<T>(object? value)
DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture);
return Unsafe.As<DateTime?, T>(ref t);
}
#if NET6_0_OR_GREATER
else if (typeof(T) == typeof(DateOnly))
{
if (value is DateOnly only) return Unsafe.As<DateOnly, T>(ref only);

DateTime t = Convert.ToDateTime(value, CultureInfo.InvariantCulture);
var dateOnly = DateOnly.FromDateTime(t);
return Unsafe.As<DateOnly, T>(ref dateOnly);
}
else if (typeof(T) == typeof(DateOnly?))
{
DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture);
DateOnly? dateOnly = t is null ? null : DateOnly.FromDateTime(t.Value);
return Unsafe.As<DateOnly?, T>(ref dateOnly);
}
else if (typeof(T) == typeof(TimeOnly))
{
if (value is TimeSpan timeSpan)
{
var fromSpan = TimeOnly.FromTimeSpan(timeSpan);
return Unsafe.As<TimeOnly, T>(ref fromSpan);
}

DateTime t = Convert.ToDateTime(value, CultureInfo.InvariantCulture);
var timeOnly = TimeOnly.FromDateTime(t);
return Unsafe.As<TimeOnly, T>(ref timeOnly);
}
else if (typeof(T) == typeof(TimeOnly?))
{
if (value is TimeSpan timeSpan)
{
var fromSpan = TimeOnly.FromTimeSpan(timeSpan);
return Unsafe.As<TimeOnly, T>(ref fromSpan);
}

DateTime? t = Convert.ToDateTime(value, CultureInfo.InvariantCulture);
TimeOnly? timeOnly = t is null ? null : TimeOnly.FromDateTime(t.Value);
return Unsafe.As<TimeOnly?, T>(ref timeOnly);
}
#endif
else if (typeof(T) == typeof(Guid) && (s = value as string) is not null)
{
Guid t = Guid.Parse(s);
Expand Down
3 changes: 3 additions & 0 deletions src/Dapper.AOT/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Dapper.AOT.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a17ba361da0990b3da23f3c20f2a002242397b452a28f27832d61d49f35edb54a68b98d98557b8a02be79be42142339c7861af309c8917dee972775e2c358dd6b96109a9147987652b25b8dc52e7f61f22a755831674f0a3cea17bef9abb6b23ef1856a02216864a1ffbb04a4c549258d32ba740fe141dad2f298a8130ea56d0")]
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net6.0;net48</TargetFrameworks>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
<RootNamespace>Dapper.AOT.Test.Integration.Executables</RootNamespace>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace Dapper.AOT.Test.Integration.Executables.Models;

public class DateOnlyTimeOnlyPoco
{
public const string TableName = "dateOnlyTimeOnly";

public static DateOnly SpecificDate => DateOnly.FromDateTime(new DateTime(year: 2022, month: 2, day: 2));
public static TimeOnly SpecificTime => TimeOnly.FromDateTime(new DateTime(year: 2022, month: 1, day: 1, hour: 10, minute: 11, second: 12));

public int Id { get; set; }
public DateOnly Date { get; set; }
public TimeOnly Time { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Data;
using System.Linq;
using Dapper.AOT.Test.Integration.Executables.Models;

namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly;

[DapperAot]
public class DateOnlyInsert : IExecutable<DateOnlyTimeOnlyPoco>
{
public DateOnlyTimeOnlyPoco Execute(IDbConnection connection)
{
connection.Execute(
$"""
insert into {DateOnlyTimeOnlyPoco.TableName}(id, date, time)
values (@id, @date, @time)
on conflict (id) do nothing;
""",
new DateOnlyTimeOnlyPoco { Id = 2, Date = DateOnlyTimeOnlyPoco.SpecificDate, Time = DateOnlyTimeOnlyPoco.SpecificTime }
);

var results = connection.Query<DateOnlyTimeOnlyPoco>(
$"""
select * from {DateOnlyTimeOnlyPoco.TableName}
where id = 2
"""
);

return results.First();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Data;
using System.Linq;
using Dapper.AOT.Test.Integration.Executables.Models;

namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly;

[DapperAot]
public class DateOnlyTimeOnlyUsage : IExecutable<DateOnlyTimeOnlyPoco>
{
public DateOnlyTimeOnlyPoco Execute(IDbConnection connection)
{
var results = connection.Query<DateOnlyTimeOnlyPoco>($"select * from {DateOnlyTimeOnlyPoco.TableName}");
return results.First();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Data;
using System.Linq;
using Dapper.AOT.Test.Integration.Executables.Models;

namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly;

[DapperAot]
public class DateOnlyUsageWithDateFilter : IExecutable<DateOnlyTimeOnlyPoco>
{
public DateOnlyTimeOnlyPoco Execute(IDbConnection connection)
{
var results = connection.Query<DateOnlyTimeOnlyPoco>(
$"""
select * from {DateOnlyTimeOnlyPoco.TableName}
where date = @date
""",
new { date = DateOnlyTimeOnlyPoco.SpecificDate }
);

return results.First();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Data;
using System.Linq;
using Dapper.AOT.Test.Integration.Executables.Models;

namespace Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly;

[DapperAot]
public class DateOnlyUsageWithTimeFilter : IExecutable<DateOnlyTimeOnlyPoco>
{
public DateOnlyTimeOnlyPoco Execute(IDbConnection connection)
{
var results = connection.Query<DateOnlyTimeOnlyPoco>(
$"""
select * from {DateOnlyTimeOnlyPoco.TableName}
where time = @time
""",
new { time = DateOnlyTimeOnlyPoco.SpecificTime }
);

return results.First();
}
}
69 changes: 69 additions & 0 deletions test/Dapper.AOT.Test.Integration/DateOnlyTimeOnlyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Data;
using Dapper.AOT.Test.Integration.Executables.Models;
using Dapper.AOT.Test.Integration.Executables.UserCode.DateOnlyTimeOnly;
using Dapper.AOT.Test.Integration.Setup;

namespace Dapper.AOT.Test.Integration;

[Collection(SharedPostgresqlClient.Collection)]
public class DateOnlyTimeOnlyTests : IntegrationTestsBase
{
public DateOnlyTimeOnlyTests(PostgresqlFixture fixture) : base(fixture)
{
}

protected override void SetupDatabase(IDbConnection dbConnection)
{
base.SetupDatabase(dbConnection);

dbConnection.Execute($"""
CREATE TABLE IF NOT EXISTS {DateOnlyTimeOnlyPoco.TableName}(
id integer PRIMARY KEY,
date DATE,
time TIME
);

TRUNCATE {DateOnlyTimeOnlyPoco.TableName};

INSERT INTO {DateOnlyTimeOnlyPoco.TableName} (id, date, time)
VALUES (1, '{DateOnlyTimeOnlyPoco.SpecificDate.ToString("yyyy-MM-dd")}', '{DateOnlyTimeOnlyPoco.SpecificTime.ToString("HH:mm:ss")}')
""");
}

[Fact]
public void DateOnly_BasicUsage_InterceptsAndReturnsExpectedData()
{
var result = ExecuteInterceptedUserCode<DateOnlyTimeOnlyUsage, DateOnlyTimeOnlyPoco>(DbConnection);
Assert.Equal(1, result.Id);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time);
}

[Fact]
public void DateOnly_WithDateFilter_InterceptsAndReturnsExpectedData()
{
var result = ExecuteInterceptedUserCode<DateOnlyUsageWithDateFilter, DateOnlyTimeOnlyPoco>(DbConnection);
Assert.Equal(1, result.Id);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time);
}

[Fact]
public void DateOnly_WithTimeFilter_InterceptsAndReturnsExpectedData()
{
var result = ExecuteInterceptedUserCode<DateOnlyUsageWithTimeFilter, DateOnlyTimeOnlyPoco>(DbConnection);
Assert.Equal(1, result.Id);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time);
}

[Fact]
public void DateOnly_Inserts_InterceptsAndReturnsExpectedData()
{
var result = ExecuteInterceptedUserCode<DateOnlyInsert, DateOnlyTimeOnlyPoco>(DbConnection);
Assert.Equal(2, result.Id);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificDate, result.Date);
Assert.Equal(DateOnlyTimeOnlyPoco.SpecificTime, result.Time);
}
}
29 changes: 29 additions & 0 deletions test/Dapper.AOT.Test.Integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Dapper.AOT.Test - Integration

This is a project for integration tests against a non-mocked database using generated interceptor bits.

### Requirements
Make sure you have Docker Desktop running to be able to initialize a container with PostgreSQL db (or other).

### How to add a new test
1) Add your tests to `Dapper.AOT.Test.Integration` and inherit the [IntegrationTestBase](./Setup/IntegrationTestsBase.cs) class
2) Override `SetupDatabase` with in example creating a table and filling it in with some data to query later
3) Call `IntegrationTestBase.ExecuteInterceptedUserCode<TUsage, TPoco>()` where:
- `TUsage` is a type specified in [Dapper.AOT.Test.Integration.Executables/UserCode](../Dapper.AOT.Test.Integration.Executables/UserCode) project
and implements [IExecutable](../Dapper.AOT.Test.Integration.Executables/IExecutable.cs) interface.
The method `IExecutable.Execute(DbConnection connection)` will be executed by the framework automatically
- `TPoco` is the return type of `IExecutable.Execute(DbConnection connection)`. `TPoco` has to be also defined in [Dapper.AOT.Test.Integration.Executables/Models](../Dapper.AOT.Test.Integration.Executables/Models)
4) Assert that returned `TPoco` has all the data you expected

### How test framework works

- Create compilation with reference to user code ([Dapper.AOT.Test.Integration.Executables](../Dapper.AOT.Test.Integration.Executables)) via `CSharpCompilation.Create()`
- give it to `ISourceGenerator` and get an output compilation
- call `outputCompilation.Emit()` and get an assembly
- create instance of user code via reflection (Activator) - this is where we need the `IExecutable` interface to properly cast the type to it
- call `IExecutable.Execute(DbConnection connection)` passing the connection to container-db
- get the output `object`, cast it back to `TPoco` (also defined in [Dapper.AOT.Test.Integration.Executables](../Dapper.AOT.Test.Integration.Executables)) and return it to the test.
Developer can write assertions based on the returned object

_note_: SourceGenerator is specifically created with the injected `IInterceptorRecorder`, which gets the stack trace of execution and verifies that generated bits were executed.
This gives a full confidence that Interceptors worked as expected. Also it reports diagnostics if something unexpected occured, and test verifies there are none of those.
16 changes: 13 additions & 3 deletions test/Dapper.AOT.Test.Integration/Setup/IntegrationTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Data.SqlClient;

namespace Dapper.AOT.Test.Integration.Setup;

Expand Down Expand Up @@ -61,6 +62,7 @@ protected TResult ExecuteInterceptedUserCode<TExecutable, TResult>(IDbConnection

// Additional stuff required by Dapper.AOT generators
MetadataReference.CreateFromFile(Assembly.Load("System.Collections").Location), // System.Collections
MetadataReference.CreateFromFile(Assembly.Load("System.Collections.Immutable").Location), // System.Collections.Immutable
],
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

Expand All @@ -77,8 +79,8 @@ protected TResult ExecuteInterceptedUserCode<TExecutable, TResult>(IDbConnection
var mainMethod = type.GetMethod(nameof(IExecutable<TExecutable>.Execute), BindingFlags.Public | BindingFlags.Instance);
var result = mainMethod!.Invoke(obj: executableInstance, [ dbConnection ]);

Assert.True(interceptorRecorder.WasCalled);
Assert.True(string.IsNullOrEmpty(interceptorRecorder.Diagnostics), userMessage: interceptorRecorder.Diagnostics);
Assert.True(interceptorRecorder.WasCalled, userMessage: "No interception code invoked");
Assert.True(string.IsNullOrEmpty(interceptorRecorder.Diagnostics), userMessage: $"Expected no diagnostics from interceptorRecorder. Actual: {interceptorRecorder.Diagnostics}");

return (TResult)result!;
}
Expand All @@ -96,6 +98,14 @@ private static string ReadUserSourceCode<TExecutable>()
// it's very fragile to get user code cs files into output directory (btw we can't remove them from compilation, because we will use them for assertions)
// so let's simply get back to test\ dir, and try to find Executables.UserCode from there
var testDir = Directory.GetParent(Directory.GetParent(Directory.GetParent(Directory.GetParent(Directory.GetCurrentDirectory())!.FullName)!.FullName)!.FullName);
return File.ReadAllText(Path.Combine(testDir!.FullName, "Dapper.AOT.Test.Integration.Executables", "UserCode", $"{userTypeName}.cs"));

var sourceCodeFile = Directory
.GetFiles(
path: Path.Combine(testDir!.FullName, "Dapper.AOT.Test.Integration.Executables", "UserCode"),
searchPattern: $"{userTypeName}.cs",
searchOption: SearchOption.AllDirectories)
.First();

return File.ReadAllText(sourceCodeFile);
}
}
1 change: 1 addition & 0 deletions test/Dapper.AOT.Test/Dapper.AOT.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Analyzer.Testing.XUnit" />
<PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.CodeFix.Testing.XUnit" />
<PackageReference Include="Testcontainers.PostgreSql" />
<PackageReference Include="Testcontainers.MsSql" />
</ItemGroup>

<ItemGroup>
Expand Down
28 changes: 28 additions & 0 deletions test/Dapper.AOT.Test/Helpers/TestFramework.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Dapper.AOT.Test.Helpers
{
internal static class TestFramework
{
public static readonly ISet<string> NetVersions
= ((Net[])Enum.GetValues(typeof(Net)))
.Select(static x => x.ToString())
.ToHashSet();

public static Net DetermineNetVersion()
{
#if NET6_0_OR_GREATER
return Net.Net6;
#endif
return Net.Net48;

Check warning on line 19 in test/Dapper.AOT.Test/Helpers/TestFramework.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 19 in test/Dapper.AOT.Test/Helpers/TestFramework.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 19 in test/Dapper.AOT.Test/Helpers/TestFramework.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected

Check warning on line 19 in test/Dapper.AOT.Test/Helpers/TestFramework.cs

View workflow job for this annotation

GitHub Actions / build

Unreachable code detected
}

public enum Net
{
Net48,
Net6
}
}
}
Loading