From 116c95d8a7346a585444bc901781ee54f760ad4f Mon Sep 17 00:00:00 2001 From: Pawel Gerr Date: Sun, 14 Mar 2021 13:30:27 +0100 Subject: [PATCH] Extension method AddEnumAndValueTypeConverters is using ctors of the value types for reading instead of factories because DB is "source of truth" --- .../Extensions/ModelBuilderExtensions.cs | 19 +++++ .../CodeAnalysis/ValueTypeSourceGenerator.cs | 2 +- .../ValueTypeValueConverterFactoryTests.cs | 78 +++++++++++++++++++ .../AddEnumAndValueTypeConverters.cs | 11 ++- .../TestEntities/TestDbContext.cs | 2 + .../TestEntity_with_Enum_and_ValueTypes.cs | 31 ++++++++ .../ValueTypeSourceGeneratorTests.cs | 6 +- .../TestValueTypes/Boundary.cs | 19 +++++ .../StringBasedReferenceValueType.cs | 2 +- ...ure.Runtime.Extensions.Tests.Shared.csproj | 1 + 10 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/EntityFrameworkCore/ValueConversion/ValueTypeValueConverterFactoryTests.cs create mode 100644 test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestEntity_with_Enum_and_ValueTypes.cs create mode 100644 test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/Boundary.cs diff --git a/src/Thinktecture.Runtime.Extensions.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs b/src/Thinktecture.Runtime.Extensions.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs index d98823a4..eb9ba5e7 100644 --- a/src/Thinktecture.Runtime.Extensions.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -35,11 +36,29 @@ public static void AddEnumAndValueTypeConverters( foreach (var entity in modelBuilder.Model.GetEntityTypes()) { + AddNonKeyedValueTypeMembers(entity); + AddConverterForScalarProperties(entity, validateOnWrite, configure); AddConvertersForNavigations(entity, modelBuilder, validateOnWrite, configure); } } + private static void AddNonKeyedValueTypeMembers(IMutableEntityType entity) + { + if (entity.ClrType.GetCustomAttribute() is not null) + return; + + var ctorAttr = entity.ClrType.GetCustomAttribute(); + + if (ctorAttr is not null && ctorAttr.Members.Length != 0) + { + foreach (string memberName in ctorAttr.Members) + { + entity.AddProperty(memberName); + } + } + } + private static void AddConvertersForNavigations( IMutableEntityType entity, ModelBuilder modelBuilder, diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGenerator.cs index 69d77a64..8ee0140c 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueTypeSourceGenerator.cs @@ -131,7 +131,7 @@ private void GenerateValueType() internal static void ModuleInit() {{ var convertFromKey = new Func<{keyMember.Type}, {_state.TypeIdentifier}>({_state.TypeIdentifier}.Create); - Expression> convertFromKeyExpression = {keyMember.ArgumentName} => {_state.TypeIdentifier}.Create({keyMember.ArgumentName}); + Expression> convertFromKeyExpression = {keyMember.ArgumentName} => new {_state.TypeIdentifier}({keyMember.ArgumentName}); var convertToKey = new Func<{_state.TypeIdentifier}, {keyMember.Type}>(item => item.{keyMember.Identifier}); Expression> convertToKeyExpression = obj => obj.{keyMember.Identifier}; diff --git a/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/EntityFrameworkCore/ValueConversion/ValueTypeValueConverterFactoryTests.cs b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/EntityFrameworkCore/ValueConversion/ValueTypeValueConverterFactoryTests.cs new file mode 100644 index 00000000..c8e5856a --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/EntityFrameworkCore/ValueConversion/ValueTypeValueConverterFactoryTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Thinktecture.Runtime.Tests.TestEntities; +using Thinktecture.Runtime.Tests.TestEnums; +using Thinktecture.Runtime.Tests.TestValueTypes; +using Xunit; + +namespace Thinktecture.Runtime.Tests.EntityFrameworkCore.ValueConversion +{ + public class ValueTypeValueConverterFactoryTests : IDisposable + { + private readonly TestDbContext _ctx; + + public ValueTypeValueConverterFactoryTests() + { + _ctx = new(); + _ctx.Database.OpenConnection(); + _ctx.Database.EnsureCreated(); + } + + [Fact] + public async Task Should_write_and_read_enums_and_value_types() + { + var entity = new TestEntity_with_Enum_and_ValueTypes + { + Id = new Guid("A53F60CD-B53E-40E3-B16F-05E9A223E238"), + TestEnum = TestEnum.Item1, + IntBasedReferenceValueType = IntBasedReferenceValueType.Create(42), + IntBasedStructValueType = IntBasedStructValueType.Create(43), + StringBasedReferenceValueType = StringBasedReferenceValueType.Create("value 1"), + StringBasedStructValueType = StringBasedStructValueType.Create("value 2"), + Boundary = Boundary.Create(10, 20) + }; + _ctx.Add(entity); + await _ctx.SaveChangesAsync(); + + _ctx.ChangeTracker.Clear(); + (await _ctx.TestEntities_with_Enum_and_ValueTypes.SingleAsync()) + .Should().BeEquivalentTo(entity); + } + + [Fact] + public async Task Should_use_ctor_of_value_types_instead_of_factory_because_EF_is_source_of_truth() + { + var entity = new TestEntity_with_Enum_and_ValueTypes + { + Id = new Guid("A53F60CD-B53E-40E3-B16F-05E9A223E238"), + StringBasedReferenceValueType = StringBasedReferenceValueType.Create("value"), + StringBasedStructValueType = StringBasedStructValueType.Create("other value"), + Boundary = Boundary.Create(10, 20) + }; + _ctx.Add(entity); + await _ctx.SaveChangesAsync(); + + await using var command = _ctx.Database.GetDbConnection().CreateCommand(); + command.CommandText = @" +UPDATE TestEntities_with_Enum_and_ValueTypes +SET + StringBasedStructValueType = '', + Boundary_Lower = 30 +"; + await command.ExecuteNonQueryAsync(); + + _ctx.ChangeTracker.Clear(); + var loadedEntity = await _ctx.TestEntities_with_Enum_and_ValueTypes.SingleAsync(); + loadedEntity.StringBasedStructValueType.Property.Should().Be(String.Empty); + loadedEntity.Boundary.Lower.Should().Be(30); + loadedEntity.Boundary.Upper.Should().Be(20); + } + + public void Dispose() + { + _ctx.Dispose(); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests/AddEnumAndValueTypeConverters.cs b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests/AddEnumAndValueTypeConverters.cs index 4f09486d..bcf4d430 100644 --- a/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests/AddEnumAndValueTypeConverters.cs +++ b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/Extensions/ModelBuilderExtensionsTests/AddEnumAndValueTypeConverters.cs @@ -11,17 +11,17 @@ // ReSharper disable InconsistentNaming namespace Thinktecture.Runtime.Tests.Extensions.ModelBuilderExtensionsTests { - public class AddEnumAndValueTypeConverters + public class AddEnumAndValueTypeConverters : IDisposable { private static readonly Type _converterType = typeof(ValueTypeValueConverterFactory).GetNestedTypes(BindingFlags.NonPublic) .Single(t => t.Name.StartsWith("ValidatableEnumValueConverter", StringComparison.Ordinal)); - private readonly TestDbContext SUT = new(); + private readonly TestDbContext _ctx = new(); [Fact] public void Should_add_converters_for_owned_types() { - var entityType = SUT.Model.FindEntityType(typeof(TestEntity_with_OwnedTypes)); + var entityType = _ctx.Model.FindEntityType(typeof(TestEntity_with_OwnedTypes)); ValidateConverter(entityType, nameof(TestEntity_with_OwnedTypes.TestEnum)); var inline_inline = entityType.FindNavigation(nameof(TestEntity_with_OwnedTypes.Inline_Inline)).TargetEntityType; @@ -74,5 +74,10 @@ private static void ValidateConverter(IEntityType entityType, string propertyNam { entityType.FindProperty(propertyName).GetValueConverter().Should().BeOfType(_converterType); } + + public void Dispose() + { + _ctx.Dispose(); + } } } diff --git a/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestDbContext.cs b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestDbContext.cs index 220ed915..1550ac92 100644 --- a/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestDbContext.cs +++ b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestDbContext.cs @@ -5,6 +5,7 @@ namespace Thinktecture.Runtime.Tests.TestEntities public class TestDbContext : DbContext { public DbSet TestEntities_with_OwnedTypes { get; set; } + public DbSet TestEntities_with_Enum_and_ValueTypes { get; set; } public TestDbContext() { @@ -27,6 +28,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); TestEntity_with_OwnedTypes.Configure(modelBuilder); + TestEntity_with_Enum_and_ValueTypes.Configure(modelBuilder); modelBuilder.AddEnumAndValueTypeConverters(true); } diff --git a/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestEntity_with_Enum_and_ValueTypes.cs b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestEntity_with_Enum_and_ValueTypes.cs new file mode 100644 index 00000000..6868e9d8 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Tests/TestEntities/TestEntity_with_Enum_and_ValueTypes.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Thinktecture.Runtime.Tests.TestEnums; +using Thinktecture.Runtime.Tests.TestValueTypes; + +namespace Thinktecture.Runtime.Tests.TestEntities +{ + public class TestEntity_with_Enum_and_ValueTypes + { + public Guid Id { get; set; } + + public TestEnum TestEnum { get; set; } + public IntBasedReferenceValueType IntBasedReferenceValueType { get; set; } + public IntBasedStructValueType IntBasedStructValueType { get; set; } + public StringBasedReferenceValueType StringBasedReferenceValueType { get; set; } + public StringBasedStructValueType StringBasedStructValueType { get; set; } + public Boundary Boundary { get; set; } + + public static void Configure(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + // struct are not added bey EF by default + builder.Property(p => p.StringBasedStructValueType); + builder.Property(p => p.IntBasedStructValueType); + + builder.OwnsOne(e => e.Boundary); + }); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueTypeSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueTypeSourceGeneratorTests.cs index 561da2f5..b8527c28 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueTypeSourceGeneratorTests.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueTypeSourceGeneratorTests.cs @@ -445,7 +445,7 @@ partial class TestValueType : System.IEquatable, System.ICompara internal static void ModuleInit() { var convertFromKey = new Func(TestValueType.Create); - Expression> convertFromKeyExpression = referenceField => TestValueType.Create(referenceField); + Expression> convertFromKeyExpression = referenceField => new TestValueType(referenceField); var convertToKey = new Func(item => item.ReferenceField); Expression> convertToKeyExpression = obj => obj.ReferenceField; @@ -666,7 +666,7 @@ partial class TestValueType : System.IEquatable, System.ICompara internal static void ModuleInit() { var convertFromKey = new Func(TestValueType.Create); - Expression> convertFromKeyExpression = referenceField => TestValueType.Create(referenceField); + Expression> convertFromKeyExpression = referenceField => new TestValueType(referenceField); var convertToKey = new Func(item => item.ReferenceField); Expression> convertToKeyExpression = obj => obj.ReferenceField; @@ -880,7 +880,7 @@ partial class TestValueType : System.IEquatable, System.IFormatt internal static void ModuleInit() { var convertFromKey = new Func(TestValueType.Create); - Expression> convertFromKeyExpression = referenceField => TestValueType.Create(referenceField); + Expression> convertFromKeyExpression = referenceField => new TestValueType(referenceField); var convertToKey = new Func(item => item.ReferenceField); Expression> convertToKeyExpression = obj => obj.ReferenceField; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/Boundary.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/Boundary.cs new file mode 100644 index 00000000..e2c0c897 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/Boundary.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace Thinktecture.Runtime.Tests.TestValueTypes +{ + [ValueType] + public partial class Boundary + { + public decimal Lower { get; } + public decimal Upper { get; } + + static partial void ValidateFactoryArguments(ref ValidationResult? validationResult, ref decimal lower, ref decimal upper) + { + if (lower <= upper) + return; + + validationResult = new ValidationResult($"Lower boundary '{lower}' must be less than upper boundary '{upper}'"); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedReferenceValueType.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedReferenceValueType.cs index 8ad12d18..4b5d61d4 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedReferenceValueType.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestValueTypes/StringBasedReferenceValueType.cs @@ -8,7 +8,7 @@ public partial class StringBasedReferenceValueType { public string Property { get; } - static partial void ValidateFactoryArguments(ref ValidationResult validationResult, ref string property) + static partial void ValidateFactoryArguments(ref ValidationResult? validationResult, ref string property) { if (String.IsNullOrWhiteSpace(property)) { diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj b/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj index 129cd650..d5a65f2f 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/Thinktecture.Runtime.Extensions.Tests.Shared.csproj @@ -1,6 +1,7 @@ + enable $(NoWarn);CS1591;CA1052;CA1716;CA1801;CA1052;CA1707;CS1718;CA1062;CA1806;CA1822;CA1825;CA2000;CA2007;CA2234