Skip to content

Commit a16b873

Browse files
Support type mappings in NativeAOT compiled models.
dotnet ef dbcontext optimize --nativeaot failed because the Firebird type mappings had no public static Default property. Every Fb*TypeMapping now has one. The generated code recreates a mapping as Default.Clone(...) from the standard RelationalTypeMappingParameters only, so FbDateTimeTypeMapping, FbStringTypeMapping and FbTimeSpanTypeMapping additionally expose FbDbType and Clone(FbDbType), and the new FbCSharpRuntimeAnnotationCodeGenerator emits that Clone when the FbDbType differs from the one of Default (e.g. DATE vs TIMESTAMP, CHAR vs VARCHAR). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent fef2e3a commit a16b873

12 files changed

Lines changed: 278 additions & 0 deletions
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* The contents of this file are subject to the Initial
3+
* Developer's Public License Version 1.0 (the "License");
4+
* you may not use this file except in compliance with the
5+
* License. You may obtain a copy of the License at
6+
* https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt.
7+
*
8+
* Software distributed under the License is distributed on
9+
* an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
10+
* express or implied. See the License for the specific
11+
* language governing rights and limitations under the License.
12+
*
13+
* All Rights Reserved.
14+
*/
15+
16+
//$Authors = Niek Schoemaker
17+
18+
using System;
19+
using System.Collections.Generic;
20+
using System.Linq;
21+
using System.Reflection;
22+
using FirebirdSql.Data.FirebirdClient;
23+
using FirebirdSql.EntityFrameworkCore.Firebird.Design.Internal;
24+
using FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal;
25+
using Microsoft.EntityFrameworkCore.Design;
26+
using Microsoft.EntityFrameworkCore.Design.Internal;
27+
using Microsoft.EntityFrameworkCore.Infrastructure;
28+
using Microsoft.EntityFrameworkCore.Metadata;
29+
using Microsoft.EntityFrameworkCore.Storage;
30+
using Microsoft.Extensions.DependencyInjection;
31+
using NUnit.Framework;
32+
33+
namespace FirebirdSql.EntityFrameworkCore.Firebird.Tests.Design;
34+
35+
#pragma warning disable EF1001
36+
public class FbCSharpRuntimeAnnotationCodeGeneratorTests
37+
{
38+
ServiceProvider _serviceProvider;
39+
IRelationalTypeMappingSource _typeMappingSource;
40+
ICSharpRuntimeAnnotationCodeGenerator _generator;
41+
42+
[OneTimeSetUp]
43+
public void OneTimeSetUp()
44+
{
45+
var services = new ServiceCollection().AddEntityFrameworkDesignTimeServices();
46+
new FbDesignTimeServices().ConfigureDesignTimeServices(services);
47+
_serviceProvider = services.BuildServiceProvider();
48+
_typeMappingSource = _serviceProvider.GetRequiredService<IRelationalTypeMappingSource>();
49+
_generator = _serviceProvider.GetRequiredService<ICSharpRuntimeAnnotationCodeGenerator>();
50+
}
51+
52+
[OneTimeTearDown]
53+
public void OneTimeTearDown()
54+
{
55+
_serviceProvider.Dispose();
56+
}
57+
58+
[Test]
59+
public void DesignTimeServicesUseFirebirdGenerator()
60+
{
61+
Assert.IsInstanceOf<FbCSharpRuntimeAnnotationCodeGenerator>(_generator);
62+
}
63+
64+
[Test]
65+
public void AllTypeMappingsHaveDefaultUsableByCompiledModel()
66+
{
67+
var mappingTypes = typeof(FbTypeMappingSource).Assembly.GetTypes()
68+
.Where(t => typeof(RelationalTypeMapping).IsAssignableFrom(t) && !t.IsAbstract)
69+
.ToList();
70+
Assert.IsNotEmpty(mappingTypes);
71+
72+
foreach (var mappingType in mappingTypes)
73+
{
74+
// Mirrors the requirements of CSharpRuntimeAnnotationCodeGenerator.CreateDefaultTypeMapping.
75+
var defaultProperty = mappingType.GetProperty("Default");
76+
Assert.IsNotNull(defaultProperty, mappingType.Name);
77+
Assert.IsTrue(defaultProperty.GetMethod.IsStatic && defaultProperty.GetMethod.IsPublic, mappingType.Name);
78+
Assert.IsTrue(mappingType.IsAssignableFrom(defaultProperty.PropertyType), mappingType.Name);
79+
Assert.IsInstanceOf(mappingType, defaultProperty.GetValue(null), mappingType.Name);
80+
}
81+
}
82+
83+
[TestCase(typeof(DateTime), "TIMESTAMP", null)]
84+
[TestCase(typeof(DateTime), "DATE", FbDbType.Date)]
85+
[TestCase(typeof(TimeSpan), "TIME", null)]
86+
[TestCase(typeof(string), "VARCHAR(50)", FbDbType.VarChar)]
87+
[TestCase(typeof(string), "CHAR(10)", FbDbType.Char)]
88+
[TestCase(typeof(string), "BLOB SUB_TYPE TEXT", null)]
89+
public void GeneratesFbDbTypeOnlyWhenItDiffersFromDefault(Type clrType, string storeType, FbDbType? expectedFbDbType)
90+
{
91+
var mapping = _typeMappingSource.FindMapping(clrType, storeType);
92+
93+
var code = Generate(mapping);
94+
95+
if (expectedFbDbType == null)
96+
{
97+
StringAssert.DoesNotContain("fbDbType", code);
98+
}
99+
else
100+
{
101+
StringAssert.Contains($"property.TypeMapping = (({mapping.GetType().Name})property.TypeMapping).Clone(fbDbType: FbDbType.{expectedFbDbType})", code);
102+
}
103+
}
104+
105+
[TestCase(typeof(DateTime), "DATE")]
106+
[TestCase(typeof(DateTime), "TIMESTAMP")]
107+
[TestCase(typeof(string), "VARCHAR(50)")]
108+
[TestCase(typeof(string), "CHAR(10)")]
109+
[TestCase(typeof(string), "BLOB SUB_TYPE TEXT")]
110+
[TestCase(typeof(TimeSpan), "TIME")]
111+
public void RecreatedMappingBehavesLikeOriginal(Type clrType, string storeType)
112+
{
113+
var mapping = _typeMappingSource.FindMapping(clrType, storeType);
114+
var fbDbType = (FbDbType)mapping.GetType().GetProperty("FbDbType").GetValue(mapping);
115+
116+
// What the generated code does: Default.Clone(mappingInfo: ...), followed by Clone(fbDbType: ...) when needed.
117+
var defaultMapping = (RelationalTypeMapping)mapping.GetType().GetProperty("Default", BindingFlags.Public | BindingFlags.Static).GetValue(null);
118+
var recreated = defaultMapping.Clone(new RelationalTypeMappingInfo(storeTypeName: mapping.StoreType, size: mapping.Size, fixedLength: mapping.IsFixedLength, dbType: mapping.DbType));
119+
recreated = (RelationalTypeMapping)recreated.GetType().GetMethod("Clone", [typeof(FbDbType)]).Invoke(recreated, [fbDbType]);
120+
121+
var value = clrType == typeof(DateTime) ? new DateTime(2024, 5, 6, 7, 8, 9) : clrType == typeof(TimeSpan) ? new TimeSpan(7, 8, 9) : (object)"x";
122+
using var command = new FbCommand();
123+
var original = (FbParameter)mapping.CreateParameter(command, "p", value);
124+
var copy = (FbParameter)recreated.CreateParameter(command, "p", value);
125+
126+
Assert.AreEqual(mapping.StoreType, recreated.StoreType);
127+
Assert.AreEqual(original.FbDbType, copy.FbDbType);
128+
// FbTimeSpanTypeMapping cannot generate literals at all: its format string leaves the '.' unescaped.
129+
if (clrType != typeof(TimeSpan))
130+
{
131+
Assert.AreEqual(mapping.GenerateSqlLiteral(value), recreated.GenerateSqlLiteral(value));
132+
}
133+
}
134+
135+
string Generate(RelationalTypeMapping mapping)
136+
{
137+
var parameters = new CSharpRuntimeAnnotationCodeGeneratorParameters(
138+
"property",
139+
"TestEntityType",
140+
"TestNamespace",
141+
new IndentedStringBuilder(),
142+
new IndentedStringBuilder(),
143+
new HashSet<string>(),
144+
new Dictionary<string, object>(),
145+
new Dictionary<object, string>(),
146+
new Dictionary<ITypeBase, string>(),
147+
nullable: false,
148+
nativeAot: true);
149+
_generator.Create(mapping, parameters);
150+
return parameters.MainBuilder.ToString();
151+
}
152+
}
153+
#pragma warning restore EF1001

