Skip to content

Commit

Permalink
NodaTime plugin
Browse files Browse the repository at this point in the history
Closes #333
  • Loading branch information
roji committed May 15, 2018
1 parent 621bdaf commit 5655b1c
Show file tree
Hide file tree
Showing 11 changed files with 1,028 additions and 2 deletions.
15 changes: 15 additions & 0 deletions EFCore.PG.sln
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.NTS", "src\EFCore
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.Plugins.FunctionalTests", "test\EFCore.PG.Plugins.FunctionalTests\EFCore.PG.Plugins.FunctionalTests.csproj", "{B78A7825-BE72-4509-B0AD-01EEC67A9624}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.PG.NodaTime", "src\EFCore.PG.NodaTime\EFCore.PG.NodaTime.csproj", "{77F0608F-6D0C-481C-9108-D5176E2EAD69}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -80,6 +82,18 @@ Global
{B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x64.Build.0 = Release|Any CPU
{B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x86.ActiveCfg = Release|Any CPU
{B78A7825-BE72-4509-B0AD-01EEC67A9624}.Release|x86.Build.0 = Release|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|x64.ActiveCfg = Debug|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|x64.Build.0 = Debug|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|x86.ActiveCfg = Debug|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Debug|x86.Build.0 = Debug|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Release|Any CPU.Build.0 = Release|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Release|x64.ActiveCfg = Release|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Release|x64.Build.0 = Release|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Release|x86.ActiveCfg = Release|Any CPU
{77F0608F-6D0C-481C-9108-D5176E2EAD69}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -90,6 +104,7 @@ Global
{05A7D0B7-4AE1-4BC8-A1BE-2389F1593B2D} = {ED612DB1-AB32-4603-95E7-891BACA71C39}
{78E89DB4-233B-4F93-A405-A1849D8B1A85} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}
{B78A7825-BE72-4509-B0AD-01EEC67A9624} = {ED612DB1-AB32-4603-95E7-891BACA71C39}
{77F0608F-6D0C-481C-9108-D5176E2EAD69} = {8537E50E-CF7F-49CB-B4EF-3E2A1B11F050}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F4EAAE6D-758C-4184-9D8C-7113384B61A8}
Expand Down
50 changes: 50 additions & 0 deletions doc/mapping/nodatime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Date/Time Mapping with NodaTime

