From 121e69a0a8eca97e2e30b3a769aefb7fc3009fd9 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 16 Jul 2025 15:11:08 +0200 Subject: [PATCH 1/2] Use SqlClient 6.1.0-preview2.25178.5 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 086bb8e4c66..c0abbe61659 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + From e3bf2be5ccfe6f5ba333ae08368790f288a0bf7c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Wed, 16 Jul 2025 16:38:43 +0200 Subject: [PATCH 2/2] Implement support for SQL Server vector search Closes #34659 --- src/EFCore.SqlServer/EFCore.SqlServer.csproj | 1 + .../SqlServerDbFunctionsExtensions.cs | 28 +++ .../Internal/SqlServerModelValidator.cs | 29 ++++ .../Properties/SqlServerStrings.Designer.cs | 22 +++ .../Properties/SqlServerStrings.resx | 9 + .../SqlServerMemberTranslatorProvider.cs | 3 +- .../SqlServerMethodCallTranslatorProvider.cs | 3 +- .../Translators/SqlServerVectorTranslator.cs | 97 +++++++++++ .../Internal/SqlServerDatabaseModelFactory.cs | 32 ++-- .../Internal/SqlServerTypeMappingSource.cs | 88 +++++----- .../Internal/SqlServerVectorTypeMapping.cs | 151 +++++++++++++++++ .../VectorTranslationsSqlServerTest.cs | 136 +++++++++++++++ .../SqlServerDatabaseModelFactoryTest.cs | 27 +++ .../TestUtilities/SqlServerCondition.cs | 1 + .../SqlServerConditionAttribute.cs | 5 + .../TestUtilities/TestEnvironment.cs | 29 ++++ .../SqlServerModelValidatorTest.cs | 55 ++++++ .../Storage/SqlServerTypeMappingSourceTest.cs | 39 ++++- .../Storage/SqlServerTypeMappingTest.cs | 160 +++--------------- 19 files changed, 712 insertions(+), 203 deletions(-) create mode 100644 src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs create mode 100644 src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs diff --git a/src/EFCore.SqlServer/EFCore.SqlServer.csproj b/src/EFCore.SqlServer/EFCore.SqlServer.csproj index fff89e95202..64c3f917d64 100644 --- a/src/EFCore.SqlServer/EFCore.SqlServer.csproj +++ b/src/EFCore.SqlServer/EFCore.SqlServer.csproj @@ -50,6 +50,7 @@ + diff --git a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs index 94b3b3aca77..baa97ffb632 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs @@ -3,6 +3,8 @@ // ReSharper disable once CheckNamespace +using Microsoft.Data.SqlTypes; + namespace Microsoft.EntityFrameworkCore; /// @@ -2452,4 +2454,30 @@ public static long PatIndex( => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VariancePopulation))); #endregion Population variance + + #region Vector functions + + /// + /// Calculates the distance between two vectors using a specified distance metric. + /// + /// The instance. + /// + /// A string with the name of the distance metric to use to calculate the distance between the two given vectors. The following distance metrics are supported: cosine, euclidean or dot. + /// + /// The first vector. + /// The second vector. + /// + /// Vector distance is always exact and doesn't use any vector index, even if available. + /// + /// SQL Server documentation for VECTOR_DISTANCE. + /// Vectors in the SQL Database Engine. + public static double VectorDistance( + this DbFunctions _, + [NotParameterized] string distanceMetric, + SqlVector vector1, + SqlVector vector2) + where T : unmanaged + => throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(VectorDistance))); + + #endregion Vector functions } diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index 02e49289a8e..a40aea55bfe 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -2,10 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Extensions.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; @@ -43,6 +45,7 @@ public override void Validate(IModel model, IDiagnosticsLogger + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected virtual void ValidateVectorColumns( + IModel model, + IDiagnosticsLogger logger) + { + foreach (IConventionProperty property in model.GetEntityTypes() + .SelectMany(t => t.GetDeclaredProperties()) + .Where(p => p.ClrType.UnwrapNullableType() == typeof(SqlVector))) + { + if (property.GetTypeMapping() is not SqlServerVectorTypeMapping { Size: not null } vectorTypeMapping) + { + throw new InvalidOperationException(SqlServerStrings.VectorDimensionsMissing(property.DeclaringType.DisplayName(), property.Name)); + } + + if (property.DeclaringType.IsMappedToJson()) + { + throw new InvalidOperationException(SqlServerStrings.VectorPropertiesNotSupportedInJson(property.DeclaringType.DisplayName(), property.Name)); + } + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs index 60215b821cb..32e9b4c4278 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.Designer.cs @@ -417,6 +417,28 @@ public static string TemporalSetOperationOnMismatchedSources(object? entityType) public static string TransientExceptionDetected => GetString("TransientExceptionDetected"); + /// + /// Vector properties require a positive size (number of dimensions). + /// + public static string VectorDimensionsInvalid + => GetString("VectorDimensionsInvalid"); + + /// + /// Vector property '{structuralType}.{propertyName}' was not configured with the number of dimensions. Set the column type to 'vector(x)' with the desired number of dimensions, or use the 'MaxLength' APIs. + /// + public static string VectorDimensionsMissing(object? structuralType, object? propertyName) + => string.Format( + GetString("VectorDimensionsMissing", nameof(structuralType), nameof(propertyName)), + structuralType, propertyName); + + /// + /// Vector property '{propertyName}' is on '{structuralType}' which is mapped to JSON. Vector properties are not supported within JSON documents. + /// + public static string VectorPropertiesNotSupportedInJson(object? structuralType, object? propertyName) + => string.Format( + GetString("VectorPropertiesNotSupportedInJson", nameof(structuralType), nameof(propertyName)), + structuralType, propertyName); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx index dc00eb238cb..4d581755f60 100644 --- a/src/EFCore.SqlServer/Properties/SqlServerStrings.resx +++ b/src/EFCore.SqlServer/Properties/SqlServerStrings.resx @@ -369,4 +369,13 @@ An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure' to the 'UseSqlServer' call. + + Vector properties require a positive size (number of dimensions). + + + Vector property '{structuralType}.{propertyName}' was not configured with the number of dimensions. Set the column type to 'vector(x)' with the desired number of dimensions, or use the 'MaxLength' APIs. + + + Vector property '{propertyName}' is on '{structuralType}' which is mapped to JSON. Vector properties are not supported within JSON documents. + \ No newline at end of file diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs index f397ef5e767..a85edd1d63e 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMemberTranslatorProvider.cs @@ -30,7 +30,8 @@ public SqlServerMemberTranslatorProvider( new SqlServerDateTimeMemberTranslator(sqlExpressionFactory, typeMappingSource), new SqlServerStringMemberTranslator(sqlExpressionFactory), new SqlServerTimeSpanMemberTranslator(sqlExpressionFactory), - new SqlServerTimeOnlyMemberTranslator(sqlExpressionFactory) + new SqlServerTimeOnlyMemberTranslator(sqlExpressionFactory), + new SqlServerVectorTranslator(sqlExpressionFactory, typeMappingSource) ]); } } diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs index 79a99b8c437..d9952444e5a 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerMethodCallTranslatorProvider.cs @@ -42,7 +42,8 @@ public SqlServerMethodCallTranslatorProvider( new SqlServerNewGuidTranslator(sqlExpressionFactory), new SqlServerObjectToStringTranslator(sqlExpressionFactory, typeMappingSource), new SqlServerStringMethodTranslator(sqlExpressionFactory, sqlServerSingletonOptions), - new SqlServerTimeOnlyMethodTranslator(sqlExpressionFactory) + new SqlServerTimeOnlyMethodTranslator(sqlExpressionFactory), + new SqlServerVectorTranslator(sqlExpressionFactory, typeMappingSource) ]); } } diff --git a/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs new file mode 100644 index 00000000000..3e53d8baacf --- /dev/null +++ b/src/EFCore.SqlServer/Query/Internal/Translators/SqlServerVectorTranslator.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Data.SqlTypes; +using Microsoft.EntityFrameworkCore.Query.SqlExpressions; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.SqlServer.Query.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerVectorTranslator( + ISqlExpressionFactory sqlExpressionFactory, + IRelationalTypeMappingSource typeMappingSource) + : IMethodCallTranslator, IMemberTranslator +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method.DeclaringType == typeof(SqlServerDbFunctionsExtensions)) + { + switch (method.Name) + { + case nameof(SqlServerDbFunctionsExtensions.VectorDistance) + when arguments is [_, var distanceMetric, var vector1, var vector2]: + { + var vectorTypeMapping = vector1.TypeMapping ?? vector2.TypeMapping + ?? throw new InvalidOperationException( + "One of the arguments to EF.Functions.VectorDistance must be a vector column."); + + return sqlExpressionFactory.Function( + "VECTOR_DISTANCE", + [ + sqlExpressionFactory.ApplyTypeMapping(distanceMetric, typeMappingSource.FindMapping("varchar(max)")), + sqlExpressionFactory.ApplyTypeMapping(vector1, vectorTypeMapping), + sqlExpressionFactory.ApplyTypeMapping(vector2, vectorTypeMapping) + ], + nullable: true, + argumentsPropagateNullability: [true, true, true], + typeof(double), + typeMappingSource.FindMapping(typeof(double))); + } + } + } + + return null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + if (member.DeclaringType == typeof(SqlVector)) + { + switch (member.Name) + { + case nameof(SqlVector<>.Length) when instance is not null: + { + return sqlExpressionFactory.Function( + "VECTORPROPERTY", + [ + instance, + sqlExpressionFactory.Constant("Dimensions", typeMappingSource.FindMapping("varchar(max)")) + ], + nullable: true, + argumentsPropagateNullability: [true, true], + typeof(int), + typeMappingSource.FindMapping(typeof(int))); + } + } + } + + return null; + } +} + diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index 4b6fde64fdf..78de96ff183 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -395,7 +395,7 @@ FROM [sys].[types] AS [t] var precision = reader.GetValueOrDefault("precision"); var scale = reader.GetValueOrDefault("scale"); - var storeType = GetStoreType(systemType, maxLength, precision, scale); + var storeType = GetStoreType(systemType, maxLength, precision, scale, vectorDimensions: 0); _logger.TypeAliasFound(DisplayName(schema, userType), storeType); @@ -472,7 +472,7 @@ FROM [sys].[sequences] AS [s] storeType = value.storeType; } - storeType = GetStoreType(storeType, maxLength: 0, precision: precision, scale: scale); + storeType = GetStoreType(storeType, maxLength: 0, precision, scale, vectorDimensions: 0); _logger.SequenceFound(DisplayName(schema, name), storeType, cyclic, incrementBy, startValue, minValue, maxValue); @@ -730,6 +730,7 @@ private void GetColumns( CAST([c].[max_length] AS int) AS [max_length], CAST([c].[precision] AS int) AS [precision], CAST([c].[scale] AS int) AS [scale], + {(_compatibilityLevel is >= 170 ? "[c].[vector_dimensions]" : "NULL as [vector_dimensions]")}, [c].[is_nullable], [c].[is_identity], [dc].[definition] AS [default_sql], @@ -801,6 +802,7 @@ FROM [sys].[views] v var maxLength = dataRecord.GetValueOrDefault("max_length"); var precision = dataRecord.GetValueOrDefault("precision"); var scale = dataRecord.GetValueOrDefault("scale"); + var vectorDimensions = dataRecord.GetValueOrDefault("vector_dimensions"); var nullable = dataRecord.GetValueOrDefault("is_nullable"); var isIdentity = dataRecord.GetValueOrDefault("is_identity"); var defaultValueSql = dataRecord.GetValueOrDefault("default_sql"); @@ -835,15 +837,19 @@ FROM [sys].[views] v string storeType; string systemTypeName; - // Swap store type if type alias is used - if (typeAliases.TryGetValue($"[{dataTypeSchemaName}].[{dataTypeName}]", out var value)) + // If the store type is in our loaded aliases dictionary, resolve to the canonical type. + // Note that the vector type is implemented as an alias for varbinary, but we do not want + // to scaffold vectors as varbinary. + var fullQualifiedTypeName = $"[{dataTypeSchemaName}].[{dataTypeName}]"; + if (fullQualifiedTypeName is not "[sys].[vector]" + && typeAliases.TryGetValue(fullQualifiedTypeName, out var value)) { storeType = value.storeType; systemTypeName = value.typeName; } else { - storeType = GetStoreType(dataTypeName, maxLength, precision, scale); + storeType = GetStoreType(dataTypeName, maxLength, precision, scale, vectorDimensions); systemTypeName = dataTypeName; } @@ -995,16 +1001,16 @@ void Unwrap() } } - private static string GetStoreType(string dataTypeName, int maxLength, int precision, int scale) + private static string GetStoreType(string dataTypeName, int maxLength, int precision, int scale, int vectorDimensions) { - if (dataTypeName == "timestamp") + switch (dataTypeName) { - return "rowversion"; - } - - if (dataTypeName is "decimal" or "numeric") - { - return $"{dataTypeName}({precision}, {scale})"; + case "timestamp": + return "rowversion"; + case "decimal" or "numeric": + return $"{dataTypeName}({precision}, {scale})"; + case "vector": + return $"vector({vectorDimensions})"; } if (DateTimePrecisionTypes.Contains(dataTypeName) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs index b89cfbc9f60..1b0bbdb08a8 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerTypeMappingSource.cs @@ -3,6 +3,8 @@ using System.Collections; using System.Data; +using Microsoft.Data.SqlTypes; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -205,6 +207,7 @@ static SqlServerTypeMappingSource() { "varbinary(max)", [VariableLengthMaxBinary] }, { "varchar", [SqlServerStringTypeMapping.Default] }, { "varchar(max)", [VariableLengthMaxAnsiString] }, + { "vector", [SqlServerVectorTypeMapping.Default] }, { "xml", [Xml] } }; // ReSharper restore CoVariantArrayConversion @@ -304,62 +307,54 @@ public SqlServerTypeMappingSource( return mapping; } - if (clrType == typeof(ulong) && mappingInfo.IsRowVersion == true) + switch (clrType) { - return UlongRowversion; - } + case Type t when t == typeof(ulong) && mappingInfo.IsRowVersion is true: + return UlongRowversion; - if (clrType == typeof(long) && mappingInfo.IsRowVersion == true) - { - return LongRowversion; - } + case Type t when t == typeof(long) && mappingInfo.IsRowVersion is true: + return LongRowversion; - if (clrType == typeof(string)) - { - if (storeTypeName == "json") - { - return SqlServerStringTypeMapping.JsonTypeDefault; - } + case Type t when t == typeof(byte[]) && mappingInfo.IsRowVersion is true: + return Rowversion; - var isAnsi = mappingInfo.IsUnicode == false; - var isFixedLength = mappingInfo.IsFixedLength == true; - var maxSize = isAnsi ? 8000 : 4000; + case Type t when t == typeof(string) && storeTypeName == "json": + return SqlServerStringTypeMapping.JsonTypeDefault; - var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? isAnsi ? 900 : 450 : null); - if (size < 0 || size > maxSize) + case Type t when t == typeof(string): { - size = isFixedLength ? maxSize : null; - } + var isAnsi = mappingInfo.IsUnicode == false; + var isFixedLength = mappingInfo.IsFixedLength == true; + var maxSize = isAnsi ? 8000 : 4000; - if (size == null - && storeTypeName == null - && !mappingInfo.IsKeyOrIndex) - { - return isAnsi - ? isFixedLength - ? FixedLengthAnsiString - : VariableLengthMaxAnsiString - : isFixedLength - ? FixedLengthUnicodeString - : VariableLengthMaxUnicodeString; - } + var size = mappingInfo.Size ?? (mappingInfo.IsKeyOrIndex ? isAnsi ? 900 : 450 : null); + if (size < 0 || size > maxSize) + { + size = isFixedLength ? maxSize : null; + } - return new SqlServerStringTypeMapping( - unicode: !isAnsi, - size: size, - fixedLength: isFixedLength, - storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None, - useKeyComparison: mappingInfo.IsKey); - } + if (size == null + && storeTypeName == null + && !mappingInfo.IsKeyOrIndex) + { + return isAnsi + ? isFixedLength + ? FixedLengthAnsiString + : VariableLengthMaxAnsiString + : isFixedLength + ? FixedLengthUnicodeString + : VariableLengthMaxUnicodeString; + } - if (clrType == typeof(byte[])) - { - if (mappingInfo.IsRowVersion == true) - { - return Rowversion; + return new SqlServerStringTypeMapping( + unicode: !isAnsi, + size: size, + fixedLength: isFixedLength, + storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None, + useKeyComparison: mappingInfo.IsKey); } - if (mappingInfo.ElementTypeMapping == null) + case Type t when t == typeof(byte[]) && mappingInfo.ElementTypeMapping is null: { var isFixedLength = mappingInfo.IsFixedLength == true; @@ -376,6 +371,9 @@ public SqlServerTypeMappingSource( fixedLength: isFixedLength, storeTypePostfix: storeTypeName == null ? StoreTypePostfix.Size : StoreTypePostfix.None); } + + case Type t when t == typeof(SqlVector): + return new SqlServerVectorTypeMapping(mappingInfo.Size); } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs new file mode 100644 index 00000000000..b3c627f39a9 --- /dev/null +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerVectorTypeMapping.cs @@ -0,0 +1,151 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Data.SqlTypes; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; + +namespace Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class SqlServerVectorTypeMapping : RelationalTypeMapping +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static SqlServerVectorTypeMapping Default { get; } = new(dimensions: null); + + private static readonly VectorComparer _comparerInstance = new(); + + // Note that dimensions is mandatory with SQL Server vector. + // However, our scaffolder looks up each type mapping without the facets, to find out whether the scaffolded + // facet happens to be the default (and therefore can be omitted). So we allow constructing a SqlServerVectorTypeMapping + // without dimensions, and validate against it in SqlServerModelValidator. + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public SqlServerVectorTypeMapping(int? dimensions) + : this( + new RelationalTypeMappingParameters( + new CoreTypeMappingParameters(typeof(SqlVector), comparer: _comparerInstance), + "vector", + StoreTypePostfix.Size, + size: dimensions)) + { + if (dimensions is <= 0) + { + throw new InvalidOperationException(SqlServerStrings.VectorDimensionsInvalid); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected SqlServerVectorTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) + { + if (parameters.Size is <= 0) + { + throw new InvalidOperationException(SqlServerStrings.VectorDimensionsInvalid); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new SqlServerVectorTypeMapping(parameters); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override string GenerateNonNullSqlLiteral(object value) + { + Check.DebugAssert(Size is not null); + + var sqlVector = (SqlVector)value; + + if (sqlVector.IsNull) + { + return "NULL"; + } + + // SQL Server has an implicit cast from JSON arrays (as strings or as the json type) to vector - + // that's the literal representation (though use-cases are probably mostly contrived/testing-only). + var builder = new StringBuilder(); + var floats = sqlVector.Memory.Span; + + builder.Append("CAST('["); + + for (var i = 0; i < floats.Length; i++) + { + if (i > 0) + { + builder.Append(','); + } + + builder.Append(floats[i]); + } + + builder + .Append("]' AS VECTOR(") + .Append(Size) + .Append("))"); + + return builder.ToString(); + } + + private sealed class VectorComparer() : ValueComparer>( + (x, y) => CalculateEquality(x, y), + v => CalculateHashCode(v), + v => v) + { + // Note that we do not perform value comparison here, only checking that the SqlVector wraps the same memory. + // This is because vectors are basically immutable, and it's better to have more efficient change tracking + // equality checks. + private static bool CalculateEquality(SqlVector? x, SqlVector? y) + => x is null + ? y is null + : y is not null && (x.IsNull + ? y.IsNull + : !y.IsNull && x.Memory.Span == y.Memory.Span); + + private static int CalculateHashCode(SqlVector vector) + { + if (vector.IsNull) + { + return 0; + } + + var hash = new HashCode(); + + foreach (var value in vector.Memory.Span) + { + hash.Add(value); + } + + return hash.ToHashCode(); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs new file mode 100644 index 00000000000..d795d499ef2 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Translations/VectorTranslationsSqlServerTest.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.Data.SqlTypes; + +namespace Microsoft.EntityFrameworkCore.Query.Translations; + +[SqlServerCondition(SqlServerCondition.SupportsVectorType)] +public class VectorTranslationsSqlServerTest : IClassFixture +{ + private VectorQueryFixture Fixture { get; } + + public VectorTranslationsSqlServerTest(VectorQueryFixture fixture, ITestOutputHelper testOutputHelper) + { + Fixture = fixture; + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalFact] + public async Task VectorDistance_with_parameter() + { + using var ctx = CreateContext(); + + var vector = new SqlVector(new float[] { 1, 2, 100 }); + var results = await ctx.VectorEntities + .OrderBy(v => EF.Functions.VectorDistance("cosine", v.Vector, vector)) + .Take(1) + .ToListAsync(); + + Assert.Equal(2, results.Single().Id); + + AssertSql( + """ +@p='1' +@vector='Microsoft.Data.SqlTypes.SqlVector`1[System.Single]' (Size = 20) (DbType = Binary) + +SELECT TOP(@p) [v].[Id], [v].[Vector] +FROM [VectorEntities] AS [v] +ORDER BY VECTOR_DISTANCE('cosine', [v].[Vector], @vector) +"""); + } + + [ConditionalFact] + public async Task VectorDistance_with_constant() + { + using var ctx = CreateContext(); + + var results = await ctx.VectorEntities + .OrderBy(v => EF.Functions.VectorDistance("cosine", v.Vector, new SqlVector(new float[] { 1, 2, 100 }))) + .Take(1) + .ToListAsync(); + + Assert.Equal(2, results.Single().Id); + + AssertSql( + """ +@p='1' + +SELECT TOP(@p) [v].[Id], [v].[Vector] +FROM [VectorEntities] AS [v] +ORDER BY VECTOR_DISTANCE('cosine', [v].[Vector], CAST('[1,2,100]' AS VECTOR(3))) +"""); + } + + [ConditionalFact] + public async Task Length() + { + using var ctx = CreateContext(); + + var count = await ctx.VectorEntities + .Where(v => v.Vector.Length == 3) + .CountAsync(); + + using (Fixture.TestSqlLoggerFactory.SuspendRecordingEvents()) + { + Assert.Equal(await ctx.VectorEntities.CountAsync(), count); + } + + AssertSql( + """ +SELECT COUNT(*) +FROM [VectorEntities] AS [v] +WHERE VECTORPROPERTY([v].[Vector], 'Dimensions') = 3 +"""); + } + + protected VectorQueryContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + public class VectorQueryContext(DbContextOptions options) : PoolableDbContext(options) + { + public DbSet VectorEntities { get; set; } = null!; + + public static async Task SeedAsync(VectorQueryContext context) + { + var vectorEntities = new VectorEntity[] + { + new() { Id = 1, Vector = new(new float[] { 1, 2, 3 }) }, + new() { Id = 2, Vector = new(new float[] { 1, 2, 100 }) }, + new() { Id = 3, Vector = new(new float[] { 1, 2, 1000 }) } + }; + + context.VectorEntities.AddRange(vectorEntities); + await context.SaveChangesAsync(); + } + } + + public class VectorEntity + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + [Column(TypeName = "vector(3)")] + public SqlVector Vector { get; set; } = null!; + } + + public class VectorQueryFixture : SharedStoreFixtureBase + { + protected override string StoreName + => "VectorQueryTest"; + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override Task SeedAsync(VectorQueryContext context) + => VectorQueryContext.SeedAsync(context); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs index 0be424ecf12..499bc3faa52 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/SqlServerDatabaseModelFactoryTest.cs @@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; // ReSharper disable InconsistentNaming @@ -5726,6 +5727,32 @@ FOREIGN KEY (ForeignKeyId) REFERENCES PrincipalTable(Id) ON DELETE SET NULL, #endregion + #region Types + + [ConditionalFact] + [SqlServerCondition(SqlServerCondition.SupportsVectorType)] + public void Vector_type() + => Test( + "CREATE TABLE [dbo].[VectorTable] (vector VECTOR(3))", + tables: [], + schemas: [], + (dbModel, scaffoldingFactory) => + { + var table = Assert.Single(dbModel.Tables); + var column = Assert.Single(table.Columns); + Assert.Equal("vector", column.Name); + Assert.Equal("vector(3)", column.StoreType); + + var model = scaffoldingFactory.Create(dbModel, new ModelReverseEngineerOptions()); + var entityType = Assert.Single(model.GetEntityTypes()); + var property = Assert.Single(entityType.GetProperties()); + Assert.Equal("Vector", property.Name); + Assert.True(property.GetTypeMapping() is SqlServerVectorTypeMapping { Size: 3 }); + }, + "DROP TABLE [dbo].[VectorTable]"); + + #endregion + #region Warnings [ConditionalFact] diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs index 24b17c653ef..791720b9abf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerCondition.cs @@ -22,4 +22,5 @@ public enum SqlServerCondition SupportsFunctions2019 = 1 << 13, SupportsFunctions2022 = 1 << 14, SupportsJsonType = 1 << 15, + SupportsVectorType = 1 << 15, } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs index f2c4c635fb0..1c5ec540b8f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerConditionAttribute.cs @@ -97,6 +97,11 @@ public ValueTask IsMetAsync() isMet &= TestEnvironment.IsJsonTypeSupported; } + if (Conditions.HasFlag(SqlServerCondition.SupportsVectorType)) + { + isMet &= TestEnvironment.IsVectorTypeSupported; + } + return ValueTask.FromResult(isMet); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs index 34e7e76ab28..f31640344ab 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestEnvironment.cs @@ -43,6 +43,8 @@ public static class TestEnvironment private static bool? _supportsJsonPathExpressions; + private static bool? _isVectorTypeSupported; + private static bool? _supportsFunctions2017; private static bool? _supportsFunctions2019; @@ -402,6 +404,33 @@ public static bool IsFunctions2022Supported public static bool IsJsonTypeSupported => false; + public static bool IsVectorTypeSupported + { + get + { + if (!IsConfigured) + { + return false; + } + + if (_isVectorTypeSupported.HasValue) + { + return _isVectorTypeSupported.Value; + } + + try + { + _isVectorTypeSupported = GetProductMajorVersion() >= 17 || IsSqlAzure; + } + catch (PlatformNotSupportedException) + { + _isVectorTypeSupported = false; + } + + return _isVectorTypeSupported.Value; + } + } + public static byte SqlServerMajorVersion => GetProductMajorVersion(); diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 6660e3f221a..0fa763c3ad9 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Internal; @@ -965,6 +967,8 @@ public void DefaultValue_with_implicit_constraint_name_throws_for_TPC() modelBuilder); } + #region Temporal tables + [ConditionalFact] public void Temporal_can_only_be_specified_on_root_entities() { @@ -1158,6 +1162,57 @@ public void Temporal_table_with_owned_with_explicit_precision_on_period_columns_ Assert.Equal(2, ownedEntity.FindProperty("End").GetPrecision()); } + #endregion Temporal tables + + #region Vector + + [ConditionalFact] + public virtual void Throws_for_vector_property_without_dimensions() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(); + + VerifyError( + SqlServerStrings.VectorDimensionsMissing(nameof(VectorWithoutDimensionsEntity), nameof(VectorWithoutDimensionsEntity.Vector)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Throws_for_vector_property_inside_JSON() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity().OwnsOne(v => v.VectorContainer, n => + { + n.ToJson(); + n.Property(v => v.Vector).HasMaxLength(3); + }); + + VerifyError( + SqlServerStrings.VectorPropertiesNotSupportedInJson(nameof(VectorContainer), nameof(VectorContainer.Vector)), + modelBuilder); + } + + public class VectorWithoutDimensionsEntity + { + public int Id { get; set; } + public SqlVector Vector { get; set; } + } + + public class VectorInsideJsonEntity + { + public int Id { get; set; } + public VectorContainer VectorContainer { get; set; } + } + + public class VectorContainer + { + public SqlVector Vector { get; set; } + } + + #endregion Vector + public class Human { public int Id { get; set; } diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs index ce3ef9ee209..28427209481 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingSourceTest.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; // ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore; +namespace Microsoft.EntityFrameworkCore.Storage; public class SqlServerTypeMappingSourceTest : RelationalTypeMappingSourceTestBase { @@ -1329,6 +1331,7 @@ public void Throws_for_unrecognized_property_types() [InlineData("varchar(333)", typeof(string), 333, false, false)] [InlineData("varchar(max)", typeof(string), -1, false, false)] [InlineData("VARCHAR(max)", typeof(string), -1, false, false, "VARCHAR(max)")] + [InlineData("vector(3)", typeof(SqlVector), 3, false, false)] public void Can_map_by_store_type(string storeType, Type type, int? size, bool unicode, bool fixedLength, string expectedType = null) { var mapping = CreateRelationalTypeMappingSource(CreateModel()).FindMapping(storeType); @@ -1346,6 +1349,7 @@ public void Can_map_by_store_type(string storeType, Type type, int? size, bool u [InlineData(typeof(DateTime), "date")] [InlineData(typeof(TimeOnly), "time")] [InlineData(typeof(TimeSpan), "time")] + [InlineData(typeof(SqlVector), "vector(3)")] public void Can_map_by_clr_and_store_types(Type clrType, string storeType) { var mapping = CreateRelationalTypeMappingSource(CreateModel()).FindMapping(clrType, storeType); @@ -1762,6 +1766,37 @@ public void String_FK_unicode_is_preferred_if_specified() mapper.GetMapping(model.FindEntityType(typeof(MyRelatedType4)).FindProperty("Relationship2Id")).StoreType); } + #region Vector + + [ConditionalFact] + public void Vector_is_properly_mapped() + { + var typeMapping = GetTypeMapping(typeof(SqlVector), maxLength: 3); + + Assert.Null(typeMapping.DbType); + Assert.Equal("vector(3)", typeMapping.StoreType); + Assert.Equal(3, typeMapping.Size); + Assert.Null(typeMapping.Precision); + Assert.Null(typeMapping.Scale); + } + + [ConditionalFact] + public void Vector_requires_positive_dimensions() + { + var exception = Assert.Throws(() => GetTypeMapping(typeof(SqlVector), maxLength: 0)); + Assert.Equal(SqlServerStrings.VectorDimensionsInvalid, exception.Message); + + exception = Assert.Throws(() => GetTypeMapping(typeof(SqlVector), maxLength: -1)); + Assert.Equal(SqlServerStrings.VectorDimensionsInvalid, exception.Message); + + // We do allow constructing a vector type mapping with no dimensions, since the scaffolder requires it + // (see comment in SqlServerVectorTypeMapping) + var typeMapping = GetTypeMapping(typeof(SqlVector)); + Assert.Null(typeMapping.Size); + } + + #endregion Vector + [ConditionalFact] public void Plugins_can_override_builtin_mappings() { @@ -1769,7 +1804,7 @@ public void Plugins_can_override_builtin_mappings() TestServiceFactory.Instance.Create(), TestServiceFactory.Instance.Create() with { - Plugins = new[] { new FakeTypeMappingSourcePlugin() } + Plugins = [new FakeTypeMappingSourcePlugin()] }); Assert.Equal("String", typeMappingSource.GetMapping("datetime2").ClrType.Name); diff --git a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs index e7c0d89a840..e357123ecea 100644 --- a/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs +++ b/test/EFCore.SqlServer.Tests/Storage/SqlServerTypeMappingTest.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Data; -using System.Globalization; using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlTypes; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; @@ -408,6 +408,24 @@ public virtual void TimeOnly_code_literal_generated_correctly() "new TimeOnly(12, 30, 10, 500).Add(TimeSpan.FromTicks(10))"); } + #region Vector + + [ConditionalFact] + public virtual void Vector_comparer_compares_Memory() + { + var typeMapping = new SqlServerVectorTypeMapping(3); + + float[] array = [1, 2, 3]; + var vector1 = new SqlVector(array); + var vector2 = new SqlVector(array); + var vector3 = new SqlVector(new float[] { 1, 2, 3 }); + + Assert.True(typeMapping.Comparer.Equals(vector1, vector2)); + Assert.False(typeMapping.Comparer.Equals(vector1, vector3)); + } + + #endregion Vector + public static RelationalTypeMapping GetMapping(string type) => GetTypeMappingSource().FindMapping(type); @@ -430,146 +448,6 @@ protected virtual void Test_GenerateCodeLiteral_helper( Assert.Equal(expectedCode, csharpHelper.UnknownLiteral(value)); } - private class FakeType(string fullName) : Type - { - public override object[] GetCustomAttributes(bool inherit) - => throw new NotImplementedException(); - - public override bool IsDefined(Type attributeType, bool inherit) - => throw new NotImplementedException(); - - public override ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type GetInterface(string name, bool ignoreCase) - => throw new NotImplementedException(); - - public override Type[] GetInterfaces() - => throw new NotImplementedException(); - - public override EventInfo GetEvent(string name, BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override EventInfo[] GetEvents(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type[] GetNestedTypes(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type GetNestedType(string name, BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override Type GetElementType() - => throw new NotImplementedException(); - - protected override bool HasElementTypeImpl() - => throw new NotImplementedException(); - - protected override PropertyInfo GetPropertyImpl( - string name, - BindingFlags bindingAttr, - Binder binder, - Type returnType, - Type[] types, - ParameterModifier[] modifiers) - => throw new NotImplementedException(); - - public override PropertyInfo[] GetProperties(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - protected override MethodInfo GetMethodImpl( - string name, - BindingFlags bindingAttr, - Binder binder, - CallingConventions callConvention, - Type[] types, - ParameterModifier[] modifiers) - => throw new NotImplementedException(); - - public override MethodInfo[] GetMethods(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override FieldInfo GetField(string name, BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override FieldInfo[] GetFields(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - public override MemberInfo[] GetMembers(BindingFlags bindingAttr) - => throw new NotImplementedException(); - - protected override TypeAttributes GetAttributeFlagsImpl() - => throw new NotImplementedException(); - - protected override bool IsArrayImpl() - => throw new NotImplementedException(); - - protected override bool IsByRefImpl() - => throw new NotImplementedException(); - - protected override bool IsPointerImpl() - => throw new NotImplementedException(); - - protected override bool IsPrimitiveImpl() - => throw new NotImplementedException(); - - protected override bool IsCOMObjectImpl() - => throw new NotImplementedException(); - - public override object InvokeMember( - string name, - BindingFlags invokeAttr, - Binder binder, - object target, - object[] args, - ParameterModifier[] modifiers, - CultureInfo culture, - string[] namedParameters) - => throw new NotImplementedException(); - - public override Type UnderlyingSystemType { get; } - - protected override ConstructorInfo GetConstructorImpl( - BindingFlags bindingAttr, - Binder binder, - CallingConventions callConvention, - Type[] types, - ParameterModifier[] modifiers) - => throw new NotImplementedException(); - - public override string Name - => throw new NotImplementedException(); - - public override Guid GUID - => throw new NotImplementedException(); - - public override Module Module - => throw new NotImplementedException(); - - public override Assembly Assembly - => throw new NotImplementedException(); - - public override string Namespace - => throw new NotImplementedException(); - - public override string AssemblyQualifiedName - => throw new NotImplementedException(); - - public override Type BaseType - => throw new NotImplementedException(); - - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - => throw new NotImplementedException(); - - public override string FullName { get; } = fullName; - - public override int GetHashCode() - => FullName.GetHashCode(); - - public override bool Equals(object o) - => ReferenceEquals(this, o); - } - protected override DbContextOptions ContextOptions { get; } = new DbContextOptionsBuilder() .UseInternalServiceProvider(SqlServerFixture.DefaultServiceProvider)