‎src/FirebirdSql.EntityFrameworkCore.Firebird.Tests/FirebirdSql.EntityFrameworkCore.Firebird.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<Using Include="NUnit.Framework.Legacy.StringAssert" Alias="StringAssert" />
1818
</ItemGroup>
1919
<ItemGroup>
20+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="$(EFCoreReferencePackageVersion)" />
2021
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
2122
<PackageReference Include="NUnit" Version="4.2.2" />
2223
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* The contents of this file are subject to the Initial
3+
* Developer's Public License Version 1.0 (the "License");
4+
* you may not use this file except in compliance with the
5+
* License. You may obtain a copy of the License at
6+
* https://github.com/FirebirdSQL/NETProvider/raw/master/license.txt.
7+
*
8+
* Software distributed under the License is distributed on
9+
* an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
10+
* express or implied. See the License for the specific
11+
* language governing rights and limitations under the License.
12+
*
13+
* All Rights Reserved.
14+
*/
15+
16+
//$Authors = Niek Schoemaker
17+
18+
using System;
19+
using FirebirdSql.Data.FirebirdClient;
20+
using FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal;
21+
using Microsoft.EntityFrameworkCore.ChangeTracking;
22+
using Microsoft.EntityFrameworkCore.Design.Internal;
23+
using Microsoft.EntityFrameworkCore.Storage;
24+
25+
namespace FirebirdSql.EntityFrameworkCore.Firebird.Design.Internal;
26+
27+
#pragma warning disable EF1001
28+
public class FbCSharpRuntimeAnnotationCodeGenerator : RelationalCSharpRuntimeAnnotationCodeGenerator
29+
{
30+
int _typeMappingNestingCount;
31+
32+
public FbCSharpRuntimeAnnotationCodeGenerator(
33+
CSharpRuntimeAnnotationCodeGeneratorDependencies dependencies,
34+
RelationalCSharpRuntimeAnnotationCodeGeneratorDependencies relationalDependencies)
35+
: base(dependencies, relationalDependencies)
36+
{ }
37+
38+
public override bool Create(
39+
CoreTypeMapping typeMapping,
40+
CSharpRuntimeAnnotationCodeGeneratorParameters parameters,
41+
ValueComparer valueComparer = null,
42+
ValueComparer keyValueComparer = null,
43+
ValueComparer providerValueComparer = null)
44+
{
45+
_typeMappingNestingCount++;
46+
try
47+
{
48+
var result = base.Create(typeMapping, parameters, valueComparer, keyValueComparer, providerValueComparer);
49+
AddFbDbType(typeMapping, parameters);
50+
return result;
51+
}
52+
finally
53+
{
54+
_typeMappingNestingCount--;
55+
}
56+
}
57+
58+
// The base generator recreates a mapping as Default.Clone(...) from the standard RelationalTypeMappingParameters only,
59+
// so the FbDbType of mappings that have several variants (e.g. TIMESTAMP and DATE) would silently be the one of Default.
60+
void AddFbDbType(CoreTypeMapping typeMapping, CSharpRuntimeAnnotationCodeGeneratorParameters parameters)
61+
{
62+
var (fbDbType, defaultFbDbType) = typeMapping switch
63+
{
64+
FbDateTimeTypeMapping mapping => (mapping.FbDbType, FbDateTimeTypeMapping.Default.FbDbType),
65+
FbStringTypeMapping mapping => (mapping.FbDbType, FbStringTypeMapping.Default.FbDbType),
66+
FbTimeSpanTypeMapping mapping => (mapping.FbDbType, FbTimeSpanTypeMapping.Default.FbDbType),
67+
_ => ((FbDbType?)null, (FbDbType?)null),
68+
};
69+
if (fbDbType == defaultFbDbType)
70+
{
71+
return;
72+
}
73+
74+
if (_typeMappingNestingCount > 1)
75+
{
76+
throw new NotSupportedException(
77+
$"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.");
78+
}
79+
80+
var code = Dependencies.CSharpHelper;
81+
AddNamespace(typeof(FbDbType), parameters.Namespaces);
82+
parameters.MainBuilder
83+
.AppendLine(";")
84+
.Append($"{parameters.TargetName}.TypeMapping = (({typeMapping.GetType().Name}){parameters.TargetName}.TypeMapping).Clone(fbDbType: ")
85+
.Append(code.Literal(fbDbType.Value))
86+
.Append(")");
87+
}
88+
}
89+
#pragma warning restore EF1001

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Design/Internal/FbDesignTimeServices.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using FirebirdSql.EntityFrameworkCore.Firebird.Scaffolding.Internal;
1919
using Microsoft.EntityFrameworkCore;
2020
using Microsoft.EntityFrameworkCore.Design;
21+
using Microsoft.EntityFrameworkCore.Design.Internal;
2122
using Microsoft.EntityFrameworkCore.Scaffolding;
2223
using Microsoft.Extensions.DependencyInjection;
2324