> [!NOTE]
> This feature is only available in Npgsql 4.0, which is currently in preview.
By default, [the PostgreSQL date/time types](https://www.postgresql.org/docs/current/static/datatype-datetime.html) are mapped to the built-in .NET types (`DateTime`, `TimeSpan`). Unfortunately, these built-in types (`DateTime`, `DateTimeOffset`) are flawed in many ways; regardless of PostgreSQL or databases. The [NodaTime library](http://nodatime.org/) was created to solve many of these problems, and if your application handles dates and times in anything but the most basic way, you should seriously consider using NodaTime. To learn more [read this blog post by Jon Skeet](http://blog.nodatime.org/2011/08/what-wrong-with-datetime-anyway.html).

For PostgreSQL specifically, the NodaTime types map more naturally to the database types - everything is simpler and works in a more predictable way.

> [!NOTE]
> This plugin, which works at the Entity Framework Core level, is distinct from [the Npgsql ADO plugin for NodaTime](http://www.npgsql.org/doc/types/nodatime.html); the EF Core plugin references and relies on the ADO plugin.
# Setup

To set up the NodaTime plugin, add the [Npgsql.NodaTime nuget](https://www.nuget.org/packages/Npgsql.NodaTime) to your project. Then, make the following modification to your `UseNpgsql()` line:

```c#
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
builder.UseNpgsql("Host=localhost;Database=test;Username=npgsql_tests;Password=npgsql_tests",
o => o.UseNodaTime());
}
```

This will set up all the necessary mappings and operation translators. You can now use NodaTime types as regular properties in your entities, and even perform some operations:

```c#
public class Post
{
public int Id { get; set; }
public string Name { get; set; }
public Instant CreationTime { get; set; }
}

var recentPosts = context.Posts.Where(p => p.CreationTime > someInstant);
```

## Member Translation

Currently, the EF Core provider knows how to translate the most date/time component members of NodaTime's `LocalDateTime`, `LocalDate`, `LocalTime` and `Period`. In other words, the following query will be translated to SQL and evaluated server-side:

```c#
// Get all events which occurred on a Monday
var mondayEvents = context.Events.Where(p => p.SomeDate.DayOfWeek == DayOfWeek.Monday);

// Get all events which occurred before the year 2000
var oldEvents = context.Events.Where(p => p.SomeDate.Year < 2000);
```

Note that the plugin is far from covering all translations. If a translation you need is missing, please open an issue to request for it.
4 changes: 2 additions & 2 deletions src/EFCore.PG.NTS/EFCore.PG.NTS.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionPrefix>2.1.0-preview3</VersionPrefix>
<VersionPrefix>2.1.0-rtm</VersionPrefix>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite</RootNamespace>
Expand Down Expand Up @@ -29,4 +29,4 @@
<PackageReference Include="Npgsql.NetTopologySuite" Version="1.0.0-ci.*" />
<PackageReference Include="Npgsql" Version="4.0.0-ci.*" />
</ItemGroup>
</Project>
</Project>
32 changes: 32 additions & 0 deletions src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionPrefix>2.1.0-rtm</VersionPrefix>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime</AssemblyName>
<RootNamespace>Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AssemblyOriginatorKeyFile>../../Npgsql.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<Description>NodaTime support plugin for PostgreSQL/Npgsql Entity Framework Core provider.</Description>
<Authors>Shay Rojansky</Authors>
<Company>Npgsql</Company>
<PackageTags>npgsql;postgresql;postgres;Entity Framework Core;entity-framework-core;ef;efcore;orm;sql;nodatime</PackageTags>
<Copyright>Copyright 2017 © The Npgsql Development Team</Copyright>
<PackageIconUrl>http://www.npgsql.org/img/postgresql.gif</PackageIconUrl>
<PackageLicenseUrl>https://raw.githubusercontent.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/dev/LICENSE.txt</PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>git://github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL</RepositoryUrl>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\*.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EFCore.PG\EFCore.PG.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql" Version="4.0.0-ci.*" />
<PackageReference Include="Npgsql.NodaTime" Version="1.0.0-ci.*" />
</ItemGroup>
</Project>
25 changes: 25 additions & 0 deletions src/EFCore.PG.NodaTime/NodaTimeDbContextOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using JetBrains.Annotations;
using Npgsql;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime;
using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore
{
public static class NodaTimeDbContextOptionsExtensions
{
public static NpgsqlDbContextOptionsBuilder UseNodaTime(
[NotNull] this NpgsqlDbContextOptionsBuilder optionsBuilder)
{
Check.NotNull(optionsBuilder, nameof(optionsBuilder));

// TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection?
NpgsqlConnection.GlobalTypeMapper.UseNodatime();

optionsBuilder.UsePlugin(new NodaTimePlugin());

return optionsBuilder;
}
}
}
194 changes: 194 additions & 0 deletions src/EFCore.PG.NodaTime/NodaTimeMappings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NodaTime;
using NodaTime.Text;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using NpgsqlTypes;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime
{
#region timestamp

public class TimestampInstantMapping : NpgsqlTypeMapping
{
public TimestampInstantMapping() : base("timestamp", typeof(Instant), NpgsqlDbType.Timestamp) {}

protected TimestampInstantMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Timestamp) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimestampInstantMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimestampInstantMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMP '{InstantPattern.ExtendedIso.Format((Instant)value)}'";
}

public class TimestampLocalDateTimeMapping : NpgsqlTypeMapping
{
public TimestampLocalDateTimeMapping() : base("timestamp", typeof(LocalDateTime), NpgsqlDbType.Timestamp) {}

protected TimestampLocalDateTimeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Timestamp) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimestampLocalDateTimeMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimestampLocalDateTimeMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMP '{LocalDateTimePattern.ExtendedIso.Format((LocalDateTime)value)}'";
}

#endregion timestamp

#region timestamptz

public class TimestampTzInstantMapping : NpgsqlTypeMapping
{
public TimestampTzInstantMapping() : base("timestamp with time zone", typeof(Instant), NpgsqlDbType.TimestampTz) {}

protected TimestampTzInstantMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.TimestampTz) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimestampTzInstantMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimestampTzInstantMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMPTZ '{InstantPattern.ExtendedIso.Format((Instant)value)}'";
}

public class TimestampTzOffsetDateTimeMapping : NpgsqlTypeMapping
{
public TimestampTzOffsetDateTimeMapping() : base("timestamp with time zone", typeof(OffsetDateTime), NpgsqlDbType.TimestampTz) {}

protected TimestampTzOffsetDateTimeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.TimestampTz) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimestampTzOffsetDateTimeMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimestampTzOffsetDateTimeMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMPTZ '{OffsetDateTimePattern.ExtendedIso.Format((OffsetDateTime)value)}'";
}

