From 4cb129bb80437f9be5da9bab5201c26823b3d457 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Mon, 31 Jul 2023 15:22:24 -0700 Subject: [PATCH] Add DateTimeOffset converter for Postgres. --- .../Pages/Account/Manage/ManageNavPages.cs | 215 +++++++++--------- .../PostgresDateTimeOffsetConverter.cs | 37 +++ Server/Data/AppDb.cs | 45 ++-- 3 files changed, 172 insertions(+), 125 deletions(-) create mode 100644 Server/Converters/PostgresDateTimeOffsetConverter.cs diff --git a/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs index 1419219b2..728451292 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs +++ b/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -5,119 +5,118 @@ using System; using Microsoft.AspNetCore.Mvc.Rendering; -namespace Remotely.Server.Areas.Identity.Pages.Account.Manage +namespace Remotely.Server.Areas.Identity.Pages.Account.Manage; + +/// +/// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public static class ManageNavPages { /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public static class ManageNavPages + public static string Index => "Index"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string Email => "Email"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ChangePassword => "ChangePassword"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DownloadPersonalData => "DownloadPersonalData"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DeletePersonalData => "DeletePersonalData"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ExternalLogins => "ExternalLogins"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string PersonalData => "PersonalData"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string TwoFactorAuthentication => "TwoFactorAuthentication"; + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); + + /// + /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public static string PageNavClass(ViewContext viewContext, string page) { - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string Index => "Index"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string Email => "Email"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ChangePassword => "ChangePassword"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DownloadPersonalData => "DownloadPersonalData"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DeletePersonalData => "DeletePersonalData"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ExternalLogins => "ExternalLogins"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string PersonalData => "PersonalData"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string TwoFactorAuthentication => "TwoFactorAuthentication"; - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string EmailNavClass(ViewContext viewContext) => PageNavClass(viewContext, Email); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DownloadPersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DownloadPersonalData); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string DeletePersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, DeletePersonalData); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string PersonalDataNavClass(ViewContext viewContext) => PageNavClass(viewContext, PersonalData); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); - - /// - /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public static string PageNavClass(ViewContext viewContext, string page) - { - var activePage = viewContext.ViewData["ActivePage"] as string - ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); - return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; - } + var activePage = viewContext.ViewData["ActivePage"] as string + ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; } } diff --git a/Server/Converters/PostgresDateTimeOffsetConverter.cs b/Server/Converters/PostgresDateTimeOffsetConverter.cs new file mode 100644 index 000000000..64154b797 --- /dev/null +++ b/Server/Converters/PostgresDateTimeOffsetConverter.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System; +using System.Globalization; +using System.Linq.Expressions; + +namespace Remotely.Server.Converters; + +/// +/// +/// Postgres can only handle DateTimeOffset with an offset of 0. This converter +/// will allow us to use DateTimeOffset in entities without any manual conversion. +/// +/// +/// Docs: https://www.npgsql.org/efcore/release-notes/6.0.html?tabs=annotations#detailed-notes +/// +/// +public class PostgresDateTimeOffsetConverter : ValueConverter +{ + public PostgresDateTimeOffsetConverter() + : base(ToUtc(), ToLocalTime(), null) + { + + } + + public PostgresDateTimeOffsetConverter( + Expression> convertToProviderExpression, + Expression> convertFromProviderExpression, + ConverterMappingHints? mappingHints = null) + : base(convertToProviderExpression, convertFromProviderExpression, mappingHints) + { + } + protected static Expression> ToUtc() + => v => v.ToUniversalTime(); + + protected static Expression> ToLocalTime() + => v => v.ToLocalTime(); +} diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 14a800805..3e885f9de 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -5,12 +5,10 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Remotely.Server.Converters; using Remotely.Shared.Entities; using Remotely.Shared.Models; -using Remotely.Shared.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -210,29 +208,42 @@ protected override void OnModelCreating(ModelBuilder builder) .HasOne(x => x.User) .WithMany(x => x.Alerts); - - if (Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite") + var isSqlite = Database.IsSqlite(); + var isPostgres = Database.IsNpgsql(); + + if (isSqlite || isPostgres) { - // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations - // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations - // To work around this, when the SQLite database provider is used, all model properties of type DateTimeOffset - // use the DateTimeOffsetToBinaryConverter - // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 - // This only supports millisecond precision, but should be sufficient for most use cases. + // SQLite and PostgreSQL don't support DateTimeOffset natively (or don't support + // it correctly), so we need to use a converter. foreach (var entityType in builder.Model.GetEntityTypes()) { if (entityType.IsKeyless) { continue; } - var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset) - || p.PropertyType == typeof(DateTimeOffset?)); + + var properties = entityType.ClrType + .GetProperties() + .Where(p => + p.PropertyType == typeof(DateTimeOffset) || + p.PropertyType == typeof(DateTimeOffset?)); + foreach (var property in properties) { - builder - .Entity(entityType.Name) - .Property(property.Name) - .HasConversion(new DateTimeOffsetToStringConverter()); + if (isSqlite) + { + builder + .Entity(entityType.Name) + .Property(property.Name) + .HasConversion(new DateTimeOffsetToStringConverter()); + } + else if (isPostgres) + { + builder + .Entity(entityType.Name) + .Property(property.Name) + .HasConversion(new PostgresDateTimeOffsetConverter()); + } } } }