@@ -28,10 +29,13 @@ public class FbDesignTimeServices : IDesignTimeServices
2829
public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
2930
{
3031
serviceCollection.AddEntityFrameworkFirebird();
32+
#pragma warning disable EF1001
3133
new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
3234
.TryAdd<IAnnotationCodeGenerator, AnnotationCodeGenerator>()
35+
.TryAdd<ICSharpRuntimeAnnotationCodeGenerator, FbCSharpRuntimeAnnotationCodeGenerator>()
3336
.TryAdd<IDatabaseModelFactory, FbDatabaseModelFactory>()
3437
.TryAdd<IProviderConfigurationCodeGenerator, FbProviderCodeGenerator>()
3538
.TryAddCoreServices();
39+
#pragma warning restore EF1001
3640
}
3741
}

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbBoolTypeMapping.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal;
2121

2222
public class FbBoolTypeMapping : BoolTypeMapping
2323
{
24+
public static new FbBoolTypeMapping Default { get; } = new();
25+
2426
public FbBoolTypeMapping()
2527
: base("BOOLEAN", System.Data.DbType.Boolean)
2628
{ }

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbByteArrayTypeMapping.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal;
2222

2323
public class FbByteArrayTypeMapping : ByteArrayTypeMapping
2424
{
25+
public static new FbByteArrayTypeMapping Default { get; } = new();
26+
2527
public FbByteArrayTypeMapping()
2628
: base("BLOB SUB_TYPE BINARY", System.Data.DbType.Binary)
2729
{ }

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateOnlyTypeMapping.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal;
2121

2222
public class FbDateOnlyTypeMapping : DateOnlyTypeMapping
2323
{
24+
public static new FbDateOnlyTypeMapping Default { get; } = new("DATE");
25+
2426
public FbDateOnlyTypeMapping(string storeType)
2527
: base(storeType)
2628
{ }

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbDateTimeTypeMapping.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public class FbDateTimeTypeMapping : DateTimeTypeMapping
2626
{
2727
readonly FbDbType _fbDbType;
2828

29+
public static new FbDateTimeTypeMapping Default { get; } = new("TIMESTAMP", FbDbType.TimeStamp);
30+
2931
public FbDateTimeTypeMapping(string storeType, FbDbType fbDbType)
3032
: base(storeType)
3133
{
@@ -38,6 +40,11 @@ protected FbDateTimeTypeMapping(RelationalTypeMappingParameters parameters, FbDb
3840
_fbDbType = fbDbType;
3941
}
4042

43+
public FbDbType FbDbType => _fbDbType;
44+
45+
public virtual FbDateTimeTypeMapping Clone(FbDbType fbDbType)
46+
=> new FbDateTimeTypeMapping(Parameters, fbDbType);
47+
4148
protected override void ConfigureParameter(DbParameter parameter)
4249
{
4350
((FbParameter)parameter).FbDbType = _fbDbType;

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbGuidTypeMapping.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ namespace FirebirdSql.EntityFrameworkCore.Firebird.Storage.Internal;
2323

2424
public class FbGuidTypeMapping : GuidTypeMapping
2525
{
26+
public static new FbGuidTypeMapping Default { get; } = new();
27+
2628
public FbGuidTypeMapping()
2729
: base("CHAR(16) CHARACTER SET OCTETS")
2830
{ }

‎src/FirebirdSql.EntityFrameworkCore.Firebird/Storage/Internal/FbStringTypeMapping.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public class FbStringTypeMapping : StringTypeMapping
2626
{
2727
readonly FbDbType _fbDbType;
2828

29+
public static new FbStringTypeMapping Default { get; } = new("BLOB SUB_TYPE TEXT", System.Data.DbType.String, FbDbType.Text);
30+
2931
public FbStringTypeMapping(string storeType, DbType dbType, FbDbType fbDbType, int? size = null, bool unicode = true)
3032
: base(storeType, dbType, unicode: unicode, size: size)
3133
{
@@ -38,6 +40,11 @@ protected FbStringTypeMapping(RelationalTypeMappingParameters parameters, FbDbTy
3840
_fbDbType = fbDbType;
3941
}
4042

43+
public FbDbType FbDbType => _fbDbType;
44+
45+
public virtual FbStringTypeMapping Clone(FbDbType fbDbType)
46+
=> new FbStringTypeMapping(Parameters, fbDbType);
47+
4148
protected override void ConfigureParameter(DbParameter parameter)
4249
{
4350
((FbParameter)parameter).FbDbType = _fbDbType;

0 commit comments

Comments
 (0)