From 064c8f0f893b4d02c3d0bbc57c2186b8d5c80ed8 Mon Sep 17 00:00:00 2001 From: Svyatoslav Danyliv Date: Tue, 20 Oct 2020 21:51:00 +0300 Subject: [PATCH] Improved ChangeTracker feeding. Improved ConnectionString retrieval for NpgSql (#68) * Improved ChangeTracker feeding. Improved ConnectionString retrieval for NpgSql. * Updated linq2db version. * Increased library version to 3.7.0 --- Build/linq2db.Default.props | 2 +- NuGet/linq2db.EntityFrameworkCore.nuspec | 2 +- .../LinqToDBForEFTools.cs | 26 ++- .../LinqToDBForEFToolsDataConnection.cs | 29 ++- .../linq2db.EntityFrameworkCore.csproj | 2 +- ...ntityFrameworkCore.PostgreSQL.Tests.csproj | 1 + .../NpgSqlTests.cs | 2 +- .../SampleTests/AAA.cs | 105 +++++++++++ .../SampleTests/Child.cs | 10 + .../SampleTests/DataContextExtensions.cs | 12 ++ .../SampleTests/Detail.cs | 13 ++ .../SampleTests/Entity.cs | 14 ++ .../SampleTests/Entity2Item.cs | 15 ++ .../SampleTests/IHasId.cs | 12 ++ .../SampleTests/Id.cs | 28 +++ .../SampleTests/IdTests.cs | 177 ++++++++++++++++++ .../SampleTests/IdValueConverter.cs | 50 +++++ .../SampleTests/Item.cs | 8 + .../SampleTests/ModelBuilderExtensions.cs | 169 +++++++++++++++++ .../SampleTests/QueryableExtensions.cs | 15 ++ .../SampleTests/StringExtensions.cs | 16 ++ .../SampleTests/SubDetail.cs | 10 + .../SampleTests/TypeExtensions.cs | 10 + azure-pipelines.yml | 4 +- 24 files changed, 723 insertions(+), 9 deletions(-) create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/AAA.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Child.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/DataContextExtensions.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Detail.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity2Item.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IHasId.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Id.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdTests.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdValueConverter.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Item.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/ModelBuilderExtensions.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/QueryableExtensions.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/StringExtensions.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/SubDetail.cs create mode 100644 Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/TypeExtensions.cs diff --git a/Build/linq2db.Default.props b/Build/linq2db.Default.props index 7b36d44..c69d3ef 100644 --- a/Build/linq2db.Default.props +++ b/Build/linq2db.Default.props @@ -1,6 +1,6 @@  - 3.6.0 + 3.7.0 Allows to execute Linq to DB (linq2db) queries in Entity Framework Core DbContext. Linq to DB (linq2db) extensions for Entity Framework Core diff --git a/NuGet/linq2db.EntityFrameworkCore.nuspec b/NuGet/linq2db.EntityFrameworkCore.nuspec index f71fdf4..a6dc599 100644 --- a/NuGet/linq2db.EntityFrameworkCore.nuspec +++ b/NuGet/linq2db.EntityFrameworkCore.nuspec @@ -16,7 +16,7 @@ - + diff --git a/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFTools.cs b/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFTools.cs index 6e46472..06ae890 100644 --- a/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFTools.cs +++ b/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFTools.cs @@ -3,7 +3,7 @@ using System.Data.Common; using System.Linq; using System.Linq.Expressions; - +using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -390,6 +390,9 @@ public static DataConnection CreateLinq2DbConnectionDetached([JetBrains.Annotati return dc; } + + static ConcurrentDictionary> _connectionStringExtractors = new ConcurrentDictionary>(); + /// /// Extracts database connection information from EF.Core provider data. /// @@ -398,7 +401,26 @@ public static DataConnection CreateLinq2DbConnectionDetached([JetBrains.Annotati public static EFConnectionInfo GetConnectionInfo(EFProviderInfo info) { var connection = info.Connection; - var connectionString = connection?.ConnectionString; + string connectionString = null; + if (connection != null) + { + var connectionStringFunc = _connectionStringExtractors.GetOrAdd(connection.GetType(), t => + { + // NpgSQL workaround + var originalProp = t.GetProperty("OriginalConnectionString", BindingFlags.Instance | BindingFlags.NonPublic); + + if (originalProp == null) + return c => c.ConnectionString; + + var parameter = Expression.Parameter(typeof(DbConnection), "c"); + var lambda = Expression.Lambda>( + Expression.MakeMemberAccess(Expression.Convert(parameter, t), originalProp), parameter); + + return lambda.Compile(); + }); + + connectionString = connectionStringFunc(connection); + } if (connection != null && connectionString != null) return new EFConnectionInfo { Connection = connection, ConnectionString = connectionString }; diff --git a/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFToolsDataConnection.cs b/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFToolsDataConnection.cs index 93bc0c2..4c7fd22 100644 --- a/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFToolsDataConnection.cs +++ b/Source/LinqToDB.EntityFrameworkCore/LinqToDBForEFToolsDataConnection.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata; @@ -100,7 +101,33 @@ private void OnEntityCreatedHandler(EntityCreatedEventArgs args) if (_stateManager == null) _stateManager = Context.GetService(); - var entry = _stateManager.StartTrackingFromQuery(_lastEntityType, args.Entity, ValueBuffer.Empty); + + // It is a real pain to register entity in change tracker + // + InternalEntityEntry entry = null; + + foreach (var key in _lastEntityType.GetKeys()) + { + //TODO: Find faster way + var keyArray = key.Properties.Where(p => p.PropertyInfo != null || p.FieldInfo != null).Select(p => + p.PropertyInfo != null + ? p.PropertyInfo.GetValue(args.Entity) + : p.FieldInfo.GetValue(args.Entity)).ToArray(); + + if (keyArray.Length == key.Properties.Count) + { + entry = _stateManager.TryGetEntry(key, keyArray); + + if (entry != null) + break; + } + } + + if (entry == null) + { + entry = _stateManager.StartTrackingFromQuery(_lastEntityType, args.Entity, ValueBuffer.Empty); + } + args.Entity = entry.Entity; } diff --git a/Source/LinqToDB.EntityFrameworkCore/linq2db.EntityFrameworkCore.csproj b/Source/LinqToDB.EntityFrameworkCore/linq2db.EntityFrameworkCore.csproj index 9ce6406..305b934 100644 --- a/Source/LinqToDB.EntityFrameworkCore/linq2db.EntityFrameworkCore.csproj +++ b/Source/LinqToDB.EntityFrameworkCore/linq2db.EntityFrameworkCore.csproj @@ -9,7 +9,7 @@ - + diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.csproj b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.csproj index 43efdf3..6d46c4e 100644 --- a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.csproj +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.csproj @@ -5,6 +5,7 @@ + diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/NpgSqlTests.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/NpgSqlTests.cs index 35a1803..5f84557 100644 --- a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/NpgSqlTests.cs +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/NpgSqlTests.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore; using NUnit.Framework; -namespace LinqToDB.EntityFrameworkCore.Tests +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests { public class NpgSqlTests : TestsBase { diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/AAA.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/AAA.cs new file mode 100644 index 0000000..a422e58 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/AAA.cs @@ -0,0 +1,105 @@ +using System; +using System.Threading.Tasks; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public class Unit + { + + } + + public static class ExceptionExtensions + { + public static Unit Throw(this Exception e) => throw e; + } + + public static class AAA + { + public static ArrangeResult Arrange(this T @object, Action action) + { + action(@object); + return new ArrangeResult(@object, default); + } + + public static ArrangeResult Arrange(T @object) + => new ArrangeResult(@object, default); + + public static ArrangeResult Arrange(this TMock mock, Func @object) + => new ArrangeResult(@object(mock), mock); + + public static ActResult Act(this ArrangeResult arrange, Action act) + { + try + { + act(arrange.Object); + return new ActResult(arrange.Object, arrange.Mock, default); + } + catch (Exception e) + { + return new ActResult(arrange.Object, arrange.Mock, e); + } + } + + public static ActResult Act(this ArrangeResult arrange, Func act) + { + try + { + return new ActResult(act(arrange.Object), arrange.Mock, default); + } + catch (Exception e) + { + return new ActResult(default, arrange.Mock, e); + } + } + + public static void Assert(this ActResult act, Action assert) + { + act.Exception?.Throw(); + assert(act.Object); + } + + public static void Assert(this ActResult act, Action assert) + { + act.Exception?.Throw(); + assert(act.Object, act.Mock); + } + + public static Task> ArrangeAsync(T @object) + => Task.FromResult(new ArrangeResult(@object, default)); + + public static async Task> Act(this Task> arrange, Func> act) + { + var a = await arrange; + try + { + return new ActResult(await act(a.Object), a.Mock, default); + } + catch (Exception e) + { + return new ActResult(default, a.Mock, e); + } + } + + public static async Task Assert(this Task> act, Func assert) + { + var result = await act; + await assert(result.Object); + } + + public readonly struct ArrangeResult + { + internal ArrangeResult(T @object, TMock mock) => (Object, Mock) = (@object, mock); + internal T Object { get; } + internal TMock Mock { get; } + } + + public readonly struct ActResult + { + internal ActResult(T @object, TMock mock, Exception exception) + => (Object, Mock, Exception) = (@object, mock, exception); + internal T Object { get; } + internal TMock Mock { get; } + internal Exception Exception { get; } + } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Child.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Child.cs new file mode 100644 index 0000000..1004de5 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Child.cs @@ -0,0 +1,10 @@ +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class Child : IHasWriteableId + { + public Id Id { get; set; } + public Id ParentId { get; set; } + public string Name { get; set; } + public Entity Parent { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/DataContextExtensions.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/DataContextExtensions.cs new file mode 100644 index 0000000..a3e9b78 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/DataContextExtensions.cs @@ -0,0 +1,12 @@ +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public static class DataContextExtensions + { + public static Id Insert(this IDataContext context, T item) + where T : IHasWriteableId + { + item.Id = context.InsertWithInt64Identity(item).AsId(); + return item.Id; + } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Detail.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Detail.cs new file mode 100644 index 0000000..ec983be --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Detail.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class Detail : IHasWriteableId + { + public Id Id { get; set; } + public Id MasterId { get; set; } + public string Name { get; set; } + public Entity Master { get; set; } + public IEnumerable Details { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity.cs new file mode 100644 index 0000000..4405575 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class Entity : IHasWriteableId + { + public Id Id { get; set; } + public string Name { get; set; } + + public IEnumerable Details { get; set; } + public IEnumerable Children { get; set; } + public IEnumerable Items { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity2Item.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity2Item.cs new file mode 100644 index 0000000..745e9a9 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Entity2Item.cs @@ -0,0 +1,15 @@ +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class Entity2Item + { + public Id EntityId { get; set; } + public Entity Entity { get; set; } + public Id ItemId { get; set; } + + public Entity2Item() + { + } + + public Item Item { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IHasId.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IHasId.cs new file mode 100644 index 0000000..2402dd5 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IHasId.cs @@ -0,0 +1,12 @@ +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public interface IHasId where T: IHasId + { + Id Id { get; } + } + + public interface IHasWriteableId : IHasId where T: IHasWriteableId + { + new Id Id { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Id.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Id.cs new file mode 100644 index 0000000..1a59ff9 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Id.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public static class Id + { + public static Id AsId(this long id) where T : IHasId => id.AsId(); + + public static Id AsId(this TId id) where T : IHasId + => new Id(id); + } + + public readonly struct Id where T : IHasId + { + internal Id(TId value) => Value = value; + TId Value { get; } + + public static implicit operator TId (in Id id) => id.Value; + public static bool operator == (Id left, Id right) + => EqualityComparer.Default.Equals(left.Value, right.Value); + public static bool operator != (Id left, Id right) => !(left == right); + + public override string ToString() => $"{typeof(T).Name}({Value})"; + public bool Equals(Id other) => EqualityComparer.Default.Equals(Value, other.Value); + public override bool Equals(object obj) => obj is Id other && Equals(other); + public override int GetHashCode() => EqualityComparer.Default.GetHashCode(Value); + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdTests.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdTests.cs new file mode 100644 index 0000000..bbf8447 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdTests.cs @@ -0,0 +1,177 @@ +using System; +using System.Diagnostics; +using System.Linq; +using FluentAssertions; +using LinqToDB.Common.Logging; +using LinqToDB.EntityFrameworkCore.BaseTests; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NUnit.Framework; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + [TestFixture] + public sealed class IdTests : IDisposable + { + public IdTests() + { + _efContext = new TestContext( + new DbContextOptionsBuilder() + .ReplaceService() + .UseLoggerFactory(TestUtils.LoggerFactory) + .EnableSensitiveDataLogging() + .UseNpgsql("Server=localhost;Port=5433;Database=SampleTests;User Id=postgres;Password=TestPassword;Pooling=true;MinPoolSize=10;MaxPoolSize=100;") + .Options); + _efContext.Database.EnsureDeleted(); + _efContext.Database.EnsureCreated(); + } + + IDataContext CreateLinqToDbContext(TestContext testContext) + { + var result = testContext.CreateLinqToDbContext(); + result.GetTraceSwitch().Level = TraceLevel.Verbose; + return result; + } + + readonly TestContext _efContext; + + [Test] + [Ignore("Incomplete.")] + public void TestInsertWithoutTracker([Values("test insert")] string name) + => _efContext + .Arrange(c => CreateLinqToDbContext(c)) + .Act(c => c.Insert(new Entity { Name = name })) + .Assert(id => _efContext.Entitites.Single(e => e.Id == id).Name.Should().Be(name)); + + [Test] + [Ignore("Incomplete.")] + public void TestInsertWithoutNew([Values("test insert")] string name) + => _efContext.Entitites + .Arrange(e => e.ToLinqToDBTable()) + .Act(e => e.InsertWithInt64Identity(() => new Entity {Name = name})) + .Assert(id => _efContext.Entitites.Single(e => e.Id == id).Name.Should().Be(name)); + + [Test] + [Ignore("Incomplete.")] + public void TestInsertEfCore([Values("test insert ef")] string name) + => _efContext + .Arrange(c => c.Entitites.Add(new Entity {Name = "test insert ef"})) + .Act(_ => _efContext.SaveChanges()) + .Assert(_ => _efContext.Entitites.Single().Name.Should().Be(name)); + + [Test] + [Ignore("Incomplete.")] + public void TestIncludeDetails([Values] bool l2db, [Values] bool tracking) + => _efContext + .Arrange(c => InsertDefaults(CreateLinqToDbContext(c))) + .Act(c => c + .Entitites + .Where(e => e.Name == "Alpha") + .Include(e => e.Details) + .ThenInclude(d => d.Details) + .Include(e => e.Children) + .AsLinqToDb(l2db) + .AsTracking(tracking) + .ToArray()) + .Assert(e => e.First().Details.First().Details.Count().Should().Be(2)); + + [Test] + public void TestManyToManyIncludeTrackerPoison([Values] bool l2db) + => _efContext + .Arrange(c => InsertDefaults(CreateLinqToDbContext(c))) + .Act(c => + { + var q = c.Entitites + .Include(e => e.Items) + .ThenInclude(x => x.Item); + var f = q.AsLinqToDb(l2db).AsTracking().ToArray(); + var s = q.AsLinqToDb(!l2db).AsTracking().ToArray(); + return (First: f, Second: s); + }) + .Assert(r => r.First[0].Items.Count().Should().Be(r.Second[0].Items.Count())); + + + [Test] + [Ignore("Incomplete.")] + public void TestManyToManyInclude([Values] bool l2db, [Values] bool tracking) + => _efContext + .Arrange(c => InsertDefaults(CreateLinqToDbContext(c))) + .Act(c => c.Entitites + .Include(e => e.Items) + .ThenInclude(x => x.Item) + .AsLinqToDb(l2db) + .AsTracking(tracking) + .ToArray()) + .Assert(m => m[0].Items.First().Item.Should().BeSameAs(m[1].Items.First().Item)); + + [Test] + [Ignore("Incomplete.")] + public void TestMasterInclude([Values] bool l2db, [Values] bool tracking) + => _efContext + .Arrange(c => InsertDefaults(CreateLinqToDbContext(c))) + .Act(c => c + .Details + .Include(d => d.Master) + .AsLinqToDb(l2db) + .AsTracking(tracking) + .ToArray()) + .Assert(m => m[0].Master.Should().BeSameAs(m[1].Master)); + + [Test] + [Ignore("Incomplete.")] + public void TestMasterInclude2([Values] bool l2db, [Values] bool tracking) + => _efContext + .Arrange(c => InsertDefaults(CreateLinqToDbContext(c))) + .Act(c => c + .Details + .Include(d => d.Master) + .AsTracking(tracking) + .AsLinqToDb(l2db) + .ToArray()) + .Assert(m => m[0].Master.Should().BeSameAs(m[1].Master)); + + void InsertDefaults(IDataContext dataContext) + { + var a = dataContext.Insert(new Entity {Name = "Alpha"}); + var b = dataContext.Insert(new Entity {Name = "Bravo"}); + var d = dataContext.Insert(new Detail {Name = "First", MasterId = a}); + var r = dataContext.Insert(new Item {Name = "Red"}); + var g = dataContext.Insert(new Item {Name = "Green"}); + var w = dataContext.Insert(new Item {Name = "White"}); + + dataContext.Insert(new Detail {Name = "Second", MasterId = a}); + dataContext.Insert(new SubDetail {Name = "Plus", MasterId = d}); + dataContext.Insert(new SubDetail {Name = "Minus", MasterId = d}); + dataContext.Insert(new Child {Name = "One", ParentId = a}); + dataContext.Insert(new Child {Name = "Two", ParentId = a}); + dataContext.Insert(new Child {Name = "Three", ParentId = a}); + dataContext.Insert(new Entity2Item {EntityId = a, ItemId = r}); + dataContext.Insert(new Entity2Item {EntityId = a, ItemId = g}); + dataContext.Insert(new Entity2Item {EntityId = b, ItemId = r}); + dataContext.Insert(new Entity2Item {EntityId = b, ItemId = w}); + } + + public class TestContext : DbContext + { + public TestContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(x => new { x.EntityId, x.ItemId}); + modelBuilder + .UseSnakeCase() + .UseIdAsKey() + .UseOneIdSequence("test", sn => $"nextval('{sn}')"); + } + + + public DbSet Entitites { get; set; } + public DbSet Details { get; set; } + public DbSet SubDetails { get; set; } + public DbSet Items { get; set; } + public DbSet Children { get; set; } + } + + public void Dispose() => _efContext.Dispose(); + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdValueConverter.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdValueConverter.cs new file mode 100644 index 0000000..a3672b3 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/IdValueConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class IdValueConverter : ValueConverter, TId> + where T : IHasId + { + public IdValueConverter(ConverterMappingHints mappingHints = null) + : base(id => id, id => id.AsId()) { } + } + + public sealed class IdValueConverterSelector : ValueConverterSelector + { + public IdValueConverterSelector([System.Diagnostics.CodeAnalysis.NotNull] ValueConverterSelectorDependencies dependencies) : base(dependencies) + { + } + + public override IEnumerable Select(Type modelClrType, Type providerClrType = null) + { + var baseConverters = base.Select(modelClrType, providerClrType); + foreach (var converter in baseConverters) + yield return converter; + + modelClrType = modelClrType.UnwrapNullable(); + providerClrType = providerClrType.UnwrapNullable(); + + if (!modelClrType.IsGenericType) + yield break; + + if (modelClrType.GetGenericTypeDefinition() != typeof(Id<,>)) + yield break; + + var t = modelClrType.GetGenericArguments(); + var key = t[1]; + providerClrType ??= key; + if (key != providerClrType) + yield break; + + var ct = typeof(IdValueConverter<,>).MakeGenericType(key, t[0]); + yield return new ValueConverterInfo + ( + modelClrType, + providerClrType, + i => (ValueConverter)Activator.CreateInstance(ct, i.MappingHints) + ); + } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Item.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Item.cs new file mode 100644 index 0000000..d1493c6 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/Item.cs @@ -0,0 +1,8 @@ +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class Item : IHasWriteableId + { + public Id Id { get; set; } + public string Name { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/ModelBuilderExtensions.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/ModelBuilderExtensions.cs new file mode 100644 index 0000000..51e221c --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/ModelBuilderExtensions.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public static class ModelBuilderExtensions + { + public static ModelBuilder UseIdAsKey(this ModelBuilder modelBuilder) + { + var entities = modelBuilder.Model.GetEntityTypes().Select(e => e.ClrType).ToHashSet(); + + // For all entities in the data model + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + // Find the properties that are our strongly-typed ID + var properties = entityType + .ClrType + .GetProperties() + .Where(p => + { + var unwrappedType = p.PropertyType.UnwrapNullable(); + return unwrappedType.IsGenericType && unwrappedType.GetGenericTypeDefinition() == typeof(Id<,>); + }); + + foreach (var property in properties) + { + var entity = property.PropertyType.UnwrapNullable().GetGenericArguments()[0]; + + if (!entities.Contains(entity)) + continue; + + if (entity == entityType.ClrType && property.Name == "Id") + { + modelBuilder + .Entity(entityType.Name) + .HasKey(property.Name); + continue; + } + + var oneNavigation = entityType.ClrType.GetProperties() + .SingleOrDefault(p => p.PropertyType == entity); + var manyNavigation = entity.GetProperties() + .SingleOrDefault(p => + { + var pt = p.PropertyType; + return pt.IsGenericType + && pt.GetGenericTypeDefinition() == typeof(IEnumerable<>) + && pt.GetGenericArguments()[0] == entityType.ClrType; + }); + + modelBuilder + .Entity(entityType.Name) + .HasOne(entity, oneNavigation?.Name) + .WithMany(manyNavigation?.Name) + .HasForeignKey(property.Name) + .OnDelete(DeleteBehavior.Restrict); + } + } + + return modelBuilder; + + + } + + public static ModelBuilder UseOneIdSequence(this ModelBuilder modelBuilder, string sequenceName, Func nextval) + { + modelBuilder.HasSequence(sequenceName); + var entities = modelBuilder.Model.GetEntityTypes().Select(e => e.ClrType).ToHashSet(); + + // For all entities in the data model + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + // Find the properties that are our strongly-typed ID + var id = entityType + .ClrType + .GetProperties() + .FirstOrDefault(p => p.PropertyType.IsGenericType + && p.PropertyType.GetGenericTypeDefinition() == typeof(Id<,>) + && p.PropertyType.GetGenericArguments()[0] == entityType.ClrType + && p.PropertyType.GetGenericArguments()[1] == typeof(T) + && p.Name == "Id"); + + if (id == null) + continue; + + modelBuilder + .Entity(entityType.Name) + .Property(id.Name) + .HasDefaultValueSql(nextval(sequenceName)) + .ValueGeneratedOnAdd(); + } + + return modelBuilder; + } + + public static ModelBuilder UseSnakeCase(this ModelBuilder modelBuilder) + { + modelBuilder.Model.SetDefaultSchema(modelBuilder.Model.GetDefaultSchema().ToSnakeCase()); + foreach (var entity in modelBuilder.Model.GetEntityTypes()) + { + entity.SetTableName(entity.GetTableName().ToSnakeCase()); + + foreach (var property in entity.GetProperties()) + property.SetColumnName(property.GetColumnName().ToSnakeCase()); + + foreach (var key in entity.GetKeys()) + key.SetName(key.GetName().ToSnakeCase()); + + foreach (var key in entity.GetForeignKeys()) + key.SetConstraintName(key.GetConstraintName().ToSnakeCase()); + + foreach (var index in entity.GetIndexes()) + index.SetName(index.GetName().ToSnakeCase()); + } + return modelBuilder; + } + + public static ModelBuilder UsePermanentId(this ModelBuilder modelBuilder) + { + // For all entities in the data model + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + // Find the properties that are our strongly-typed ID + var properties = entityType + .ClrType + .GetProperties() + .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(Id<,>)); + + foreach (var property in properties) + { + var entity = property.PropertyType.GetGenericArguments()[0]; + + if (entity != entityType.ClrType || property.Name != "PermanentId") + continue; + + modelBuilder + .Entity(entityType.Name) + .HasIndex(property.Name) + .IsUnique(); + } + } + return modelBuilder; + } + + public static ModelBuilder UseCode(this ModelBuilder modelBuilder) + { + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + var properties = entityType + .ClrType + .GetProperties(); + + foreach (var property in properties) + { + if (property.Name != "Code") + continue; + + modelBuilder + .Entity(entityType.Name) + .HasIndex(property.Name) + .IsUnique(); + } + } + return modelBuilder; + } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/QueryableExtensions.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/QueryableExtensions.cs new file mode 100644 index 0000000..3dfccf2 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/QueryableExtensions.cs @@ -0,0 +1,15 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public static class QueryableExtensions + { + public static IQueryable AsLinqToDb(this IQueryable queryable, bool l2db) + => l2db ? queryable.ToLinqToDB() : queryable; + + public static IQueryable AsTracking(this IQueryable queryable, bool tracking) + where T : class + => tracking ? queryable.AsTracking() : queryable.AsNoTracking(); + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/StringExtensions.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/StringExtensions.cs new file mode 100644 index 0000000..687b3ee --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/StringExtensions.cs @@ -0,0 +1,16 @@ +using System.Text.RegularExpressions; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public static class StringExtensions + { + public static string ToSnakeCase(this string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var startUnderscores = Regex.Match(input, @"^_+"); + return startUnderscores + Regex.Replace(input, @"([a-z0-9])([A-Z])", "$1_$2").ToLower(); + } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/SubDetail.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/SubDetail.cs new file mode 100644 index 0000000..530c7e5 --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/SubDetail.cs @@ -0,0 +1,10 @@ +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public sealed class SubDetail : IHasWriteableId + { + public Id Id { get; set; } + public Id MasterId { get; set; } + public string Name { get; set; } + public Detail Master { get; set; } + } +} diff --git a/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/TypeExtensions.cs b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/TypeExtensions.cs new file mode 100644 index 0000000..636909a --- /dev/null +++ b/Tests/LinqToDB.EntityFrameworkCore.PostgreSQL.Tests/SampleTests/TypeExtensions.cs @@ -0,0 +1,10 @@ +using System; + +namespace LinqToDB.EntityFrameworkCore.PostgreSQL.Tests.SampleTests +{ + public static class TypeExtensions + { + public static Type UnwrapNullable(this Type type) + => type == null ? null : Nullable.GetUnderlyingType(type) ?? type; + } +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2a035b6..e9ff081 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,8 +1,8 @@ variables: solution: 'linq2db.EFCore.sln' build_configuration: 'Release' - assemblyVersion: 3.6.0 - nugetVersion: 3.6.0 + assemblyVersion: 3.7.0 + nugetVersion: 3.7.0 artifact_nugets: 'nugets' # build on commits to important branches (master + release branches):