public class TimestampTzZonedDateTimeMapping : NpgsqlTypeMapping
{
static readonly ZonedDateTimePattern Pattern =
ZonedDateTimePattern.CreateWithInvariantCulture("uuuu'-'MM'-'dd'T'HH':'mm':'ss;FFFFFFo<G>",
DateTimeZoneProviders.Tzdb);

public TimestampTzZonedDateTimeMapping() : base("timestamp with time zone", typeof(ZonedDateTime), NpgsqlDbType.TimestampTz) {}

protected TimestampTzZonedDateTimeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.TimestampTz) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimestampTzZonedDateTimeMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimestampTzZonedDateTimeMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMESTAMPTZ '{Pattern.Format((ZonedDateTime)value)}'";
}

#endregion timestamptz

#region date

public class DateMapping : NpgsqlTypeMapping
{
public DateMapping() : base("date", typeof(LocalDate), NpgsqlDbType.Date) {}

protected DateMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Date) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new DateMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new DateMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"DATE '{LocalDatePattern.Iso.Format((LocalDate)value)}'";
}

#endregion date

#region time

public class TimeMapping : NpgsqlTypeMapping
{
public TimeMapping() : base("time", typeof(LocalTime), NpgsqlDbType.Time) {}

protected TimeMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Time) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimeMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimeMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIME '{LocalTimePattern.ExtendedIso.Format((LocalTime)value)}'";
}

#endregion time

#region timetz

public class TimeTzMapping : NpgsqlTypeMapping
{
static readonly OffsetTimePattern Pattern =
OffsetTimePattern.CreateWithInvariantCulture("HH':'mm':'ss;FFFFFFo<G>");

public TimeTzMapping() : base("time with time zone", typeof(OffsetTime), NpgsqlDbType.TimeTz) {}

protected TimeTzMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.TimeTz) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new TimeTzMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new TimeTzMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"TIMETZ '{Pattern.Format((OffsetTime)value)}'";
}

#endregion timetz

#region interval

public class IntervalMapping : NpgsqlTypeMapping
{
public IntervalMapping() : base("interval", typeof(Period), NpgsqlDbType.Interval) {}

protected IntervalMapping(RelationalTypeMappingParameters parameters)
: base(parameters, NpgsqlDbType.Interval) {}

public override RelationalTypeMapping Clone(string storeType, int? size)
=> new IntervalMapping(Parameters.WithStoreTypeAndSize(storeType, size));

public override CoreTypeMapping Clone(ValueConverter converter)
=> new IntervalMapping(Parameters.WithComposedConverter(converter));

protected override string GenerateNonNullSqlLiteral(object value)
=> $"INTERVAL '{PeriodPattern.NormalizingIso.Format((Period)value)}'";
}

#endregion interval
}
Loading

0 comments on commit 5655b1c

Please sign in to comment.