From 8e5b097490612d7fb8c095e49913bdd1e6395038 Mon Sep 17 00:00:00 2001 From: Skyrunner-Dev-ops Date: Fri, 9 Oct 2026 22:59:03 +0530 Subject: [PATCH 1/2] Differentiate overloaded functions and stored procedures in TableMappingBaseComparer When table-valued functions or stored procedures share the same name and schema but differ in parameter count or types, TableMappingBaseComparer previously compared them only by Table.Name and Table.Schema, causing them to compare as equal. When stored in a SortedSet (such as FunctionColumnMappings on IProperty), mappings for overloaded functions were dropped as duplicates, leading to NullReferenceException when translating queries. Fixes #33542 --- .../Internal/TableMappingBaseComparer.cs | 114 ++++++++++++++++++ .../Metadata/RelationalModelTest.cs | 4 +- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs b/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs index a2e3851d7da..03f1f60b3c4 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs @@ -129,6 +129,103 @@ public int Compare(ITableMappingBase? x, ITableMappingBase? y) return result; } + if (x.Table is IStoreFunction xFunction) + { + if (y.Table is not IStoreFunction) + { + return 1; + } + + var yFunction = (IStoreFunction)y.Table; + if (xFunction.Parameters is IReadOnlyList xParamsList + && yFunction.Parameters is IReadOnlyList yParamsList) + { + result = xParamsList.Count.CompareTo(yParamsList.Count); + if (result != 0) + { + return result; + } + + for (var i = 0; i < xParamsList.Count; i++) + { + var xp = xParamsList[i]; + var yp = yParamsList[i]; + result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); + if (result != 0) + { + return result; + } + } + } + else + { + result = xFunction.Parameters.Count().CompareTo(yFunction.Parameters.Count()); + if (result != 0) + { + return result; + } + + result = xFunction.Parameters.Zip( + yFunction.Parameters, (xp, yp) => + { + var paramResult = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); + return paramResult != 0 ? paramResult : StringComparer.Ordinal.Compare(xp.Name, yp.Name); + }) + .FirstOrDefault(r => r != 0); + + if (result != 0) + { + return result; + } + } + } + else if (y.Table is IStoreFunction) + { + return -1; + } + + if (x.Table is IStoreStoredProcedure xSproc) + { + if (y.Table is not IStoreStoredProcedure) + { + return 1; + } + + var ySproc = (IStoreStoredProcedure)y.Table; + result = xSproc.Parameters.Count.CompareTo(ySproc.Parameters.Count); + if (result != 0) + { + return result; + } + + for (var i = 0; i < xSproc.Parameters.Count; i++) + { + var xp = xSproc.Parameters[i]; + var yp = ySproc.Parameters[i]; + result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); + if (result != 0) + { + return result; + } + } + } + else if (y.Table is IStoreStoredProcedure) + { + return -1; + } + result = x.ColumnMappings.Count().CompareTo(y.ColumnMappings.Count()); return result != 0 ? result @@ -168,6 +265,23 @@ public int GetHashCode(ITableMappingBase obj) hashCode.Add(obj.TypeBase, TypeBaseNameComparer.Instance); hashCode.Add(obj.Table.Name); hashCode.Add(obj.Table.Schema); + if (obj.Table is IStoreFunction function) + { + foreach (var parameter in function.Parameters) + { + hashCode.Add(parameter.StoreType); + hashCode.Add(parameter.Name); + } + } + else if (obj.Table is IStoreStoredProcedure sproc) + { + foreach (var parameter in sproc.Parameters) + { + hashCode.Add(parameter.StoreType); + hashCode.Add(parameter.Name); + } + } + foreach (var columnMapping in obj.ColumnMappings) { hashCode.Add(columnMapping.Property.Name); diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 6f497af875c..d01be36beb9 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -3140,7 +3140,7 @@ public void Can_use_relational_model_with_functions() Assert.Null(ordersFunction.ReturnType); var orderDate = orderType.FindProperty(nameof(Order.OrderDate))!; - Assert.Equal(2, orderDate.GetFunctionColumnMappings().Count()); + Assert.Equal(3, orderDate.GetFunctionColumnMappings().Count()); var orderDateMapping = orderMapping.ColumnMappings.Single(m => m.Property == orderDate); Assert.NotNull(orderDateMapping.TypeMapping); Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); @@ -3172,6 +3172,7 @@ public void Can_use_relational_model_with_functions() Assert.Same(model.Model.GetDbFunctions().First(f => f.Parameters.Count() == 1), tvfDbFunction); Assert.Same(tvfFunction.Parameters.Single(), tvfDbFunction.Parameters.Single().StoreFunctionParameter); Assert.Equal(tvfDbFunction.Parameters.Single().Name, tvfFunction.Parameters.Single().DbFunctionParameters.Single().Name); + Assert.NotNull(tvfFunction.FindColumn(orderDate)); var tvfFunction2 = tvfMapping2.StoreFunction; Assert.Same(tvfMapping2, tvfFunction2.EntityTypeMappings.Single()); @@ -3187,6 +3188,7 @@ public void Can_use_relational_model_with_functions() Assert.Same(model.Model.GetDbFunctions().Last(f => f.Parameters.Count() == 1), tvfDbFunction2); Assert.Same(tvfFunction2.Parameters.Single(), tvfDbFunction2.Parameters.Single().StoreFunctionParameter); Assert.Equal(tvfDbFunction2.Parameters.Single().Name, tvfFunction2.Parameters.Single().DbFunctionParameters.Single().Name); + Assert.NotNull(tvfFunction2.FindColumn(orderDate)); } [Fact] From 4564281d48c1fc82182259ea13da4eded80269a2 Mon Sep 17 00:00:00 2001 From: Skyrunner-Dev-ops Date: Wed, 14 Oct 2026 00:05:53 +0530 Subject: [PATCH 2/2] Derive from IComparable in ITableMappingBase and IColumnMappingBase --- .../EFCore.Relational.baseline.json | 26 +-- .../Metadata/IColumnMappingBase.cs | 8 +- .../Metadata/ITableMappingBase.cs | 8 +- .../Metadata/Internal/ColumnMappingBase.cs | 29 +++ .../Internal/ColumnMappingBaseComparer.cs | 34 +-- .../Metadata/Internal/FunctionMapping.cs | 67 ++++++ .../Internal/StoredProcedureMapping.cs | 41 ++++ .../Metadata/Internal/TableMappingBase.cs | 122 ++++++++++ .../Internal/TableMappingBaseComparer.cs | 209 +----------------- .../Metadata/RelationalModelTest.cs | 31 ++- 10 files changed, 317 insertions(+), 258 deletions(-) diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index 6e01c2538f5..82dbb2fb97e 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -3174,7 +3174,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -3190,7 +3190,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Properties": [ { "Member": "Microsoft.EntityFrameworkCore.Metadata.IColumnBase Column { get; }" @@ -4251,7 +4251,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IFunctionColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IFunctionColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -4267,7 +4267,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IFunctionMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IFunctionMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Properties": [ { "Member": "System.Collections.Generic.IEnumerable ColumnMappings { get; }" @@ -6305,7 +6305,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ISqlQueryColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ISqlQueryColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -6321,7 +6321,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ISqlQueryMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ISqlQueryMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Properties": [ { "Member": "System.Collections.Generic.IEnumerable ColumnMappings { get; }" @@ -6375,7 +6375,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IStoredProcedureMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IStoredProcedureMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Properties": [ { "Member": "System.Collections.Generic.IEnumerable ParameterMappings { get; }" @@ -6409,7 +6409,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IStoredProcedureParameterMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IStoredProcedureParameterMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -6439,7 +6439,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IStoredProcedureResultColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IStoredProcedureResultColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -6728,7 +6728,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ITableMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ITableMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Properties": [ { "Member": "System.Collections.Generic.IEnumerable ColumnMappings { get; }" @@ -6748,7 +6748,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase : Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -6897,7 +6897,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IViewColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IViewColumnMapping : Microsoft.EntityFrameworkCore.Metadata.IColumnMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Methods": [ { "Member": "string ToDebugString(Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions options = Microsoft.EntityFrameworkCore.Infrastructure.MetadataDebugStringOptions.ShortDefault, int indent = 0);" @@ -6913,7 +6913,7 @@ ] }, { - "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IViewMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable", + "Type": "interface Microsoft.EntityFrameworkCore.Metadata.IViewMapping : Microsoft.EntityFrameworkCore.Metadata.ITableMappingBase, Microsoft.EntityFrameworkCore.Infrastructure.IAnnotatable, Microsoft.EntityFrameworkCore.Infrastructure.IReadOnlyAnnotatable, System.IComparable", "Properties": [ { "Member": "System.Collections.Generic.IEnumerable ColumnMappings { get; }" diff --git a/src/EFCore.Relational/Metadata/IColumnMappingBase.cs b/src/EFCore.Relational/Metadata/IColumnMappingBase.cs index 2dfd5558bb9..307213d2db7 100644 --- a/src/EFCore.Relational/Metadata/IColumnMappingBase.cs +++ b/src/EFCore.Relational/Metadata/IColumnMappingBase.cs @@ -9,7 +9,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// /// See Modeling entity types and relationships for more information and examples. /// -public interface IColumnMappingBase : IAnnotatable +public interface IColumnMappingBase : IAnnotatable, IComparable { /// /// Gets the mapped property. @@ -30,4 +30,10 @@ public interface IColumnMappingBase : IAnnotatable /// Gets the containing table mapping. /// ITableMappingBase TableMapping { get; } + + /// + /// Compares the current instance with another object of the same type. + /// + int IComparable.CompareTo(IColumnMappingBase? other) + => other is null ? 1 : 0; } diff --git a/src/EFCore.Relational/Metadata/ITableMappingBase.cs b/src/EFCore.Relational/Metadata/ITableMappingBase.cs index 1b4e50ca8b0..2f882d63cc8 100644 --- a/src/EFCore.Relational/Metadata/ITableMappingBase.cs +++ b/src/EFCore.Relational/Metadata/ITableMappingBase.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; /// /// See Modeling entity types and relationships for more information and examples. /// -public interface ITableMappingBase : IAnnotatable +public interface ITableMappingBase : IAnnotatable, IComparable { /// /// Gets the mapped type base. @@ -135,4 +135,10 @@ string ToDebugString(MetadataDebugStringOptions options = MetadataDebugStringOpt return builder.ToString(); } + + /// + /// Compares the current instance with another object of the same type. + /// + int IComparable.CompareTo(ITableMappingBase? other) + => other is null ? 1 : 0; } diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs index a74933e1eba..d8b9c825f90 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBase.cs @@ -74,4 +74,33 @@ ITableMappingBase IColumnMappingBase.TableMapping [DebuggerStepThrough] get => TableMapping; } + + /// + public virtual int CompareTo(IColumnMappingBase? other) + { + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (other is null) + { + return 1; + } + + var result = other.Property.IsPrimaryKey().CompareTo(Property.IsPrimaryKey()); + if (result != 0) + { + return result; + } + + result = TableMapping.CompareTo(other.TableMapping); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(Property.Name, other.Property.Name); + return result != 0 ? result : StringComparer.Ordinal.Compare(Column.Name, other.Column.Name); + } } diff --git a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs index 57646e13623..8c76aeb1f7e 100644 --- a/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/ColumnMappingBaseComparer.cs @@ -30,37 +30,9 @@ private ColumnMappingBaseComparer() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public int Compare(IColumnMappingBase? x, IColumnMappingBase? y) - { - if (ReferenceEquals(x, y)) - { - return 0; - } - - if (x is null) - { - return -1; - } - - if (y is null) - { - return 1; - } - - var result = y.Property.IsPrimaryKey().CompareTo(x.Property.IsPrimaryKey()); - if (result != 0) - { - return result; - } - - result = TableMappingBaseComparer.Instance.Compare(x.TableMapping, y.TableMapping); - if (result != 0) - { - return result; - } - - result = StringComparer.Ordinal.Compare(x.Property.Name, y.Property.Name); - return result != 0 ? result : StringComparer.Ordinal.Compare(x.Column.Name, y.Column.Name); - } + => x is null + ? y is null ? 0 : -1 + : y is null ? 1 : x.CompareTo(y); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs b/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs index d6282e8f9c5..e96b4c77404 100644 --- a/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/FunctionMapping.cs @@ -46,4 +46,71 @@ IEnumerable IFunctionMapping.ColumnMappings [DebuggerStepThrough] get => ColumnMappings; } + + /// + protected override int CompareTableDetails(ITableMappingBase other) + { + if (other is not IFunctionMapping otherFunctionMapping) + { + return 1; + } + + if (Table is StoreFunction storeFunction + && otherFunctionMapping.Table is StoreFunction otherStoreFunction) + { + var xParams = storeFunction.Parameters; + var yParams = otherStoreFunction.Parameters; + var result = xParams.Length.CompareTo(yParams.Length); + if (result != 0) + { + return result; + } + + for (var i = 0; i < xParams.Length; i++) + { + var xp = xParams[i]; + var yp = yParams[i]; + result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); + if (result != 0) + { + return result; + } + } + + return 0; + } + + var xList = StoreFunction.Parameters.ToList(); + var yList = otherFunctionMapping.StoreFunction.Parameters.ToList(); + var countResult = xList.Count.CompareTo(yList.Count); + if (countResult != 0) + { + return countResult; + } + + for (var i = 0; i < xList.Count; i++) + { + var xp = xList[i]; + var yp = yList[i]; + var result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); + if (result != 0) + { + return result; + } + } + + return 0; + } } diff --git a/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs b/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs index 4224b4ab73a..b1eec11f497 100644 --- a/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs +++ b/src/EFCore.Relational/Metadata/Internal/StoredProcedureMapping.cs @@ -104,4 +104,45 @@ IEnumerable IStoredProcedureMapping.ParameterM [DebuggerStepThrough] get => ParameterMappings; } + + /// + protected override int CompareTableDetails(ITableMappingBase other) + { + if (other is IFunctionMapping) + { + return -1; + } + + if (other is not IStoredProcedureMapping otherSprocMapping) + { + return 1; + } + + var xSproc = StoreStoredProcedure; + var ySproc = otherSprocMapping.StoreStoredProcedure; + var result = xSproc.Parameters.Count.CompareTo(ySproc.Parameters.Count); + if (result != 0) + { + return result; + } + + for (var i = 0; i < xSproc.Parameters.Count; i++) + { + var xp = xSproc.Parameters[i]; + var yp = ySproc.Parameters[i]; + result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); + if (result != 0) + { + return result; + } + } + + return 0; + } } diff --git a/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs b/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs index 68c7e859838..6e60b9b5423 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMappingBase.cs @@ -97,6 +97,128 @@ public virtual bool AddColumnMapping(TColumnMapping columnMapping) public virtual void SetIsSharedTablePrincipal(bool isSharedTablePrincipal) => throw new NotImplementedException(); + /// + public virtual int CompareTo(ITableMappingBase? other) + { + if (ReferenceEquals(this, other)) + { + return 0; + } + + if (other is null) + { + return 1; + } + + var result = 0; + if (other.IsSharedTablePrincipal == null) + { + if (IsSharedTablePrincipal != null) + { + return 1; + } + } + else + { + if (IsSharedTablePrincipal == null) + { + return -1; + } + + result = other.IsSharedTablePrincipal.Value.CompareTo(IsSharedTablePrincipal.Value); + if (result != 0) + { + return result; + } + } + + if (other.IncludesDerivedTypes == null) + { + if (IncludesDerivedTypes != null) + { + return -1; + } + } + else + { + if (IncludesDerivedTypes == null) + { + return 1; + } + + result = other.IncludesDerivedTypes.Value.CompareTo(IncludesDerivedTypes.Value); + if (result != 0) + { + return result; + } + } + + if (other.IsSplitEntityTypePrincipal == null) + { + if (IsSplitEntityTypePrincipal != null) + { + return -1; + } + } + else + { + if (IsSplitEntityTypePrincipal == null) + { + return 1; + } + + result = other.IsSplitEntityTypePrincipal.Value.CompareTo(IsSplitEntityTypePrincipal.Value); + if (result != 0) + { + return result; + } + } + + result = TypeBaseNameComparer.Instance.Compare(TypeBase, other.TypeBase); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(Table.Name, other.Table.Name); + if (result != 0) + { + return result; + } + + result = StringComparer.Ordinal.Compare(Table.Schema, other.Table.Schema); + if (result != 0) + { + return result; + } + + result = CompareTableDetails(other); + if (result != 0) + { + return result; + } + + result = ColumnMappings.Count.CompareTo(other.ColumnMappings.Count()); + return result != 0 + ? result + : ColumnMappings.Zip( + other.ColumnMappings, (xc, yc) => + { + var columnResult = StringComparer.Ordinal.Compare(xc.Property.Name, yc.Property.Name); + return columnResult != 0 ? columnResult : StringComparer.Ordinal.Compare(xc.Column.Name, yc.Column.Name); + }) + .FirstOrDefault(r => r != 0); + } + + /// + /// 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 int CompareTableDetails(ITableMappingBase other) + => other is IFunctionMapping or IStoredProcedureMapping ? -1 : 0; + /// /// 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.Relational/Metadata/Internal/TableMappingBaseComparer.cs b/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs index 03f1f60b3c4..99c33fba10c 100644 --- a/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs +++ b/src/EFCore.Relational/Metadata/Internal/TableMappingBaseComparer.cs @@ -31,212 +31,9 @@ private TableMappingBaseComparer() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public int Compare(ITableMappingBase? x, ITableMappingBase? y) - { - if (ReferenceEquals(x, y)) - { - return 0; - } - - if (x is null) - { - return -1; - } - - if (y is null) - { - return 1; - } - - var result = 0; - if (y.IsSharedTablePrincipal == null) - { - if (x.IsSharedTablePrincipal != null) - { - return 1; - } - } - else - { - if (x.IsSharedTablePrincipal == null) - { - return -1; - } - - result = y.IsSharedTablePrincipal.Value.CompareTo(x.IsSharedTablePrincipal.Value); - if (result != 0) - { - return result; - } - } - - if (y.IncludesDerivedTypes == null) - { - if (x.IncludesDerivedTypes != null) - { - return -1; - } - } - else - { - if (x.IncludesDerivedTypes == null) - { - return 1; - } - - result = y.IncludesDerivedTypes.Value.CompareTo(x.IncludesDerivedTypes.Value); - if (result != 0) - { - return result; - } - } - - if (y.IsSplitEntityTypePrincipal == null) - { - if (x.IsSplitEntityTypePrincipal != null) - { - return -1; - } - } - else - { - if (x.IsSplitEntityTypePrincipal == null) - { - return 1; - } - - result = y.IsSplitEntityTypePrincipal.Value.CompareTo(x.IsSplitEntityTypePrincipal.Value); - if (result != 0) - { - return result; - } - } - - result = TypeBaseNameComparer.Instance.Compare(x.TypeBase, y.TypeBase); - if (result != 0) - { - return result; - } - - result = StringComparer.Ordinal.Compare(x.Table.Name, y.Table.Name); - if (result != 0) - { - return result; - } - - result = StringComparer.Ordinal.Compare(x.Table.Schema, y.Table.Schema); - if (result != 0) - { - return result; - } - - if (x.Table is IStoreFunction xFunction) - { - if (y.Table is not IStoreFunction) - { - return 1; - } - - var yFunction = (IStoreFunction)y.Table; - if (xFunction.Parameters is IReadOnlyList xParamsList - && yFunction.Parameters is IReadOnlyList yParamsList) - { - result = xParamsList.Count.CompareTo(yParamsList.Count); - if (result != 0) - { - return result; - } - - for (var i = 0; i < xParamsList.Count; i++) - { - var xp = xParamsList[i]; - var yp = yParamsList[i]; - result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); - if (result != 0) - { - return result; - } - - result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); - if (result != 0) - { - return result; - } - } - } - else - { - result = xFunction.Parameters.Count().CompareTo(yFunction.Parameters.Count()); - if (result != 0) - { - return result; - } - - result = xFunction.Parameters.Zip( - yFunction.Parameters, (xp, yp) => - { - var paramResult = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); - return paramResult != 0 ? paramResult : StringComparer.Ordinal.Compare(xp.Name, yp.Name); - }) - .FirstOrDefault(r => r != 0); - - if (result != 0) - { - return result; - } - } - } - else if (y.Table is IStoreFunction) - { - return -1; - } - - if (x.Table is IStoreStoredProcedure xSproc) - { - if (y.Table is not IStoreStoredProcedure) - { - return 1; - } - - var ySproc = (IStoreStoredProcedure)y.Table; - result = xSproc.Parameters.Count.CompareTo(ySproc.Parameters.Count); - if (result != 0) - { - return result; - } - - for (var i = 0; i < xSproc.Parameters.Count; i++) - { - var xp = xSproc.Parameters[i]; - var yp = ySproc.Parameters[i]; - result = StringComparer.Ordinal.Compare(xp.StoreType, yp.StoreType); - if (result != 0) - { - return result; - } - - result = StringComparer.Ordinal.Compare(xp.Name, yp.Name); - if (result != 0) - { - return result; - } - } - } - else if (y.Table is IStoreStoredProcedure) - { - return -1; - } - - result = x.ColumnMappings.Count().CompareTo(y.ColumnMappings.Count()); - return result != 0 - ? result - : x.ColumnMappings.Zip( - y.ColumnMappings, (xc, yc) => - { - var columnResult = StringComparer.Ordinal.Compare(xc.Property.Name, yc.Property.Name); - return columnResult != 0 ? columnResult : StringComparer.Ordinal.Compare(xc.Column.Name, yc.Column.Name); - }) - .FirstOrDefault(r => r != 0); - } + => x is null + ? y is null ? 0 : -1 + : y is null ? 1 : x.CompareTo(y); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index d01be36beb9..0e7b7c9d604 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -2978,6 +2978,9 @@ private static IQueryable GetOrdersForCustomer(int id) private static IQueryable GetOrdersForCustomer(string name) => throw new NotImplementedException(); + private static IQueryable GetOrdersForCustomer(int id, string name) + => throw new NotImplementedException(); + [Fact] public void Complex_property_container_column_type_is_used_in_relational_model() { @@ -3094,32 +3097,41 @@ public void Can_use_relational_model_with_functions() typeof(RelationalModelTest).GetMethod( nameof(GetOrdersForCustomer), BindingFlags.NonPublic | BindingFlags.Static, [typeof(string)])!); + modelBuilder.HasDbFunction( + typeof(RelationalModelTest).GetMethod( + nameof(GetOrdersForCustomer), BindingFlags.NonPublic | BindingFlags.Static, [typeof(int), typeof(string)])!); + var model = Finalize(modelBuilder); Assert.Single(model.Model.GetEntityTypes()); - Assert.Equal(3, model.Functions.Count()); + Assert.Equal(4, model.Functions.Count()); Assert.Empty(model.Views); Assert.Empty(model.Tables); var orderType = model.Model.FindEntityType(typeof(Order))!; Assert.Null(orderType.FindPrimaryKey()); - Assert.Equal(3, orderType.GetFunctionMappings().Count()); + Assert.Equal(4, orderType.GetFunctionMappings().Count()); var orderMapping = orderType.GetFunctionMappings().First(); Assert.Null(orderMapping.IsSharedTablePrincipal); Assert.Null(orderMapping.IsSplitEntityTypePrincipal); Assert.True(orderMapping.IsDefaultFunctionMapping); - var tvfMapping = orderType.GetFunctionMappings().ElementAt(1); + var tvfMapping = orderType.GetFunctionMappings().ElementAt(2); Assert.Null(tvfMapping.IsSharedTablePrincipal); Assert.Null(tvfMapping.IsSplitEntityTypePrincipal); Assert.False(tvfMapping.IsDefaultFunctionMapping); - var tvfMapping2 = orderType.GetFunctionMappings().Last(); + var tvfMapping2 = orderType.GetFunctionMappings().ElementAt(3); Assert.Null(tvfMapping2.IsSharedTablePrincipal); Assert.Null(tvfMapping2.IsSplitEntityTypePrincipal); Assert.False(tvfMapping2.IsDefaultFunctionMapping); + var tvfMapping3 = orderType.GetFunctionMappings().ElementAt(1); + Assert.Null(tvfMapping3.IsSharedTablePrincipal); + Assert.Null(tvfMapping3.IsSplitEntityTypePrincipal); + Assert.False(tvfMapping3.IsDefaultFunctionMapping); + Assert.Null(orderMapping.IncludesDerivedTypes); Assert.Equal( [nameof(Order.AlternateId), nameof(Order.CustomerId), nameof(Order.Id), nameof(Order.OrderDate)], @@ -3140,7 +3152,7 @@ public void Can_use_relational_model_with_functions() Assert.Null(ordersFunction.ReturnType); var orderDate = orderType.FindProperty(nameof(Order.OrderDate))!; - Assert.Equal(3, orderDate.GetFunctionColumnMappings().Count()); + Assert.Equal(4, orderDate.GetFunctionColumnMappings().Count()); var orderDateMapping = orderMapping.ColumnMappings.Single(m => m.Property == orderDate); Assert.NotNull(orderDateMapping.TypeMapping); Assert.Equal("default_datetime_mapping", orderDateMapping.TypeMapping.StoreType); @@ -3185,10 +3197,17 @@ public void Can_use_relational_model_with_functions() var tvfDbFunction2 = tvfFunction2.DbFunctions.Single(); Assert.Same(tvfFunction2, tvfDbFunction2.StoreFunction); - Assert.Same(model.Model.GetDbFunctions().Last(f => f.Parameters.Count() == 1), tvfDbFunction2); + Assert.Same(model.Model.GetDbFunctions().First(f => f.Parameters.Count() == 1 && f.Parameters.Single().ClrType == typeof(string)), tvfDbFunction2); Assert.Same(tvfFunction2.Parameters.Single(), tvfDbFunction2.Parameters.Single().StoreFunctionParameter); Assert.Equal(tvfDbFunction2.Parameters.Single().Name, tvfFunction2.Parameters.Single().DbFunctionParameters.Single().Name); Assert.NotNull(tvfFunction2.FindColumn(orderDate)); + + var tvfFunction3 = tvfMapping3.StoreFunction; + Assert.Same(tvfMapping3, tvfFunction3.EntityTypeMappings.Single()); + Assert.Same(tvfFunction3, model.FindFunction(tvfFunction3.Name, tvfFunction3.Schema, ["default_int_mapping", "just_string(max)"])); + Assert.Equal(nameof(GetOrdersForCustomer), tvfFunction3.Name); + Assert.Equal(2, tvfFunction3.Parameters.Count()); + Assert.NotNull(tvfFunction3.FindColumn(orderDate)); } [Fact]