diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/Design/FbCSharpRuntimeAnnotationCodeGeneratorTests.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/Design/FbCSharpRuntimeAnnotationCodeGeneratorTests.cs new file mode 100644 index 000000000..8c988198a --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/Design/FbCSharpRuntimeAnnotationCodeGeneratorTests.cs @@ -0,0 +1,153 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Niek Schoemaker + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FirebirdSql.Data.FirebirdClient; +using FirebirdSql.EntityFrameworkCore.Firebird.Design.Internal; +using FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace FirebirdSql.EntityFrameworkCore.Firebird.Tests.Design; + +#pragma warning disable EF1001 +public class FbCSharpRuntimeAnnotationCodeGeneratorTests +{ + ServiceProvider _serviceProvider; + IRelationalTypeMappingSource _typeMappingSource; + ICSharpRuntimeAnnotationCodeGenerator _generator; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + var services = new ServiceCollection().AddEntityFrameworkDesignTimeServices(); + new FbDesignTimeServices().ConfigureDesignTimeServices(services); + _serviceProvider = services.BuildServiceProvider(); + _typeMappingSource = _serviceProvider.GetRequiredService(); + _generator = _serviceProvider.GetRequiredService(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + _serviceProvider.Dispose(); + } + + [Test] + public void DesignTimeServicesUseFirebirdGenerator() + { + Assert.IsInstanceOf(_generator); + } + + [Test] + public void AllTypeMappingsHaveDefaultUsableByCompiledModel() + { + var mappingTypes = typeof(FbTypeMappingSource).Assembly.GetTypes() + .Where(t => typeof(RelationalTypeMapping).IsAssignableFrom(t) && !t.IsAbstract) + .ToList(); + Assert.IsNotEmpty(mappingTypes); + + foreach (var mappingType in mappingTypes) + { + // Mirrors the requirements of CSharpRuntimeAnnotationCodeGenerator.CreateDefaultTypeMapping. + var defaultProperty = mappingType.GetProperty("Default"); + Assert.IsNotNull(defaultProperty, mappingType.Name); + Assert.IsTrue(defaultProperty.GetMethod.IsStatic && defaultProperty.GetMethod.IsPublic, mappingType.Name); + Assert.IsTrue(mappingType.IsAssignableFrom(defaultProperty.PropertyType), mappingType.Name); + Assert.IsInstanceOf(mappingType, defaultProperty.GetValue(null), mappingType.Name); + } + } + + [TestCase(typeof(DateTime), "TIMESTAMP", null)] + [TestCase(typeof(DateTime), "DATE", FbDbType.Date)] + [TestCase(typeof(TimeSpan), "TIME", null)] + [TestCase(typeof(string), "VARCHAR(50)", FbDbType.VarChar)] + [TestCase(typeof(string), "CHAR(10)", FbDbType.Char)] + [TestCase(typeof(string), "BLOB SUB_TYPE TEXT", null)] + public void GeneratesFbDbTypeOnlyWhenItDiffersFromDefault(Type clrType, string storeType, FbDbType? expectedFbDbType) + { + var mapping = _typeMappingSource.FindMapping(clrType, storeType); + + var code = Generate(mapping); + + if (expectedFbDbType == null) + { + StringAssert.DoesNotContain("fbDbType", code); + } + else + { + StringAssert.Contains($"property.TypeMapping = (({mapping.GetType().Name})property.TypeMapping).Clone(fbDbType: FbDbType.{expectedFbDbType})", code); + } + } + + [TestCase(typeof(DateTime), "DATE")] + [TestCase(typeof(DateTime), "TIMESTAMP")] + [TestCase(typeof(string), "VARCHAR(50)")] + [TestCase(typeof(string), "CHAR(10)")] + [TestCase(typeof(string), "BLOB SUB_TYPE TEXT")] + [TestCase(typeof(TimeSpan), "TIME")] + public void RecreatedMappingBehavesLikeOriginal(Type clrType, string storeType) + { + var mapping = _typeMappingSource.FindMapping(clrType, storeType); + var fbDbType = (FbDbType)mapping.GetType().GetProperty("FbDbType").GetValue(mapping); + + // What the generated code does: Default.Clone(mappingInfo: ...), followed by Clone(fbDbType: ...) when needed. + var defaultMapping = (RelationalTypeMapping)mapping.GetType().GetProperty("Default", BindingFlags.Public | BindingFlags.Static).GetValue(null); + var recreated = defaultMapping.Clone(new RelationalTypeMappingInfo(storeTypeName: mapping.StoreType, size: mapping.Size, fixedLength: mapping.IsFixedLength, dbType: mapping.DbType)); + recreated = (RelationalTypeMapping)recreated.GetType().GetMethod("Clone", [typeof(FbDbType)]).Invoke(recreated, [fbDbType]); + + var value = clrType == typeof(DateTime) ? new DateTime(2024, 5, 6, 7, 8, 9) : clrType == typeof(TimeSpan) ? new TimeSpan(7, 8, 9) : (object)"x"; + using var command = new FbCommand(); + var original = (FbParameter)mapping.CreateParameter(command, "p", value); + var copy = (FbParameter)recreated.CreateParameter(command, "p", value); + + Assert.AreEqual(mapping.StoreType, recreated.StoreType); + Assert.AreEqual(original.FbDbType, copy.FbDbType); + // FbTimeSpanTypeMapping cannot generate literals at all: its format string leaves the '.' unescaped. + if (clrType != typeof(TimeSpan)) + { + Assert.AreEqual(mapping.GenerateSqlLiteral(value), recreated.GenerateSqlLiteral(value)); + } + } + + string Generate(RelationalTypeMapping mapping) + { + var parameters = new CSharpRuntimeAnnotationCodeGeneratorParameters( + "property", + "TestEntityType", + "TestNamespace", + new IndentedStringBuilder(), + new IndentedStringBuilder(), + new HashSet(), + new Dictionary(), + new Dictionary(), + new Dictionary(), + nullable: false, + nativeAot: true); + _generator.Create(mapping, parameters); + return parameters.MainBuilder.ToString(); + } +} +#pragma warning restore EF1001 diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj index 97754a322..68c08142a 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbCSharpRuntimeAnnotationCodeGenerator.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbCSharpRuntimeAnnotationCodeGenerator.cs new file mode 100644 index 000000000..0aa0ad583 --- /dev/null +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbCSharpRuntimeAnnotationCodeGenerator.cs @@ -0,0 +1,89 @@ +/* + * The contents of this file are subject to the Initial + * Developer's Public License Version 1.0 (the "License"); + * you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt. + * + * Software distributed under the License is distributed on + * an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either + * express or implied. See the License for the specific + * language governing rights and limitations under the License. + * + * All Rights Reserved. + */ + +//$Authors = Niek Schoemaker + +using System; +using FirebirdSql.Data.FirebirdClient; +using FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Design.Internal; +using Microsoft.EntityFrameworkCore.Storage; + +namespace FirebirdSql.EntityFrameworkCore.Firebird.Design.Internal; + +#pragma warning disable EF1001 +public class FbCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator +{ + int _typeMappingNestingCount; + + public FbCSharpRuntimeAnnotationCodeGenerator( + CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies, + RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies) + : base(dependencies, relationalDependencies) + { } + + public override bool Create( + CoreTypeMapping typeMapping, + CSharpRuntimeAnnotationCodeGeneratorParameters parameters, + ValueComparer valueComparer = null, + ValueComparer keyValueComparer = null, + ValueComparer providerValueComparer = null) + { + _typeMappingNestingCount++; + try + { + var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer); + AddFbDbType(typeMapping, parameters); + return result; + } + finally + { + _typeMappingNestingCount--; + } + } + + // The base generator recreates a mapping as Default.Clone(...) from the standard RelationalTypeMappingParameters only, + // so the FbDbType of mappings that have several variants (e.g. TIMESTAMP and DATE) would silently be the one of Default. + void AddFbDbType(CoreTypeMapping typeMapping, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) + { + var (fbDbType, defaultFbDbType) = typeMapping switch + { + FbDateTimeTypeMapping mapping => (mapping.FbDbType, FbDateTimeTypeMapping.Default.FbDbType), + FbStringTypeMapping mapping => (mapping.FbDbType, FbStringTypeMapping.Default.FbDbType), + FbTimeSpanTypeMapping mapping => (mapping.FbDbType, FbTimeSpanTypeMapping.Default.FbDbType), + _ => ((FbDbType?)null, (FbDbType?)null), + }; + if (fbDbType == defaultFbDbType) + { + return; + } + + if (_typeMappingNestingCount > 1) + { + throw new NotSupportedException( + $"Type mapping '{typeMapping.GetType().Name}' with store type '{((RelationalTypeMapping)typeMapping).StoreType}' and FbDbType '{fbDbType}' is not supported as an element type mapping in a compiled model."); + } + + var code = Dependencies.CSharpHelper; + AddNamespace(typeof(FbDbType), parameters.Namespaces); + parameters.MainBuilder + .AppendLine(";") + .Append($"{parameters.TargetName}.TypeMapping = (({typeMapping.GetType().Name}){parameters.TargetName}.TypeMapping).Clone(fbDbType: ") + .Append(code.Literal(fbDbType.Value)) + .Append(")"); + } +} +#pragma warning restore EF1001 diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbDesignTimeServices.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbDesignTimeServices.cs index b01da4de0..5cd931314 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbDesignTimeServices.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbDesignTimeServices.cs @@ -18,6 +18,7 @@ using FirebirdSql.EntityFrameworkCore.Firebird.Scaffolding.Internal; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Scaffolding; using Microsoft.Extensions.DependencyInjection; @@ -28,10 +29,13 @@ public class FbDesignTimeServices : IDesignTimeServices public void ConfigureDesignTimeServices(IServiceCollection serviceCollection) { serviceCollection.AddEntityFrameworkFirebird(); +#pragma warning disable EF1001 new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection) .TryAdd() + .TryAdd() .TryAdd() .TryAdd() .TryAddCoreServices(); +#pragma warning restore EF1001 } } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbBoolTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbBoolTypeMapping.cs index a4c0a89a8..5b175fbd0 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbBoolTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbBoolTypeMapping.cs @@ -21,6 +21,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; public class FbBoolTypeMapping : BoolTypeMapping { + public static new FbBoolTypeMapping Default { get; } = new(); + public FbBoolTypeMapping() : base("BOOLEAN", System.Data.DbType.Boolean) { } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbByteArrayTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbByteArrayTypeMapping.cs index dbd4bd513..ee3923e7d 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbByteArrayTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbByteArrayTypeMapping.cs @@ -22,6 +22,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; public class FbByteArrayTypeMapping : ByteArrayTypeMapping { + public static new FbByteArrayTypeMapping Default { get; } = new(); + public FbByteArrayTypeMapping() : base("BLOB SUB_TYPE BINARY", System.Data.DbType.Binary) { } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateOnlyTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateOnlyTypeMapping.cs index 2cddcda2b..5edad4de7 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateOnlyTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateOnlyTypeMapping.cs @@ -21,6 +21,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; public class FbDateOnlyTypeMapping : DateOnlyTypeMapping { + public static new FbDateOnlyTypeMapping Default { get; } = new("DATE"); + public FbDateOnlyTypeMapping(string storeType) : base(storeType) { } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateTimeTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateTimeTypeMapping.cs index b9dc140d9..1709ff5ac 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateTimeTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateTimeTypeMapping.cs @@ -26,6 +26,8 @@ public class FbDateTimeTypeMapping : DateTimeTypeMapping { readonly FbDbType _fbDbType; + public static new FbDateTimeTypeMapping Default { get; } = new("TIMESTAMP", FbDbType.TimeStamp); + public FbDateTimeTypeMapping(string storeType, FbDbType fbDbType) : base(storeType) { @@ -38,6 +40,11 @@ protected FbDateTimeTypeMapping(RelationalTypeMappingParameters parameters, FbDb _fbDbType = fbDbType; } + public FbDbType FbDbType => _fbDbType; + + public virtual FbDateTimeTypeMapping Clone(FbDbType fbDbType) + => new FbDateTimeTypeMapping(Parameters, fbDbType); + protected override void ConfigureParameter(DbParameter parameter) { ((FbParameter)parameter).FbDbType = _fbDbType; diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbGuidTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbGuidTypeMapping.cs index 2220ea5b2..ebef1c320 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbGuidTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbGuidTypeMapping.cs @@ -23,6 +23,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; public class FbGuidTypeMapping : GuidTypeMapping { + public static new FbGuidTypeMapping Default { get; } = new(); + public FbGuidTypeMapping() : base("CHAR(16) CHARACTER SET OCTETS") { } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbStringTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbStringTypeMapping.cs index d61439c13..e00e93fca 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbStringTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbStringTypeMapping.cs @@ -26,6 +26,8 @@ public class FbStringTypeMapping : StringTypeMapping { readonly FbDbType _fbDbType; + public static new FbStringTypeMapping Default { get; } = new("BLOB SUB_TYPE TEXT", System.Data.DbType.String, FbDbType.Text); + public FbStringTypeMapping(string storeType, DbType dbType, FbDbType fbDbType, int? size = null, bool unicode = true) : base(storeType, dbType, unicode: unicode, size: size) { @@ -38,6 +40,11 @@ protected FbStringTypeMapping(RelationalTypeMappingParameters parameters, FbDbTy _fbDbType = fbDbType; } + public FbDbType FbDbType => _fbDbType; + + public virtual FbStringTypeMapping Clone(FbDbType fbDbType) + => new FbStringTypeMapping(Parameters, fbDbType); + protected override void ConfigureParameter(DbParameter parameter) { ((FbParameter)parameter).FbDbType = _fbDbType; diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeOnlyTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeOnlyTypeMapping.cs index d9d4138a0..d1b7b95b8 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeOnlyTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeOnlyTypeMapping.cs @@ -21,6 +21,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal; public class FbTimeOnlyTypeMapping : TimeOnlyTypeMapping { + public static new FbTimeOnlyTypeMapping Default { get; } = new("TIME"); + public FbTimeOnlyTypeMapping(string storeType) : base(storeType) { } diff --git a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeSpanTypeMapping.cs b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeSpanTypeMapping.cs index 0dac60aff..835ad0140 100644 --- a/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeSpanTypeMapping.cs +++ b/src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbTimeSpanTypeMapping.cs @@ -26,6 +26,8 @@ public class FbTimeSpanTypeMapping : TimeSpanTypeMapping { readonly FbDbType _fbDbType; + public static new FbTimeSpanTypeMapping Default { get; } = new("TIME", FbDbType.Time); + public FbTimeSpanTypeMapping(string storeType, FbDbType fbDbType) : base(storeType) { @@ -38,6 +40,11 @@ protected FbTimeSpanTypeMapping(RelationalTypeMappingParameters parameters, FbDb _fbDbType = fbDbType; } + public FbDbType FbDbType => _fbDbType; + + public virtual FbTimeSpanTypeMapping Clone(FbDbType fbDbType) + => new FbTimeSpanTypeMapping(Parameters, fbDbType); + protected override void ConfigureParameter(DbParameter parameter) { ((FbParameter)parameter).FbDbType = _fbDbType;