diff --git a/.ci/unit-tests/BHoM_Engine_Tests.sln b/.ci/unit-tests/BHoM_Engine_Tests.sln index a9d6cce4a5..91dec0f2e0 100644 --- a/.ci/unit-tests/BHoM_Engine_Tests.sln +++ b/.ci/unit-tests/BHoM_Engine_Tests.sln @@ -15,9 +15,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Versioning_Engine", "..\..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Base_Engine_Tests", "Base_Engine_Tests\Base_Engine_Tests.csproj", "{CC1F950E-D22F-443E-BB63-D6C0B013E3E7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serialiser_Engine_Tests", "Serialiser_Engine_Tests\Serialiser_Engine_Tests.csproj", "{9D02A8E2-D414-4633-9487-75A779E483A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serialiser_Engine_Tests", "Serialiser_Engine_Tests\Serialiser_Engine_Tests.csproj", "{9D02A8E2-D414-4633-9487-75A779E483A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Structure_Engine_Tests", "Structure_Engine_Tests\Structure_Engine_Tests.csproj", "{FCC23B54-9A9E-4DA0-AB91-B4D5A739A9B6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Structure_Engine_Tests", "Structure_Engine_Tests\Structure_Engine_Tests.csproj", "{FCC23B54-9A9E-4DA0-AB91-B4D5A739A9B6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestHelper_Engine", "TestHelper_Engine\TestHelper_Engine.csproj", "{04990536-063C-446B-8043-E3850617CF0A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -68,6 +70,12 @@ Global {FCC23B54-9A9E-4DA0-AB91-B4D5A739A9B6}.Release|Any CPU.Build.0 = Release|Any CPU {FCC23B54-9A9E-4DA0-AB91-B4D5A739A9B6}.Test|Any CPU.ActiveCfg = Debug|Any CPU {FCC23B54-9A9E-4DA0-AB91-B4D5A739A9B6}.Test|Any CPU.Build.0 = Debug|Any CPU + {04990536-063C-446B-8043-E3850617CF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04990536-063C-446B-8043-E3850617CF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04990536-063C-446B-8043-E3850617CF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04990536-063C-446B-8043-E3850617CF0A}.Release|Any CPU.Build.0 = Release|Any CPU + {04990536-063C-446B-8043-E3850617CF0A}.Test|Any CPU.ActiveCfg = Debug|Any CPU + {04990536-063C-446B-8043-E3850617CF0A}.Test|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/.ci/unit-tests/Base_Engine_Tests/Base_Engine_Tests.csproj b/.ci/unit-tests/Base_Engine_Tests/Base_Engine_Tests.csproj index 8bbf9f1e19..21b3bdd940 100644 --- a/.ci/unit-tests/Base_Engine_Tests/Base_Engine_Tests.csproj +++ b/.ci/unit-tests/Base_Engine_Tests/Base_Engine_Tests.csproj @@ -1,4 +1,4 @@ - + 8.0.0.0 @@ -22,7 +22,7 @@ - + @@ -30,6 +30,7 @@ + diff --git a/.ci/unit-tests/Base_Engine_Tests/Compute/TryRunExtensionMethod.cs b/.ci/unit-tests/Base_Engine_Tests/Compute/TryRunExtensionMethod.cs new file mode 100644 index 0000000000..9275ba6735 --- /dev/null +++ b/.ci/unit-tests/Base_Engine_Tests/Compute/TryRunExtensionMethod.cs @@ -0,0 +1,157 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2024, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.oM.Structure.Elements; +using NUnit.Framework; + +namespace BH.Tests.Engine.Base.Compute +{ + public class TryRunExtensionMethodTests + { + [Test] + public void BarDoubleDoubleDouble() + { + Bar bar = new Bar(); + double dbl = 123; + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Structure.Elements.Bar, System.Double, System.Double, System.Double)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, dbl, dbl, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarBarDoubleDouble() + { + Bar bar = new Bar(); + double dbl = 123; + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Structure.Elements.Bar, BH.oM.Structure.Elements.Bar, System.Double, System.Double)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, bar, dbl, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarBarBarDouble() + { + Bar bar = new Bar(); + double dbl = 123; + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Structure.Elements.Bar, System.Object, System.Object, System.Object)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, bar, bar, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarBarBarBar() + { + Bar bar = new Bar(); + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Structure.Elements.Bar, System.Object, System.Object, System.Object)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, bar, bar, bar); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarBarPanelDouble() + { + Bar bar = new Bar(); + Panel panel = new Panel(); + double dbl = 123; + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Structure.Elements.Bar, System.Object, BH.oM.Structure.Elements.Panel, System.Object)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, bar, panel, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void PanelDoubleDoubleDouble() + { + Panel panel = new Panel(); + double dbl = 123; + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Dimensional.IElement2D, System.Double, System.Double, System.Double)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(panel, dbl, dbl, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarNullPanelNull() + { + Bar bar = new Bar(); + Panel panel = new Panel(); + string expected = "BH.Engine.TestHelper.Compute.ExtensionMethodToCallHelper(BH.oM.Structure.Elements.Bar, System.Object, BH.oM.Structure.Elements.Panel, System.Object)"; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, null, panel, null); + Assert.AreEqual(result, expected); + } + + [Test] + public void PanelPanelPanelDouble() + { + Panel panel = new Panel(); + double dbl = 123; + string expected = null; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(panel, panel, panel, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarDoubleNullNull() + { + Bar bar = new Bar(); + double dbl = 123; + string expected = null; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, dbl, null, null); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarNullNullBar() + { + Bar bar = new Bar(); + string expected = null; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, null, null, bar); + Assert.AreEqual(result, expected); + } + + [Test] + public void BarNullNullNull() + { + Bar bar = new Bar(); + string expected = null; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(bar, null, null, null); + Assert.AreEqual(result, expected); + } + + [Test] + public void NullDoubleDoubleDouble() + { + Bar bar = new Bar(); + double dbl = 123; + string expected = null; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(null, dbl, dbl, dbl); + Assert.AreEqual(result, expected); + } + + [Test] + public void NullNullNullNull() + { + Bar bar = new Bar(); + string expected = null; + string result = BH.Engine.TestHelper.Compute.IExtensionMethodToCallHelper(null, null, null, null); + Assert.AreEqual(result, expected); + } + } +} diff --git a/.ci/unit-tests/Serialiser_Engine_Tests/Serialiser_Engine_Tests.csproj b/.ci/unit-tests/Serialiser_Engine_Tests/Serialiser_Engine_Tests.csproj index 8f87320aa4..291204a709 100644 --- a/.ci/unit-tests/Serialiser_Engine_Tests/Serialiser_Engine_Tests.csproj +++ b/.ci/unit-tests/Serialiser_Engine_Tests/Serialiser_Engine_Tests.csproj @@ -23,7 +23,7 @@ - + diff --git a/.ci/unit-tests/Structure_Engine_Tests/Structure_Engine_Tests.csproj b/.ci/unit-tests/Structure_Engine_Tests/Structure_Engine_Tests.csproj index a81d5e9b24..2742863665 100644 --- a/.ci/unit-tests/Structure_Engine_Tests/Structure_Engine_Tests.csproj +++ b/.ci/unit-tests/Structure_Engine_Tests/Structure_Engine_Tests.csproj @@ -1,4 +1,4 @@ - + 8.0.0.0 @@ -22,7 +22,7 @@ - + diff --git a/.ci/unit-tests/TestHelper_Engine/Compute/ExtensionMethodToCallHelper.cs b/.ci/unit-tests/TestHelper_Engine/Compute/ExtensionMethodToCallHelper.cs new file mode 100644 index 0000000000..64627916f2 --- /dev/null +++ b/.ci/unit-tests/TestHelper_Engine/Compute/ExtensionMethodToCallHelper.cs @@ -0,0 +1,79 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2024, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Versioning; +using BH.oM.Dimensional; +using BH.oM.Structure.Elements; +using System.Reflection; + +namespace BH.Engine.TestHelper +{ + public static partial class Compute + { + public static string ExtensionMethodToCallHelper(this Bar a, double b, double c, double d) + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string ExtensionMethodToCallHelper(this Bar a, Bar b, double c, double d) + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string ExtensionMethodToCallHelper(this Bar a, object b, double c, double d) + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string ExtensionMethodToCallHelper(this Bar a, object b, object c, object d) + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string ExtensionMethodToCallHelper(this Bar a, object b, Panel c, object d) + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string ExtensionMethodToCallHelper(this IElement2D a, double b, double c, double d) + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string ExtensionMethodToCallHelper() + { + return MethodBase.GetCurrentMethod().VersioningKey(); + } + + public static string IExtensionMethodToCallHelper(this object a, object b, object c, object d) + { + object result; + if (!BH.Engine.Base.Compute.TryRunExtensionMethod(a, nameof(ExtensionMethodToCallHelper), new object[] { b, c, d }, out result)) + { + BH.Engine.Base.Compute.RecordError("Extension method not found."); + return null; + } + else + return (string)result; + } + } +} diff --git a/.ci/unit-tests/TestHelper_Engine/TestHelper_Engine.csproj b/.ci/unit-tests/TestHelper_Engine/TestHelper_Engine.csproj new file mode 100644 index 0000000000..bd4d4c7c61 --- /dev/null +++ b/.ci/unit-tests/TestHelper_Engine/TestHelper_Engine.csproj @@ -0,0 +1,72 @@ + + + netstandard2.0 + 8.0.0.0 + https://github.com/BHoM/BHoM_Engine + 5.0.0 + BHoM + Copyright © https://github.com/BHoM + BH.Engine.TestHelper + 8.0.0.0 + + + ..\Build\ + + + + + + + + + $(ProgramData)\BHoM\Assemblies\Analytical_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\BHoM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Data_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Dimensional_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Geometry_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Matter_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Quantities_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Spatial_oM.dll + false + false + + + $(ProgramData)\BHoM\Assemblies\Structure_oM.dll + false + false + + + + + + + + diff --git a/BHoM_Engine/Query/ExtensionMethodHierarchy.cs b/BHoM_Engine/Query/ExtensionMethodHierarchy.cs index c45200b86c..10ad876472 100644 --- a/BHoM_Engine/Query/ExtensionMethodHierarchy.cs +++ b/BHoM_Engine/Query/ExtensionMethodHierarchy.cs @@ -35,38 +35,61 @@ public static partial class Query /**** Public Methods ****/ /***************************************************/ - [Description("Builds a hierarchy of methods based on distance of each method's first parameter to the provided type. " + - "First level of hierarchy will contain methods that extend types that are closest in inheritance hierarchy to the input type.")] - [Input("methods", "The list of extentionmethods to sort. Will assume the first inputparameter of the methods to be of a type assignable from the provided Type")] - [Input("type", "Type to check closeness to. Assumed to match first input parameter of the methods")] - [Output("hierarchy", "Hierarchy of input methods based on distance of each method's first parameter to the input type.")] - public static List> ExtensionMethodHierarchy(this IEnumerable methods, Type type) + [PreviousVersion("8.0", "BH.Engine.Base.Query.ExtensionMethodHierarchy(System.Collections.Generic.IEnumerable, System.Type)")] + [Description("Builds a hierarchy of methods based on distance of each method's input parameter to the correspondent input type, first to first, second to second etc." + + "\nThree levels of hierarchy are returned:" + + "\n- first level of hierarchy groups the output by order of inputs, i.e. first sublist corresponds to first input type and first method parameter, second to second etc." + + "\n- second level of hierarchy groups methods by the distance in inheritance hierarchy between their input parameter and the correspondent input type" + + "\n- third level of hierarchy is an unordered set of methods with same distance in inheritance hierarchy between their input parameter and the correspondent input type.")] + [Input("methods", "The list of extension methods to sort. Will assume the input parameters of the methods to be of a type assignable from the provided types.")] + [Input("types", "Types to check closeness to. First provided type is assumed to match first input parameter of the methods, others to follow respectively.")] + [Output("hierarchy", "Hierarchy of input methods based on distance of each method's parameters to the input types.")] + public static List>> ExtensionMethodHierarchy(this IEnumerable methods, IEnumerable types) { - if (methods == null || type == null) + if (methods == null || types == null || !types.Any()) return null; if (methods.Count() == 0) - return new List>(); + return new List>>(); - // Build inheritance hierarchy for the input type - List> inheritanceHierarchy = type.InheritanceHierarchy(); - - // Organise methods into hierarchy based on hierarchy of types they extend - Dictionary> methodHierarchy = new Dictionary>(); - foreach (MethodInfo method in methods) + // Build inheritance hierarchy for the input types + // Each item of the top list represents hierarchy for each input type + List>> result = new List>>(); + int i = 0; + foreach(Type type in types) { - int hierarchyLevel = inheritanceHierarchy.InheritanceLevel(method.GetParameters()[0].ParameterType); - if (hierarchyLevel != -1) + if (type == null) + { + // If null input, all methods taking nullable types would be as suitable + List applicableMethods = methods.Where(x => x.GetParameters()[i].ParameterType.IsNullable()).ToList(); + result.Add(new List> { applicableMethods }); + } + else { - if (!methodHierarchy.ContainsKey(hierarchyLevel)) - methodHierarchy[hierarchyLevel] = new List(); + // Build inheritance hierarchy for the input type + List> inheritanceHierarchy = type.InheritanceHierarchy(); + + // Organise methods into hierarchy based on hierarchy of types they extend + Dictionary> methodHierarchy = new Dictionary>(); + foreach (MethodInfo method in methods) + { + int hierarchyLevel = inheritanceHierarchy.InheritanceLevel(method.GetParameters()[i].ParameterType); + if (hierarchyLevel != -1) + { + if (!methodHierarchy.ContainsKey(hierarchyLevel)) + methodHierarchy[hierarchyLevel] = new List(); + + methodHierarchy[hierarchyLevel].Add(method); + } + } - methodHierarchy[hierarchyLevel].Add(method); + result.Add(methodHierarchy.OrderBy(x => x.Key).Select(x => x.Value).ToList()); } + + i++; } - // Return the hierarchy - return methodHierarchy.OrderBy(x => x.Key).Select(x => x.Value).ToList(); + return result; } diff --git a/BHoM_Engine/Query/ExtensionMethodToCall.cs b/BHoM_Engine/Query/ExtensionMethodToCall.cs index 54032faff5..491271f5f0 100644 --- a/BHoM_Engine/Query/ExtensionMethodToCall.cs +++ b/BHoM_Engine/Query/ExtensionMethodToCall.cs @@ -82,36 +82,43 @@ public static MethodInfo ExtensionMethodToCall(string methodName, object[] param return null; } - // Get type of first argument, to be used for first method extraction filtering - Type type = parameters[0].GetType(); + // Get types of input arguments, to be used for first method extraction filtering + Type[] types = parameters.Select(x => x?.GetType()).ToArray(); // Construct key used to store/extract method string name = methodName + parameters.Select(x => x?.GetType()?.ToString() ?? "null").Aggregate((a, b) => a + b); - Tuple key = new Tuple(type, name); + Tuple key = new Tuple(types[0], name); // If the method has been called before, just use that if (MethodPreviouslyExtracted(key)) return GetStoredExtensionMethod(key); - // Loop through all methods with matching name, first argument and number of parameters, sorted by best match to the first argument - var applicableMethods = type.ExtensionMethods(methodName).Where(x => x.IsApplicable(parameters)).ExtensionMethodHierarchy(type); + // Loop through all methods with matching name, pick applicable ones, then sort by best match to the provided inputs + var applicableMethods = types[0].ExtensionMethods(methodName).Where(x => x.IsApplicable(parameters)).ExtensionMethodHierarchy(types); // Return null if no applicable methods - if (applicableMethods.Count == 0) + if (applicableMethods.Count == 0 || applicableMethods[0].Count == 0) { StoreExtensionMethod(key, null); return null; } - // If more than one method at the top level of applicable method hierarchy, there is a risk of ambiguous call - if (applicableMethods[0].Count != 1) + // Iterate over each method argument's suitability hierarchy and find common denominator of top levels + HashSet candidates = new HashSet(applicableMethods[0][0]); + foreach (List mostApplicable in applicableMethods.Select(x => x[0])) { - StoreExtensionMethod(key, null); - return null; + candidates.IntersectWith(mostApplicable); + + // If no candidates left, there is ambiguity that cannot be solved, stop iterating + if (candidates.Count == 0) + break; } - // Take first applicable method, if method is generic, make sure the appropriate generic arguments are set - MethodInfo methodToCall = applicableMethods[0][0].MakeGenericFromInputs(parameters.Select(x => x?.GetType()).ToList()); + // If only one candidate is left, ambiguity is solved and method to call identified + // If method is generic, make sure the appropriate generic arguments are set + MethodInfo methodToCall = null; + if (candidates.Count == 1) + methodToCall = candidates.First()?.MakeGenericFromInputs(parameters.Select(x => x?.GetType()).ToList()); // Cache and return the method StoreExtensionMethod(key, methodToCall);