Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<IRelationalTypeMappingSource>();
_generator = _serviceProvider.GetRequiredService<ICSharpRuntimeAnnotationCodeGenerator>();
}

[OneTimeTearDown]
public void OneTimeTearDown()
{
_serviceProvider.Dispose();
}

[Test]
public void DesignTimeServicesUseFirebirdGenerator()
{
Assert.IsInstanceOf<FbCSharpRuntimeAnnotationCodeGenerator>(_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<string>(),
new Dictionary<string, object>(),
new Dictionary<object, string>(),
new Dictionary<ITypeBase, string>(),
nullable: false,
nativeAot: true);
_generator.Create(mapping, parameters);
return parameters.MainBuilder.ToString();
}
}
#pragma warning restore EF1001
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<Using Include="NUnit.Framework.Legacy.StringAssert" Alias="StringAssert" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="$(EFCoreReferencePackageVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,10 +29,13 @@ public class FbDesignTimeServices : IDesignTimeServices
public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
{
serviceCollection.AddEntityFrameworkFirebird();
#pragma warning disable EF1001
new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
.TryAdd<IAnnotationCodeGenerator, AnnotationCodeGenerator>()
.TryAdd<ICSharpRuntimeAnnotationCodeGenerator, FbCSharpRuntimeAnnotationCodeGenerator>()
.TryAdd<IDatabaseModelFactory, FbDatabaseModelFactory>()
.TryAdd<IProviderConfigurationCodeGenerator, FbProviderCodeGenerator>()
.TryAddCoreServices();
#pragma warning restore EF1001
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{ }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down