From 69d2be3a309835cff3dee559e19f9ae3bc184ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 29 Jan 2021 12:20:43 +0100 Subject: [PATCH 01/15] Added basic support for extension methods --- .../Compilation/Binding/ExpressionHelper.cs | 76 +++++++++++++++---- .../Compilation/Binding/TypeRegistry.cs | 5 ++ src/DotVVM.Framework/Utils/ReflectionUtils.cs | 7 ++ 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs index 91f678ce00..cf70f272bb 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs @@ -42,8 +42,16 @@ public static Expression GetMember(Expression target, string name, Type[] typeAr if (members.Length == 0) { - if (throwExceptions) throw new Exception($"Could not find { (isStatic ? "static" : "instance") } member { name } on type { type.FullName }."); - else return null; + // We did not find any match in regular methods => try extension methods + var extensions = type.GetAllExtensions() + .Where(m => ((isGeneric && m is TypeInfo) ? genericName : name) == m.Name) + .ToArray(); + members = extensions; + + if (members.Length == 0 && throwExceptions) + throw new Exception($"Could not find { (isStatic ? "static" : "instance") } member { name } on type { type.FullName }."); + else if (members.Length == 0 && !throwExceptions) + return null; } if (members.Length == 1) { @@ -147,36 +155,73 @@ public static Expression Call(Expression target, Expression[] arguments) public static Expression CallMethod(Expression target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) { // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) - var method = FindValidMethodOveloads(target.Type, name, flags, typeArguments, arguments, namedArgs); + var method = FindValidMethodOveloads(target, target.Type, name, flags, typeArguments, arguments, namedArgs); + + if (method.IsExtension) + { + // Change to a static call + var newArguments = CreateArgumentsForExtensionMethod(target, arguments); + return Expression.Call(method.Method, newArguments); + } + return Expression.Call(target, method.Method, method.Arguments); } + private static Expression[] CreateArgumentsForExtensionMethod(Expression target, Expression[] arguments) + { + var newArguments = new Expression[arguments.Length + 1]; + Array.Copy(arguments, 0, newArguments, 1, arguments.Length); + newArguments[0] = target; + return newArguments; + } + public static Expression CallMethod(Type target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) { // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) - var method = FindValidMethodOveloads(target, name, flags, typeArguments, arguments, namedArgs); + var method = FindValidMethodOveloads(null, target, name, flags, typeArguments, arguments, namedArgs); return Expression.Call(method.Method, method.Arguments); } - private static MethodRecognitionResult FindValidMethodOveloads(Type type, string name, BindingFlags flags, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) + private static MethodRecognitionResult FindValidMethodOveloads(Expression target, Type type, string name, BindingFlags flags, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) { - var methods = FindValidMethodOveloads(type.GetAllMembers(flags).OfType().Where(m => m.Name == name), typeArguments, arguments, namedArgs).ToList(); + var methods = FindValidMethodOveloads(type.GetAllMembers(flags).OfType().Where(m => m.Name == name), typeArguments, arguments, namedArgs).ToList(); if (methods.Count == 1) return methods.FirstOrDefault(); - if (methods.Count == 0) throw new InvalidOperationException($"Could not find overload of method '{name}'."); - else + if (methods.Count == 0) { - methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ToList(); - var method = methods.FirstOrDefault(); - var method2 = methods.Skip(1).FirstOrDefault(); - if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount) + // We did not find any match in regular methods => try extension methods + if (target != null) { - // TODO: this behavior is not completed. Implement the same behavior as in roslyn. - throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'."); + // Change to a static call + var newArguments = CreateArgumentsForExtensionMethod(target, arguments); + var extensions = FindValidMethodOveloads(type.GetAllExtensions().OfType().Where(m => m.Name == name), typeArguments, newArguments, namedArgs).ToList(); + if (extensions.Count == 0) + { + throw new InvalidOperationException($"Could not find method overload and no extension method matched '{name}'."); + } + else if (extensions.Count == 1) + { + extensions[0].IsExtension = true; + return extensions.FirstOrDefault(); + } + + target = null; + methods = extensions; + arguments = newArguments; } - return method; } + + // There are multiple method candidates + methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ToList(); + var method = methods.FirstOrDefault(); + var method2 = methods.Skip(1).FirstOrDefault(); + if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount) + { + // TODO: this behavior is not completed. Implement the same behavior as in roslyn. + throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'."); + } + return method; } private static IEnumerable FindValidMethodOveloads(IEnumerable methods, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) @@ -192,6 +237,7 @@ class MethodRecognitionResult public int CastCount { get; set; } public Expression[] Arguments { get; set; } public MethodInfo Method { get; set; } + public bool IsExtension { get; set; } } private static MethodRecognitionResult TryCallMethod(MethodInfo method, Type[] typeArguments, Expression[] positionalArguments, IDictionary namedArguments) diff --git a/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs b/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs index 13f421f6c7..5300b64ceb 100644 --- a/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs +++ b/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs @@ -127,5 +127,10 @@ public static Expression CreateStatic(Type type) ImmutableList>.Empty .Add(type => CreateStatic(compiledAssemblyCache.FindType(type + (assemblyName != null ? $", {assemblyName}" : "")))) ); + + public static IEnumerable GetRegisteredTypesForExtensionMethodsLookup() + { + yield return typeof(Enumerable); + } } } diff --git a/src/DotVVM.Framework/Utils/ReflectionUtils.cs b/src/DotVVM.Framework/Utils/ReflectionUtils.cs index 50744b47a6..b5f3196c5c 100644 --- a/src/DotVVM.Framework/Utils/ReflectionUtils.cs +++ b/src/DotVVM.Framework/Utils/ReflectionUtils.cs @@ -13,6 +13,7 @@ using System.Text; using System.Globalization; using System.Collections.Concurrent; +using DotVVM.Framework.Compilation.Binding; namespace DotVVM.Framework.Utils { @@ -63,6 +64,12 @@ public static IEnumerable GetAllMembers(this Type type, BindingFlags return type.GetMembers(flags); } + public static IEnumerable GetAllExtensions(this Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Static) + { + foreach (var registeredType in TypeRegistry.GetRegisteredTypesForExtensionMethodsLookup()) + foreach (var member in registeredType.GetMembers(flags)) + yield return member; + } /// /// Gets filesystem path of assembly CodeBase From a2d6ed9a94745517ff09a51eb992e1e7979acee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 29 Jan 2021 13:56:12 +0100 Subject: [PATCH 02/15] Added tests for extension methods in bindings --- .../Binding/BindingCompilationTests.cs | 8 ++++++++ .../Binding/JavascriptCompilationTests.cs | 12 ++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index 954a3ae78d..bae6a8655b 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -200,6 +200,14 @@ public void BindingCompiler_Invalid_LambdaParameters(string expr) Assert.ThrowsException(() => ExecuteBinding(expr, viewModel)); } + [TestMethod] + public void BindingCompiler_Valid_ExtensionMethods() + { + var viewModel = new TestViewModel(); + var result = (long[])ExecuteBinding("LongArray.Where((long item) => item % 2 != 0).ToArray()", viewModel); + CollectionAssert.AreEqual(viewModel.LongArray.Where(item => item % 2 != 0).ToArray(), result); + } + class MoqComponent : DotvvmBindableObject { public object Property diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index 540049d361..7ec5dbfdc2 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -368,16 +368,20 @@ public void JsTranslator_ArrayIndexer() } [TestMethod] - public void JsTranslator_EnumerableWhere() + [DataRow("Enumerable.Where(LongArray, (long item) => item % 2 == 0)", DisplayName = "Regular call of Enumerable.Where")] + [DataRow("LongArray.Where((long item) => item % 2 == 0)", DisplayName = "Syntax sugar - extension method")] + public void JsTranslator_EnumerableWhere(string binding) { - var result = CompileBinding("Enumerable.Where(LongArray, (long item) => item % 2 == 0)", new[] { typeof(TestViewModel) }); + var result = CompileBinding(binding, new[] { typeof(TestViewModel) }); Assert.AreEqual("LongArray().filter(function(item){return ko.unwrap(item)%2==0;})", result); } [TestMethod] - public void JsTranslator_EnumerableSelect() + [DataRow("Enumerable.Select(LongArray, (long item) => -item)", DisplayName = "Regular call of Enumerable.Select")] + [DataRow("LongArray.Select((long item) => -item)", DisplayName = "Syntax sugar - extension method")] + public void JsTranslator_EnumerableSelect(string binding) { - var result = CompileBinding("Enumerable.Select(LongArray, (long item) => -item)", new[] { typeof(TestViewModel) }); + var result = CompileBinding(binding, new[] { typeof(TestViewModel) }); Assert.AreEqual("LongArray().map(function(item){return -ko.unwrap(item);})", result); } From c85e32f7e7af6e61cfab150623d86b1b5fe25470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 29 Jan 2021 14:44:00 +0100 Subject: [PATCH 03/15] Fixed issue with missing JsTranslators --- .../Binding/JavascriptCompilationTests.cs | 7 +++++++ .../Compilation/Binding/ExpressionHelper.cs | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index 7ec5dbfdc2..b95fe405f5 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -385,6 +385,13 @@ public void JsTranslator_EnumerableSelect(string binding) Assert.AreEqual("LongArray().map(function(item){return -ko.unwrap(item);})", result); } + [TestMethod] + public void JsTranslator_ValidMethod_UnsupportedTranslation() + { + Assert.ThrowsException(() => + CompileBinding("Enumerable.Skip(LongArray, 2)", new[] { typeof(TestViewModel) })); + } + [TestMethod] public void JavascriptCompilation_GuidToString() { diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs index cf70f272bb..17aefd3e93 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs @@ -196,11 +196,9 @@ private static MethodRecognitionResult FindValidMethodOveloads(Expression target // Change to a static call var newArguments = CreateArgumentsForExtensionMethod(target, arguments); var extensions = FindValidMethodOveloads(type.GetAllExtensions().OfType().Where(m => m.Name == name), typeArguments, newArguments, namedArgs).ToList(); - if (extensions.Count == 0) - { - throw new InvalidOperationException($"Could not find method overload and no extension method matched '{name}'."); - } - else if (extensions.Count == 1) + + // We found an extension method + if (extensions.Count == 1) { extensions[0].IsExtension = true; return extensions.FirstOrDefault(); @@ -210,6 +208,9 @@ private static MethodRecognitionResult FindValidMethodOveloads(Expression target methods = extensions; arguments = newArguments; } + + if (methods.Count == 0) + throw new InvalidOperationException($"Could not find method overload nor extension method that matched '{name}'."); } // There are multiple method candidates From 4f6f5ef45aaa70b28049087a60454c087a96de6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 29 Jan 2021 15:10:22 +0100 Subject: [PATCH 04/15] Adjusted sample to showcase extension methods --- .../LambdaExpressions/LambdaExpressions.dothtml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/DotVVM.Samples.Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml b/src/DotVVM.Samples.Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml index 528e754483..24f7e394be 100644 --- a/src/DotVVM.Samples.Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml +++ b/src/DotVVM.Samples.Common/Views/FeatureSamples/LambdaExpressions/LambdaExpressions.dothtml @@ -24,12 +24,21 @@

Operations

+

+ Showcasing LINQ and JsTranslator
+ Note: you can use either explicit expressions (example: Enumerable.Where(Collection, ...)), + but also extension-method-like calls (example: Collection.Where(...)) +

+ + <%--Click="{command: SetResult(Enumerable.Where(Array, (int item) => item % 2 == 0))}" />--%> + Click="{command: SetResult(Array.Where((int item) => item % 2 == 0))}" /> + <%--Click="{command: SetResult(Enumerable.Where(Array, (int item) => item % 2 == 1))}" />--%> + Click="{command: SetResult(Array.Where((int item) => item % 2 == 1))}" /> + <%--Click="{command: SetResult(Enumerable.Select(Array, (int item) => -item))}" />--%> + Click="{command: SetResult(Array.Select((int item) => -item))}" />
From de7e47906b1fe0863cf76906279e700f9c9f8451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 12 Feb 2021 11:31:57 +0100 Subject: [PATCH 05/15] Refactoring --- .../Compilation/Binding/ExpressionHelper.cs | 18 ++++-------------- src/DotVVM.Framework/Utils/ReflectionUtils.cs | 6 +++--- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs index 17aefd3e93..6ce7a600a4 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs @@ -160,21 +160,13 @@ public static Expression CallMethod(Expression target, BindingFlags flags, strin if (method.IsExtension) { // Change to a static call - var newArguments = CreateArgumentsForExtensionMethod(target, arguments); + var newArguments = new[] { target }.Concat(arguments); return Expression.Call(method.Method, newArguments); } return Expression.Call(target, method.Method, method.Arguments); } - private static Expression[] CreateArgumentsForExtensionMethod(Expression target, Expression[] arguments) - { - var newArguments = new Expression[arguments.Length + 1]; - Array.Copy(arguments, 0, newArguments, 1, arguments.Length); - newArguments[0] = target; - return newArguments; - } - public static Expression CallMethod(Type target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) { // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) @@ -194,15 +186,13 @@ private static MethodRecognitionResult FindValidMethodOveloads(Expression target if (target != null) { // Change to a static call - var newArguments = CreateArgumentsForExtensionMethod(target, arguments); - var extensions = FindValidMethodOveloads(type.GetAllExtensions().OfType().Where(m => m.Name == name), typeArguments, newArguments, namedArgs).ToList(); + var newArguments = new[] { target }.Concat(arguments).ToArray(); + var extensions = FindValidMethodOveloads(type.GetAllExtensions().OfType().Where(m => m.Name == name), typeArguments, newArguments, namedArgs) + .Select(method => { method.IsExtension = true; return method; }).ToList(); // We found an extension method if (extensions.Count == 1) - { - extensions[0].IsExtension = true; return extensions.FirstOrDefault(); - } target = null; methods = extensions; diff --git a/src/DotVVM.Framework/Utils/ReflectionUtils.cs b/src/DotVVM.Framework/Utils/ReflectionUtils.cs index b5f3196c5c..c220ef24a2 100644 --- a/src/DotVVM.Framework/Utils/ReflectionUtils.cs +++ b/src/DotVVM.Framework/Utils/ReflectionUtils.cs @@ -64,11 +64,11 @@ public static IEnumerable GetAllMembers(this Type type, BindingFlags return type.GetMembers(flags); } - public static IEnumerable GetAllExtensions(this Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Static) + public static IEnumerable GetAllExtensions(this Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Static) { foreach (var registeredType in TypeRegistry.GetRegisteredTypesForExtensionMethodsLookup()) - foreach (var member in registeredType.GetMembers(flags)) - yield return member; + foreach (var method in registeredType.GetMethods(flags)) + yield return method; } /// From e596a7967da1802319462a415332dd9c0f5fa3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 12 Feb 2021 12:10:28 +0100 Subject: [PATCH 06/15] Fixed error after merge conflict --- .../Binding/JavascriptCompilationTests.cs | 2 +- .../ConfigurationSerializationTests.ExperimentalFeatures.json | 2 +- .../config-tests/ConfigurationSerializationTests.Markup.json | 2 +- .../config-tests/ConfigurationSerializationTests.RestAPI.json | 2 +- .../ConfigurationSerializationTests.SerializeDefaultConfig.json | 2 +- .../ConfigurationSerializationTests.SerializeEmptyConfig.json | 2 +- .../ConfigurationSerializationTests.SerializeResources.json | 2 +- .../ConfigurationSerializationTests.SerializeRoutes.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index 9738c48060..53977b01fe 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -386,7 +386,7 @@ public void JsTranslator_NestedEnumerableMethods() [TestMethod] [DataRow("Enumerable.Select(LongArray, (long item) => -item)", DisplayName = "Regular call of Enumerable.Select")] [DataRow("LongArray.Select((long item) => -item)", DisplayName = "Syntax sugar - extension method")] - public void JsTranslator_EnumerableSelect() + public void JsTranslator_EnumerableSelect(string binding) { var result = CompileBinding(binding, new[] { typeof(TestViewModel) }); Assert.AreEqual("LongArray().map(function(item){return -ko.unwrap(item);})", result); diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json index 640b0d5487..77674fe378 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json @@ -12,7 +12,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": { "lazyCsrfToken": { "enabled": true, diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json index c97a035963..7c67e41df6 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json @@ -68,7 +68,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json index 152ab7fd00..c37fc019cc 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json @@ -50,7 +50,7 @@ }, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index 8135334ae6..d88535437d 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -130,7 +130,7 @@ }, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json index 12b0983b85..9b14c92cb5 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json @@ -12,7 +12,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json index 342355241e..dd022c98f3 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json @@ -89,7 +89,7 @@ }, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json index 338c4ba471..ec9547ddf2 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json @@ -43,7 +43,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "en-US", + "defaultCulture": "sk-SK", "experimentalFeatures": {}, "debug": false } From 34cd5850b24cb3759586d84df05ab330b6c0f321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 12 Feb 2021 12:35:24 +0100 Subject: [PATCH 07/15] Reverted serialization configurations changed by mistake --- .../ConfigurationSerializationTests.ExperimentalFeatures.json | 2 +- .../config-tests/ConfigurationSerializationTests.Markup.json | 2 +- .../config-tests/ConfigurationSerializationTests.RestAPI.json | 2 +- .../ConfigurationSerializationTests.SerializeDefaultConfig.json | 2 +- .../ConfigurationSerializationTests.SerializeEmptyConfig.json | 2 +- .../ConfigurationSerializationTests.SerializeResources.json | 2 +- .../ConfigurationSerializationTests.SerializeRoutes.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json index 77674fe378..640b0d5487 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.ExperimentalFeatures.json @@ -12,7 +12,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": { "lazyCsrfToken": { "enabled": true, diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json index 7c67e41df6..c97a035963 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.Markup.json @@ -68,7 +68,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json index c37fc019cc..152ab7fd00 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.RestAPI.json @@ -50,7 +50,7 @@ }, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json index d88535437d..8135334ae6 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeDefaultConfig.json @@ -130,7 +130,7 @@ }, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json index 9b14c92cb5..12b0983b85 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeEmptyConfig.json @@ -12,7 +12,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json index dd022c98f3..342355241e 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeResources.json @@ -89,7 +89,7 @@ }, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": {}, "debug": false } diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json index ec9547ddf2..338c4ba471 100644 --- a/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json +++ b/src/DotVVM.Framework.Tests.Common/Runtime/config-tests/ConfigurationSerializationTests.SerializeRoutes.json @@ -43,7 +43,7 @@ "resources": {}, "security": {}, "runtime": {}, - "defaultCulture": "sk-SK", + "defaultCulture": "en-US", "experimentalFeatures": {}, "debug": false } From fd49595cd65d6da16b4079df04f4d85e3107aab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 12 Feb 2021 16:40:06 +0100 Subject: [PATCH 08/15] Removed redundant check --- src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs index 6ce7a600a4..e38cbcc831 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs @@ -44,8 +44,7 @@ public static Expression GetMember(Expression target, string name, Type[] typeAr { // We did not find any match in regular methods => try extension methods var extensions = type.GetAllExtensions() - .Where(m => ((isGeneric && m is TypeInfo) ? genericName : name) == m.Name) - .ToArray(); + .Where(m => m.Name == name).ToArray(); members = extensions; if (members.Length == 0 && throwExceptions) From 934c00f9fbe219854cb9f84af084668d0be53898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 19 Feb 2021 17:03:42 +0100 Subject: [PATCH 09/15] Added option to register custom types for extensions method lookup Refactored ExpressionHelper --- .../Binding/ExpressionHelperTests.cs | 19 +- .../Binding/JavascriptCompilationTests.cs | 2 +- .../Binding/StaticCommandCompilationTests.cs | 2 +- .../DefaultControlTreeResolverTests.cs | 2 +- .../Binding/BindingExpressionBuilder.cs | 8 +- .../Binding/DefaultExtensionsProvider.cs | 32 ++ .../Binding/ExpressionBuildingVisitor.cs | 16 +- .../Compilation/Binding/ExpressionHelper.cs | 423 ----------------- .../GeneralBindingPropertyResolvers.cs | 6 +- .../Binding/IExtensionsProvider.cs | 14 + .../Binding/MemberExpressionFactory.cs | 448 ++++++++++++++++++ .../Binding/MethodGroupExpression.cs | 6 +- .../Compilation/Binding/TypeRegistry.cs | 5 - .../Binding/UnknownTypeSentinel.cs | 12 + .../ControlTree/DefaultControlTreeResolver.cs | 2 +- .../Resolved/ResolvedTreeBuilder.cs | 6 +- .../DotVVMServiceCollectionExtensions.cs | 1 + src/DotVVM.Framework/Utils/ReflectionUtils.cs | 7 - 18 files changed, 550 insertions(+), 461 deletions(-) create mode 100644 src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs create mode 100644 src/DotVVM.Framework/Compilation/Binding/IExtensionsProvider.cs create mode 100644 src/DotVVM.Framework/Compilation/Binding/MemberExpressionFactory.cs create mode 100644 src/DotVVM.Framework/Compilation/Binding/UnknownTypeSentinel.cs diff --git a/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs index 79e8bbf97a..81298a9690 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs @@ -9,6 +9,7 @@ using DotVVM.Framework.Utils; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CSharp.RuntimeBinder; +using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace DotVVM.Framework.Tests.Common.Binding @@ -17,13 +18,21 @@ namespace DotVVM.Framework.Tests.Common.Binding public class ExpressionHelperTests { public TestContext TestContext { get; set; } + private MemberExpressionFactory memberExpressionFactory; + + [TestInitialize] + public void INIT() + { + var configuration = DotvvmTestHelper.CreateConfiguration(); + memberExpressionFactory = configuration.ServiceProvider.GetRequiredService(); + } [TestMethod] public void UpdateMember_GetValue() { var cP = Expression.Parameter(typeof(DotvvmControl), "c"); var newValueP = Expression.Parameter(typeof(object), "newValue"); - var updateExpr = ExpressionHelper.UpdateMember(ExpressionUtils.Replace((DotvvmControl c) => c.GetValue(DotvvmBindableObject.DataContextProperty, true), cP), newValueP); + var updateExpr = memberExpressionFactory.UpdateMember(ExpressionUtils.Replace((DotvvmControl c) => c.GetValue(DotvvmBindableObject.DataContextProperty, true), cP), newValueP); Assert.IsNotNull(updateExpr); Assert.AreEqual("c.SetValue(DotvvmBindableObject.DataContextProperty, newValue)", updateExpr.ToString()); } @@ -33,7 +42,7 @@ public void UpdateMember_NormalProperty() { var vmP = Expression.Parameter(typeof(Tests.Binding.TestViewModel), "vm"); var newValueP = Expression.Parameter(typeof(DateTime), "newValue"); - var updateExpr = ExpressionHelper.UpdateMember(ExpressionUtils.Replace((Tests.Binding.TestViewModel c) => c.DateFrom, vmP), newValueP); + var updateExpr = memberExpressionFactory.UpdateMember(ExpressionUtils.Replace((Tests.Binding.TestViewModel c) => c.DateFrom, vmP), newValueP); Assert.IsNotNull(updateExpr); Assert.AreEqual("(vm.DateFrom = Convert(newValue, Nullable`1))", updateExpr.ToString()); } @@ -43,7 +52,7 @@ public void UpdateMember_ReadOnlyProperty() { var vmP = Expression.Parameter(typeof(Tests.Binding.TestViewModel), "vm"); var newValueP = Expression.Parameter(typeof(long[]), "newValue"); - var updateExpr = ExpressionHelper.UpdateMember(ExpressionUtils.Replace((Tests.Binding.TestViewModel c) => c.LongArray, vmP), newValueP); + var updateExpr = memberExpressionFactory.UpdateMember(ExpressionUtils.Replace((Tests.Binding.TestViewModel c) => c.LongArray, vmP), newValueP); Assert.IsNull(updateExpr); } @@ -57,7 +66,7 @@ public void Call_FindOverload_Generic_FirstLevel(Type resultIdentifierType, Type Call_FindOverload_Generic(typeof(MethodsGenericArgumentsResolvingSampleObject), MethodsGenericArgumentsResolvingSampleObject.MethodName, new[] { argType }, resultIdentifierType, expectedGenericArgs); } - private static void Call_FindOverload_Generic(Type targetType, string methodName, Type[] argTypes, Type resultIdentifierType, Type[] expectedGenericArgs) + private void Call_FindOverload_Generic(Type targetType, string methodName, Type[] argTypes, Type resultIdentifierType, Type[] expectedGenericArgs) { Expression target = new MethodGroupExpression() { MethodName = methodName, @@ -66,7 +75,7 @@ private static void Call_FindOverload_Generic(Type targetType, string methodName var j = 0; var arguments = argTypes.Select(s => Expression.Parameter(s, $"param_{j++}")).ToArray(); - var expression = ExpressionHelper.Call(target, arguments) as MethodCallExpression; + var expression = memberExpressionFactory.Call(target, arguments) as MethodCallExpression; Assert.IsNotNull(expression); Assert.AreEqual(resultIdentifierType, expression.Method.GetResultType()); diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index 53977b01fe..fbb68f06c4 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -47,7 +47,7 @@ public string CompileBinding(string expression, Type[] contexts, Type expectedTy { context = DataContextStack.Create(contexts[i], context); } - var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService()); + var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService(), configuration.ServiceProvider.GetRequiredService()); var parsedExpression = parser.ParseWithLambdaConversion(expression, context, BindingParserOptions.Create(), expectedType); var expressionTree = TypeConversion.MagicLambdaConversion(parsedExpression, expectedType) ?? diff --git a/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs index 455fa14916..b625237c99 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/StaticCommandCompilationTests.cs @@ -61,7 +61,7 @@ public string CompileBinding(string expression, bool niceMode, Type[] contexts, var options = BindingParserOptions.Create() .AddImports(configuration.Markup.ImportedNamespaces); - var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService()); + var parser = new BindingExpressionBuilder(configuration.ServiceProvider.GetRequiredService(), configuration.ServiceProvider.GetRequiredService()); var expressionTree = parser.ParseWithLambdaConversion(expression, context, options, expectedType); var jsExpression = configuration.ServiceProvider.GetRequiredService().CompileToJavascript(context, expressionTree); diff --git a/src/DotVVM.Framework.Tests.Common/Runtime/ControlTree/DefaultControlTreeResolverTests.cs b/src/DotVVM.Framework.Tests.Common/Runtime/ControlTree/DefaultControlTreeResolverTests.cs index a8bbd2d0aa..a29a36f5dd 100755 --- a/src/DotVVM.Framework.Tests.Common/Runtime/ControlTree/DefaultControlTreeResolverTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Runtime/ControlTree/DefaultControlTreeResolverTests.cs @@ -649,7 +649,7 @@ public void ResolvedTree_ViewModel_InvalidAssemblyQualified() { var root = ParseSource(@"@viewModel System.String, whatever"); Assert.IsTrue(root.Directives.Any(d => d.Value.Any(dd => dd.DothtmlNode.HasNodeErrors))); - Assert.AreEqual(typeof(ExpressionHelper.UnknownTypeSentinel), root.DataContextTypeStack.DataContextType); + Assert.AreEqual(typeof(UnknownTypeSentinel), root.DataContextTypeStack.DataContextType); } private ResolvedBinding[] GetLiteralBindings(ResolvedContentNode node) => diff --git a/src/DotVVM.Framework/Compilation/Binding/BindingExpressionBuilder.cs b/src/DotVVM.Framework/Compilation/Binding/BindingExpressionBuilder.cs index 1ff72eeed4..3473974eec 100644 --- a/src/DotVVM.Framework/Compilation/Binding/BindingExpressionBuilder.cs +++ b/src/DotVVM.Framework/Compilation/Binding/BindingExpressionBuilder.cs @@ -17,10 +17,12 @@ namespace DotVVM.Framework.Compilation.Binding public class BindingExpressionBuilder : IBindingExpressionBuilder { private readonly CompiledAssemblyCache compiledAssemblyCache; + private readonly MemberExpressionFactory memberExpressionFactory; - public BindingExpressionBuilder(CompiledAssemblyCache compiledAssemblyCache) + public BindingExpressionBuilder(CompiledAssemblyCache compiledAssemblyCache, MemberExpressionFactory memberExpressionFactory) { this.compiledAssemblyCache = compiledAssemblyCache; + this.memberExpressionFactory = memberExpressionFactory; } public Expression Parse(string expression, DataContextStack dataContexts, BindingParserOptions options, params KeyValuePair[] additionalSymbols) @@ -49,7 +51,7 @@ public Expression Parse(string expression, DataContextStack dataContexts, Bindin symbols = symbols.AddSymbols(options.ExtensionParameters.Select(p => CreateParameter(dataContexts, p.Identifier, p))); symbols = symbols.AddSymbols(additionalSymbols); - var visitor = new ExpressionBuildingVisitor(symbols); + var visitor = new ExpressionBuildingVisitor(symbols, memberExpressionFactory); visitor.Scope = symbols.Resolve(options.ScopeParameter); return visitor.Visit(node); } @@ -116,7 +118,7 @@ static ParameterExpression CreateParameter(DataContextStack stackItem, string na (extensionParameter == null ? stackItem.DataContextType : ResolvedTypeDescriptor.ToSystemType(extensionParameter.ParameterType)) - ?? typeof(ExpressionHelper.UnknownTypeSentinel) + ?? typeof(UnknownTypeSentinel) , name) .AddParameterAnnotation(new BindingParameterAnnotation(stackItem, extensionParameter)); } diff --git a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs new file mode 100644 index 0000000000..5f9e000098 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace DotVVM.Framework.Compilation.Binding +{ + public class DefaultExtensionsProvider : IExtensionsProvider + { + private readonly List typesLookup; + + public DefaultExtensionsProvider() + { + typesLookup = new List(); + AddTypeForExtensionsLookup(typeof(Enumerable)); + } + + protected void AddTypeForExtensionsLookup(Type type) + { + typesLookup.Add(typeof(Enumerable)); + } + + public virtual IEnumerable GetExtensionMethods() + { + foreach (var registeredType in typesLookup) + foreach (var method in registeredType.GetMethods(BindingFlags.Public | BindingFlags.Static)) + yield return method; + } + } +} diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs index 44b6b410a1..e655219247 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionBuildingVisitor.cs @@ -16,10 +16,12 @@ public class ExpressionBuildingVisitor : BindingParserNodeVisitor public bool ResolveOnlyTypeName { get; set; } private List currentErrors; + private readonly MemberExpressionFactory memberExpressionFactory; - public ExpressionBuildingVisitor(TypeRegistry registry) + public ExpressionBuildingVisitor(TypeRegistry registry, MemberExpressionFactory memberExpressionFactory) { Registry = registry; + this.memberExpressionFactory = memberExpressionFactory; } protected T HandleErrors(TNode node, Func action, string defaultErrorMessage = "Binding compilation failed", bool allowResultNull = true) @@ -120,7 +122,7 @@ protected override Expression VisitUnaryOperator(UnaryOperatorBindingParserNode default: throw new NotSupportedException($"unary operator { node.Operator } is not supported"); } - return ExpressionHelper.GetUnaryOperator(operand, eop); + return memberExpressionFactory.GetUnaryOperator(operand, eop); } protected override Expression VisitBinaryOperator(BinaryOperatorBindingParserNode node) @@ -188,7 +190,7 @@ protected override Expression VisitBinaryOperator(BinaryOperatorBindingParserNod throw new NotSupportedException($"unary operator { node.Operator } is not supported"); } - return ExpressionHelper.GetBinaryOperator(left, right, eop); + return memberExpressionFactory.GetBinaryOperator(left, right, eop); } protected override Expression VisitArrayAccess(ArrayAccessBindingParserNode node) @@ -210,7 +212,7 @@ protected override Expression VisitFunctionCall(FunctionCallBindingParserNode no } ThrowOnErrors(); - return ExpressionHelper.Call(target, args); + return memberExpressionFactory.Call(target, args); } protected override Expression VisitSimpleName(SimpleNameBindingParserNode node) @@ -260,7 +262,7 @@ protected override Expression VisitMemberAccess(MemberAccessBindingParserNode no return resolvedTypeExpression; } - return ExpressionHelper.GetMember(target, nameNode.Name, typeParameters, onlyMemberTypes: ResolveOnlyTypeName); + return memberExpressionFactory.GetMember(target, nameNode.Name, typeParameters, onlyMemberTypes: ResolveOnlyTypeName); } protected override Expression VisitGenericName(GenericNameBindingParserNode node) @@ -336,11 +338,11 @@ private Expression GetMemberOrTypeExpression(IdentifierNameBindingParserNode nod var expr = Scope == null ? Registry.Resolve(node.Name, throwOnNotFound: false) - : (ExpressionHelper.GetMember(Scope, node.Name, typeParameters, throwExceptions: false, onlyMemberTypes: ResolveOnlyTypeName) + : (memberExpressionFactory.GetMember(Scope, node.Name, typeParameters, throwExceptions: false, onlyMemberTypes: ResolveOnlyTypeName) ?? Registry.Resolve(node.Name, throwOnNotFound: false)); if (expr == null) return new UnknownStaticClassIdentifierExpression(node.Name); - if (expr is ParameterExpression && expr.Type == typeof(ExpressionHelper.UnknownTypeSentinel)) throw new Exception($"Type of '{expr}' could not be resolved."); + if (expr is ParameterExpression && expr.Type == typeof(UnknownTypeSentinel)) throw new Exception($"Type of '{expr}' could not be resolved."); return expr; } diff --git a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs index e38cbcc831..f8f7684368 100644 --- a/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs +++ b/src/DotVVM.Framework/Compilation/Binding/ExpressionHelper.cs @@ -20,392 +20,6 @@ namespace DotVVM.Framework.Compilation.Binding public static class ExpressionHelper { - public static Expression GetMember(Expression target, string name, Type[] typeArguments = null, bool throwExceptions = true, bool onlyMemberTypes = false) - { - if (target is MethodGroupExpression) - throw new Exception("Can not access member on method group."); - - var type = target.Type; - if (type == typeof(UnknownTypeSentinel)) if (throwExceptions) throw new Exception($"Type of '{target}' could not be resolved."); else return null; - - var isStatic = target is StaticClassIdentifierExpression; - - var isGeneric = typeArguments != null && typeArguments.Length != 0; - var genericName = isGeneric ? $"{name}`{typeArguments.Length}" : name; - - if (!isGeneric && !onlyMemberTypes && typeof(DotvvmBindableObject).IsAssignableFrom(target.Type) && - GetDotvvmPropertyMember(target, name) is Expression result) return result; - - var members = type.GetAllMembers(BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance)) - .Where(m => ((isGeneric && m is TypeInfo) ? genericName : name) == m.Name) - .ToArray(); - - if (members.Length == 0) - { - // We did not find any match in regular methods => try extension methods - var extensions = type.GetAllExtensions() - .Where(m => m.Name == name).ToArray(); - members = extensions; - - if (members.Length == 0 && throwExceptions) - throw new Exception($"Could not find { (isStatic ? "static" : "instance") } member { name } on type { type.FullName }."); - else if (members.Length == 0 && !throwExceptions) - return null; - } - if (members.Length == 1) - { - if (!(members[0] is TypeInfo) && onlyMemberTypes) { throw new Exception("Only type names are supported."); } - - var instance = isStatic ? null : target; - if (members[0] is PropertyInfo) - { - var property = members[0] as PropertyInfo; - return Expression.Property(instance, property); - } - else if (members[0] is FieldInfo) - { - var field = members[0] as FieldInfo; - return Expression.Field(instance, field); - } - else if (members[0] is TypeInfo) - { - var nonGenericType = (TypeInfo)members[0]; - return isGeneric - ? new StaticClassIdentifierExpression(nonGenericType.MakeGenericType(typeArguments)) - : new StaticClassIdentifierExpression(nonGenericType.UnderlyingSystemType); - } - } - return new MethodGroupExpression() { MethodName = name, Target = target, TypeArgs = typeArguments }; - } - - static Expression GetDotvvmPropertyMember(Expression target, string name) - { - var property = DotvvmProperty.ResolveProperty(target.Type, name); - if (property == null) return null; - - var field = property.DeclaringType.GetField(property.Name + "Property", BindingFlags.Static | BindingFlags.Public); - if (field == null) return null; - - return Expression.Convert( - Expression.Call(target, "GetValue", Type.EmptyTypes, - Expression.Field(null, field), - Expression.Constant(true) - ), - property.PropertyType - ); - } - - /// - /// Creates an expression that updates the member inside with a - /// new . - /// - /// - /// Should contain a call to the - /// method, it will be - /// replaced with a - /// call. - /// - public static Expression UpdateMember(Expression node, Expression value) - { - if ((node.NodeType == ExpressionType.MemberAccess - && node is MemberExpression member - && member.Member is PropertyInfo property - && property.CanWrite) - || node.NodeType == ExpressionType.Parameter - || node.NodeType == ExpressionType.Index) - { - return Expression.Assign(node, Expression.Convert(value, node.Type)); - } - - var current = node; - while (current.NodeType == ExpressionType.Convert - && current is UnaryExpression unary) - { - current = unary.Operand; - } - - if (current.NodeType == ExpressionType.Call - && current is MethodCallExpression call - && call.Method.DeclaringType == typeof(DotvvmBindableObject) - && call.Method.Name == nameof(DotvvmBindableObject.GetValue) - && call.Arguments.Count == 2 - && call.Arguments[0].Type == typeof(DotvvmProperty) - && call.Arguments[1].Type == typeof(bool)) - { - var propertyArgument = call.Arguments[0]; - var setValue = typeof(DotvvmBindableObject) - .GetMethod(nameof(DotvvmBindableObject.SetValue), - new[] { typeof(DotvvmProperty), typeof(object) }); - return Expression.Call(call.Object, setValue, propertyArgument, value); - } - - return null; - } - - public static Expression Call(Expression target, Expression[] arguments) - { - if (target is MethodGroupExpression) - { - return ((MethodGroupExpression)target).CreateMethodCall(arguments); - } - return Expression.Invoke(target, arguments); - } - - public static Expression CallMethod(Expression target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) - { - // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) - var method = FindValidMethodOveloads(target, target.Type, name, flags, typeArguments, arguments, namedArgs); - - if (method.IsExtension) - { - // Change to a static call - var newArguments = new[] { target }.Concat(arguments); - return Expression.Call(method.Method, newArguments); - } - - return Expression.Call(target, method.Method, method.Arguments); - } - - public static Expression CallMethod(Type target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) - { - // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) - var method = FindValidMethodOveloads(null, target, name, flags, typeArguments, arguments, namedArgs); - return Expression.Call(method.Method, method.Arguments); - } - - - private static MethodRecognitionResult FindValidMethodOveloads(Expression target, Type type, string name, BindingFlags flags, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) - { - var methods = FindValidMethodOveloads(type.GetAllMembers(flags).OfType().Where(m => m.Name == name), typeArguments, arguments, namedArgs).ToList(); - - if (methods.Count == 1) return methods.FirstOrDefault(); - if (methods.Count == 0) - { - // We did not find any match in regular methods => try extension methods - if (target != null) - { - // Change to a static call - var newArguments = new[] { target }.Concat(arguments).ToArray(); - var extensions = FindValidMethodOveloads(type.GetAllExtensions().OfType().Where(m => m.Name == name), typeArguments, newArguments, namedArgs) - .Select(method => { method.IsExtension = true; return method; }).ToList(); - - // We found an extension method - if (extensions.Count == 1) - return extensions.FirstOrDefault(); - - target = null; - methods = extensions; - arguments = newArguments; - } - - if (methods.Count == 0) - throw new InvalidOperationException($"Could not find method overload nor extension method that matched '{name}'."); - } - - // There are multiple method candidates - methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ToList(); - var method = methods.FirstOrDefault(); - var method2 = methods.Skip(1).FirstOrDefault(); - if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount) - { - // TODO: this behavior is not completed. Implement the same behavior as in roslyn. - throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'."); - } - return method; - } - - private static IEnumerable FindValidMethodOveloads(IEnumerable methods, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) - => from m in methods - let r = TryCallMethod(m, typeArguments, arguments, namedArgs) - where r != null - orderby r.CastCount descending, r.AutomaticTypeArgCount - select r; - - class MethodRecognitionResult - { - public int AutomaticTypeArgCount { get; set; } - public int CastCount { get; set; } - public Expression[] Arguments { get; set; } - public MethodInfo Method { get; set; } - public bool IsExtension { get; set; } - } - - private static MethodRecognitionResult TryCallMethod(MethodInfo method, Type[] typeArguments, Expression[] positionalArguments, IDictionary namedArguments) - { - var parameters = method.GetParameters(); - - int castCount = 0; - if (parameters.Length < positionalArguments.Length) return null; - var args = new Expression[parameters.Length]; - Array.Copy(positionalArguments, args, positionalArguments.Length); - int namedArgCount = 0; - for (int i = positionalArguments.Length; i < args.Length; i++) - { - if (namedArguments?.ContainsKey(parameters[i].Name) == true) - { - args[i] = namedArguments[parameters[i].Name]; - namedArgCount++; - } - else if (parameters[i].HasDefaultValue) - { - castCount++; - args[i] = Expression.Constant(parameters[i].DefaultValue, parameters[i].ParameterType); - } - else return null; - } - - // some named arguments were not used - if (namedArguments != null && namedArgCount != namedArguments.Count) return null; - - int automaticTypeArgs = 0; - // resolve generic parameters - if (method.ContainsGenericParameters) - { - var genericArguments = method.GetGenericArguments(); - var typeArgs = new Type[genericArguments.Length]; - if (typeArguments != null) - { - if (typeArguments.Length > typeArgs.Length) return null; - Array.Copy(typeArguments, typeArgs, typeArgs.Length); - } - for (int genericArgumentPosition = 0; genericArgumentPosition < typeArgs.Length; genericArgumentPosition++) - { - if (typeArgs[genericArgumentPosition] == null) - { - // try to resolve from arguments - var argType = GetGenericParameterType(genericArguments[genericArgumentPosition], parameters.Select(s => s.ParameterType).ToArray(), args.Select(s => s.Type).ToArray()); - automaticTypeArgs++; - if (argType != null) typeArgs[genericArgumentPosition] = argType; - else return null; - } - } - method = method.MakeGenericMethod(typeArgs); - parameters = method.GetParameters(); - } - else if (typeArguments != null) return null; - - // cast arguments - for (int i = 0; i < args.Length; i++) - { - var casted = TypeConversion.ImplicitConversion(args[i], parameters[i].ParameterType); - if (casted == null) return null; - if (casted != args[i]) - { - castCount++; - args[i] = casted; - } - } - - return new MethodRecognitionResult { - CastCount = castCount, - AutomaticTypeArgCount = automaticTypeArgs, - Method = method, - Arguments = args - }; - } - - private static Type GetGenericParameterType(Type genericArg, Type[] searchedGenericTypes, Type[] expressionTypes) - { - for (var i = 0; i < searchedGenericTypes.Length; i++) - { - if (expressionTypes.Length <= i) return null; - var sgt = searchedGenericTypes[i]; - if (sgt == genericArg) - { - return expressionTypes[i]; - } - if (sgt.IsArray) - { - var elementType = sgt.GetElementType(); - var expressionElementType = expressionTypes[i].GetElementType(); - if (elementType == genericArg) - return expressionElementType; - else - return GetGenericParameterType(genericArg, searchedGenericTypes[i].GetGenericArguments(), expressionTypes[i].GetGenericArguments()); - } - else if (sgt.IsGenericType) - { - var value = GetGenericParameterType(genericArg, sgt.GetGenericArguments(), expressionTypes[i].GetGenericArguments()); - if (value is Type) return value; - } - } - return null; - } - - public static Expression EqualsMethod(Expression left, Expression right) - { - Expression equatable = null; - Expression theOther = null; - if (typeof(IEquatable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IEquatable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - var m = CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { theOther }); - if (m != null) return m; - } - - if (left.Type.GetTypeInfo().IsValueType) - { - equatable = left; - theOther = right; - } - else if (left.Type.GetTypeInfo().IsValueType) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - theOther = TypeConversion.ImplicitConversion(theOther, equatable.Type); - if (theOther != null) return Expression.Equal(equatable, theOther); - } - - return CallMethod(left, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { right }); - } - - public static Expression CompareMethod(Expression left, Expression right) - { - Type compareType = typeof(object); - Expression equatable = null; - Expression theOther = null; - if (typeof(IComparable<>).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IComparable<>).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - else if (typeof(IComparable).IsAssignableFrom(left.Type)) - { - equatable = left; - theOther = right; - } - else if (typeof(IComparable).IsAssignableFrom(right.Type)) - { - equatable = right; - theOther = left; - } - - if (equatable != null) - { - return CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Compare", null, new[] { theOther }); - } - throw new NotSupportedException("IComparable is not implemented on any of specified types"); - } - public static Expression RewriteTaskSequence(Expression left, Expression right) { // if the left side is a task, make the right side also a task and join them @@ -439,44 +53,9 @@ public static Expression RewriteTaskSequence(Expression left, Expression right) } } - public static Expression UnwrapNullable(this Expression expression) => expression.Type.IsNullable() ? Expression.Property(expression, "Value") : expression; - public static Expression GetBinaryOperator(Expression left, Expression right, ExpressionType operation) - { - if (operation == ExpressionType.Coalesce) return Expression.Coalesce(left, right); - if (operation == ExpressionType.Assign) - { - return Expression.Assign(left, TypeConversion.ImplicitConversion(right, left.Type, true, true)); - } - - // TODO: type conversions - if (operation == ExpressionType.AndAlso) return Expression.AndAlso(left, right); - else if (operation == ExpressionType.OrElse) return Expression.OrElse(left, right); - - var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation( - CSharpBinderFlags.None, operation, typeof(object), GetBinderArguments(2)); - var result = ApplyBinder(binder, false, left, right); - if (result != null) return result; - if (operation == ExpressionType.Equal) return EqualsMethod(left, right); - if (operation == ExpressionType.NotEqual) return Expression.Not(EqualsMethod(left, right)); - - // lift the operator - if (left.Type.IsNullable() || right.Type.IsNullable()) - return GetBinaryOperator(left.UnwrapNullable(), right.UnwrapNullable(), operation); - - throw new Exception($"could not apply { operation } binary operator to { left } and { right }"); - // TODO: comparison operators - } - - public static Expression GetUnaryOperator(Expression expr, ExpressionType operation) - { - var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.UnaryOperation( - CSharpBinderFlags.None, operation, typeof(object), GetBinderArguments(1)); - return ApplyBinder(binder, true, expr); - } - public static Expression GetIndexer(Expression expr, Expression index) { if (expr.Type.IsArray) return Expression.ArrayIndex(expr, index); @@ -525,7 +104,5 @@ public static Expression ApplyBinder(DynamicMetaObjectBinder binder, bool throwE } return result.Expression; } - - public sealed class UnknownTypeSentinel { } } } diff --git a/src/DotVVM.Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs b/src/DotVVM.Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs index 48bcd87dda..d495cb59c7 100644 --- a/src/DotVVM.Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs +++ b/src/DotVVM.Framework/Compilation/Binding/GeneralBindingPropertyResolvers.cs @@ -28,13 +28,15 @@ public class BindingPropertyResolvers private readonly IBindingExpressionBuilder bindingParser; private readonly StaticCommandBindingCompiler staticCommandBindingCompiler; private readonly JavascriptTranslator javascriptTranslator; + private readonly MemberExpressionFactory memberExpressionFactory; - public BindingPropertyResolvers(IBindingExpressionBuilder bindingParser, StaticCommandBindingCompiler staticCommandBindingCompiler, JavascriptTranslator javascriptTranslator, DotvvmConfiguration configuration) + public BindingPropertyResolvers(IBindingExpressionBuilder bindingParser, StaticCommandBindingCompiler staticCommandBindingCompiler, JavascriptTranslator javascriptTranslator, DotvvmConfiguration configuration, MemberExpressionFactory memberExpressionFactory) { this.configuration = configuration; this.bindingParser = bindingParser; this.staticCommandBindingCompiler = staticCommandBindingCompiler; this.javascriptTranslator = javascriptTranslator; + this.memberExpressionFactory = memberExpressionFactory; } public ActionFiltersBindingProperty GetActionFilters(ParsedExpressionBindingProperty parsedExpression) @@ -71,7 +73,7 @@ public Expression CompileToUpdateDelegate(ParsedExpressio { var valueParameter = Expression.Parameter(typeof(object), "value"); var body = BindingCompiler.ReplaceParameters(binding.Expression, dataContext); - body = ExpressionHelper.UpdateMember(body, valueParameter); + body = memberExpressionFactory.UpdateMember(body, valueParameter); if (body == null) { return null; diff --git a/src/DotVVM.Framework/Compilation/Binding/IExtensionsProvider.cs b/src/DotVVM.Framework/Compilation/Binding/IExtensionsProvider.cs new file mode 100644 index 0000000000..877ceb2a3a --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Binding/IExtensionsProvider.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace DotVVM.Framework.Compilation.Binding +{ + public interface IExtensionsProvider + { + IEnumerable GetExtensionMethods(); + } +} diff --git a/src/DotVVM.Framework/Compilation/Binding/MemberExpressionFactory.cs b/src/DotVVM.Framework/Compilation/Binding/MemberExpressionFactory.cs new file mode 100644 index 0000000000..7ffdec06f1 --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Binding/MemberExpressionFactory.cs @@ -0,0 +1,448 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.Binding; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Utils; +using Microsoft.CSharp.RuntimeBinder; +using Microsoft.Extensions.DependencyInjection; + +namespace DotVVM.Framework.Compilation.Binding +{ + public class MemberExpressionFactory + { + private readonly IExtensionsProvider extensionsProvider; + + public MemberExpressionFactory(IServiceProvider serviceProvider) + { + extensionsProvider = serviceProvider.GetService(); + if (extensionsProvider == null) + extensionsProvider = new DefaultExtensionsProvider(); + } + + public Expression GetMember(Expression target, string name, Type[] typeArguments = null, bool throwExceptions = true, bool onlyMemberTypes = false) + { + if (target is MethodGroupExpression) + throw new Exception("Can not access member on method group."); + + var type = target.Type; + if (type == typeof(UnknownTypeSentinel)) if (throwExceptions) throw new Exception($"Type of '{target}' could not be resolved."); else return null; + + var isStatic = target is StaticClassIdentifierExpression; + + var isGeneric = typeArguments != null && typeArguments.Length != 0; + var genericName = isGeneric ? $"{name}`{typeArguments.Length}" : name; + + if (!isGeneric && !onlyMemberTypes && typeof(DotvvmBindableObject).IsAssignableFrom(target.Type) && + GetDotvvmPropertyMember(target, name) is Expression result) return result; + + var members = type.GetAllMembers(BindingFlags.Public | (isStatic ? BindingFlags.Static : BindingFlags.Instance)) + .Where(m => ((isGeneric && m is TypeInfo) ? genericName : name) == m.Name) + .ToArray(); + + if (members.Length == 0) + { + // We did not find any match in regular methods => try extension methods + var extensions = extensionsProvider.GetExtensionMethods() + .Where(m => m.Name == name).ToArray(); + members = extensions; + + if (members.Length == 0 && throwExceptions) + throw new Exception($"Could not find { (isStatic ? "static" : "instance") } member { name } on type { type.FullName }."); + else if (members.Length == 0 && !throwExceptions) + return null; + } + if (members.Length == 1) + { + if (!(members[0] is TypeInfo) && onlyMemberTypes) { throw new Exception("Only type names are supported."); } + + var instance = isStatic ? null : target; + if (members[0] is PropertyInfo) + { + var property = members[0] as PropertyInfo; + return Expression.Property(instance, property); + } + else if (members[0] is FieldInfo) + { + var field = members[0] as FieldInfo; + return Expression.Field(instance, field); + } + else if (members[0] is TypeInfo) + { + var nonGenericType = (TypeInfo)members[0]; + return isGeneric + ? new StaticClassIdentifierExpression(nonGenericType.MakeGenericType(typeArguments)) + : new StaticClassIdentifierExpression(nonGenericType.UnderlyingSystemType); + } + } + return new MethodGroupExpression() { MethodName = name, Target = target, TypeArgs = typeArguments }; + } + + private Expression GetDotvvmPropertyMember(Expression target, string name) + { + var property = DotvvmProperty.ResolveProperty(target.Type, name); + if (property == null) return null; + + var field = property.DeclaringType.GetField(property.Name + "Property", BindingFlags.Static | BindingFlags.Public); + if (field == null) return null; + + return Expression.Convert( + Expression.Call(target, "GetValue", Type.EmptyTypes, + Expression.Field(null, field), + Expression.Constant(true) + ), + property.PropertyType + ); + } + + /// + /// Creates an expression that updates the member inside with a + /// new . + /// + /// + /// Should contain a call to the + /// method, it will be + /// replaced with a + /// call. + /// + public Expression UpdateMember(Expression node, Expression value) + { + if ((node.NodeType == ExpressionType.MemberAccess + && node is MemberExpression member + && member.Member is PropertyInfo property + && property.CanWrite) + || node.NodeType == ExpressionType.Parameter + || node.NodeType == ExpressionType.Index) + { + return Expression.Assign(node, Expression.Convert(value, node.Type)); + } + + var current = node; + while (current.NodeType == ExpressionType.Convert + && current is UnaryExpression unary) + { + current = unary.Operand; + } + + if (current.NodeType == ExpressionType.Call + && current is MethodCallExpression call + && call.Method.DeclaringType == typeof(DotvvmBindableObject) + && call.Method.Name == nameof(DotvvmBindableObject.GetValue) + && call.Arguments.Count == 2 + && call.Arguments[0].Type == typeof(DotvvmProperty) + && call.Arguments[1].Type == typeof(bool)) + { + var propertyArgument = call.Arguments[0]; + var setValue = typeof(DotvvmBindableObject) + .GetMethod(nameof(DotvvmBindableObject.SetValue), + new[] { typeof(DotvvmProperty), typeof(object) }); + return Expression.Call(call.Object, setValue, propertyArgument, value); + } + + return null; + } + + public Expression Call(Expression target, Expression[] arguments) + { + if (target is MethodGroupExpression) + { + return ((MethodGroupExpression)target).CreateMethodCall(arguments, this); + } + return Expression.Invoke(target, arguments); + } + + public Expression CallMethod(Expression target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) + { + // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) + var method = FindValidMethodOveloads(target, target.Type, name, flags, typeArguments, arguments, namedArgs); + + if (method.IsExtension) + { + // Change to a static call + var newArguments = new[] { target }.Concat(arguments); + return Expression.Call(method.Method, newArguments); + } + + return Expression.Call(target, method.Method, method.Arguments); + } + + public Expression CallMethod(Type target, BindingFlags flags, string name, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs = null) + { + // the following piece of code is nicer and more readable than method recognition done in roslyn, C# dynamic and also expression evaluator :) + var method = FindValidMethodOveloads(null, target, name, flags, typeArguments, arguments, namedArgs); + return Expression.Call(method.Method, method.Arguments); + } + + + private MethodRecognitionResult FindValidMethodOveloads(Expression target, Type type, string name, BindingFlags flags, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) + { + var methods = FindValidMethodOveloads(type.GetAllMembers(flags).OfType().Where(m => m.Name == name), typeArguments, arguments, namedArgs).ToList(); + + if (methods.Count == 1) return methods.FirstOrDefault(); + if (methods.Count == 0) + { + // We did not find any match in regular methods => try extension methods + if (target != null) + { + // Change to a static call + var newArguments = new[] { target }.Concat(arguments).ToArray(); + var extensions = FindValidMethodOveloads(extensionsProvider.GetExtensionMethods().OfType().Where(m => m.Name == name), typeArguments, newArguments, namedArgs) + .Select(method => { method.IsExtension = true; return method; }).ToList(); + + // We found an extension method + if (extensions.Count == 1) + return extensions.FirstOrDefault(); + + target = null; + methods = extensions; + arguments = newArguments; + } + + if (methods.Count == 0) + throw new InvalidOperationException($"Could not find method overload nor extension method that matched '{name}'."); + } + + // There are multiple method candidates + methods = methods.OrderBy(s => s.CastCount).ThenBy(s => s.AutomaticTypeArgCount).ToList(); + var method = methods.FirstOrDefault(); + var method2 = methods.Skip(1).FirstOrDefault(); + if (method.AutomaticTypeArgCount == method2.AutomaticTypeArgCount && method.CastCount == method2.CastCount) + { + // TODO: this behavior is not completed. Implement the same behavior as in roslyn. + throw new InvalidOperationException($"Found ambiguous overloads of method '{name}'."); + } + return method; + } + + private IEnumerable FindValidMethodOveloads(IEnumerable methods, Type[] typeArguments, Expression[] arguments, IDictionary namedArgs) + => from m in methods + let r = TryCallMethod(m, typeArguments, arguments, namedArgs) + where r != null + orderby r.CastCount descending, r.AutomaticTypeArgCount + select r; + + class MethodRecognitionResult + { + public int AutomaticTypeArgCount { get; set; } + public int CastCount { get; set; } + public Expression[] Arguments { get; set; } + public MethodInfo Method { get; set; } + public bool IsExtension { get; set; } + } + + private MethodRecognitionResult TryCallMethod(MethodInfo method, Type[] typeArguments, Expression[] positionalArguments, IDictionary namedArguments) + { + var parameters = method.GetParameters(); + + int castCount = 0; + if (parameters.Length < positionalArguments.Length) return null; + var args = new Expression[parameters.Length]; + Array.Copy(positionalArguments, args, positionalArguments.Length); + int namedArgCount = 0; + for (int i = positionalArguments.Length; i < args.Length; i++) + { + if (namedArguments?.ContainsKey(parameters[i].Name) == true) + { + args[i] = namedArguments[parameters[i].Name]; + namedArgCount++; + } + else if (parameters[i].HasDefaultValue) + { + castCount++; + args[i] = Expression.Constant(parameters[i].DefaultValue, parameters[i].ParameterType); + } + else return null; + } + + // some named arguments were not used + if (namedArguments != null && namedArgCount != namedArguments.Count) return null; + + int automaticTypeArgs = 0; + // resolve generic parameters + if (method.ContainsGenericParameters) + { + var genericArguments = method.GetGenericArguments(); + var typeArgs = new Type[genericArguments.Length]; + if (typeArguments != null) + { + if (typeArguments.Length > typeArgs.Length) return null; + Array.Copy(typeArguments, typeArgs, typeArgs.Length); + } + for (int genericArgumentPosition = 0; genericArgumentPosition < typeArgs.Length; genericArgumentPosition++) + { + if (typeArgs[genericArgumentPosition] == null) + { + // try to resolve from arguments + var argType = GetGenericParameterType(genericArguments[genericArgumentPosition], parameters.Select(s => s.ParameterType).ToArray(), args.Select(s => s.Type).ToArray()); + automaticTypeArgs++; + if (argType != null) typeArgs[genericArgumentPosition] = argType; + else return null; + } + } + method = method.MakeGenericMethod(typeArgs); + parameters = method.GetParameters(); + } + else if (typeArguments != null) return null; + + // cast arguments + for (int i = 0; i < args.Length; i++) + { + var casted = TypeConversion.ImplicitConversion(args[i], parameters[i].ParameterType); + if (casted == null) return null; + if (casted != args[i]) + { + castCount++; + args[i] = casted; + } + } + + return new MethodRecognitionResult { + CastCount = castCount, + AutomaticTypeArgCount = automaticTypeArgs, + Method = method, + Arguments = args + }; + } + + private Type GetGenericParameterType(Type genericArg, Type[] searchedGenericTypes, Type[] expressionTypes) + { + for (var i = 0; i < searchedGenericTypes.Length; i++) + { + if (expressionTypes.Length <= i) return null; + var sgt = searchedGenericTypes[i]; + if (sgt == genericArg) + { + return expressionTypes[i]; + } + if (sgt.IsArray) + { + var elementType = sgt.GetElementType(); + var expressionElementType = expressionTypes[i].GetElementType(); + if (elementType == genericArg) + return expressionElementType; + else + return GetGenericParameterType(genericArg, searchedGenericTypes[i].GetGenericArguments(), expressionTypes[i].GetGenericArguments()); + } + else if (sgt.IsGenericType) + { + var value = GetGenericParameterType(genericArg, sgt.GetGenericArguments(), expressionTypes[i].GetGenericArguments()); + if (value is Type) return value; + } + } + return null; + } + + public Expression EqualsMethod(Expression left, Expression right) + { + Expression equatable = null; + Expression theOther = null; + if (typeof(IEquatable<>).IsAssignableFrom(left.Type)) + { + equatable = left; + theOther = right; + } + else if (typeof(IEquatable<>).IsAssignableFrom(right.Type)) + { + equatable = right; + theOther = left; + } + + if (equatable != null) + { + var m = CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { theOther }); + if (m != null) return m; + } + + if (left.Type.GetTypeInfo().IsValueType) + { + equatable = left; + theOther = right; + } + else if (left.Type.GetTypeInfo().IsValueType) + { + equatable = right; + theOther = left; + } + + if (equatable != null) + { + theOther = TypeConversion.ImplicitConversion(theOther, equatable.Type); + if (theOther != null) return Expression.Equal(equatable, theOther); + } + + return CallMethod(left, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Equals", null, new[] { right }); + } + + public Expression CompareMethod(Expression left, Expression right) + { + Type compareType = typeof(object); + Expression equatable = null; + Expression theOther = null; + if (typeof(IComparable<>).IsAssignableFrom(left.Type)) + { + equatable = left; + theOther = right; + } + else if (typeof(IComparable<>).IsAssignableFrom(right.Type)) + { + equatable = right; + theOther = left; + } + else if (typeof(IComparable).IsAssignableFrom(left.Type)) + { + equatable = left; + theOther = right; + } + else if (typeof(IComparable).IsAssignableFrom(right.Type)) + { + equatable = right; + theOther = left; + } + + if (equatable != null) + { + return CallMethod(equatable, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, "Compare", null, new[] { theOther }); + } + throw new NotSupportedException("IComparable is not implemented on any of specified types"); + } + + public Expression GetUnaryOperator(Expression expr, ExpressionType operation) + { + var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.UnaryOperation( + CSharpBinderFlags.None, operation, typeof(object), ExpressionHelper.GetBinderArguments(1)); + return ExpressionHelper.ApplyBinder(binder, true, expr); + } + + public Expression GetBinaryOperator(Expression left, Expression right, ExpressionType operation) + { + if (operation == ExpressionType.Coalesce) return Expression.Coalesce(left, right); + if (operation == ExpressionType.Assign) + { + return Expression.Assign(left, TypeConversion.ImplicitConversion(right, left.Type, true, true)); + } + + // TODO: type conversions + if (operation == ExpressionType.AndAlso) return Expression.AndAlso(left, right); + else if (operation == ExpressionType.OrElse) return Expression.OrElse(left, right); + + var binder = (DynamicMetaObjectBinder)Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation( + CSharpBinderFlags.None, operation, typeof(object), ExpressionHelper.GetBinderArguments(2)); + var result = ExpressionHelper.ApplyBinder(binder, false, left, right); + if (result != null) return result; + if (operation == ExpressionType.Equal) return EqualsMethod(left, right); + if (operation == ExpressionType.NotEqual) return Expression.Not(EqualsMethod(left, right)); + + // lift the operator + if (left.Type.IsNullable() || right.Type.IsNullable()) + return GetBinaryOperator(left.UnwrapNullable(), right.UnwrapNullable(), operation); + + throw new Exception($"could not apply { operation } binary operator to { left } and { right }"); + // TODO: comparison operators + } + } +} diff --git a/src/DotVVM.Framework/Compilation/Binding/MethodGroupExpression.cs b/src/DotVVM.Framework/Compilation/Binding/MethodGroupExpression.cs index 8dca9c2793..9055ec7213 100644 --- a/src/DotVVM.Framework/Compilation/Binding/MethodGroupExpression.cs +++ b/src/DotVVM.Framework/Compilation/Binding/MethodGroupExpression.cs @@ -72,16 +72,16 @@ public Expression CreateDelegateExpression() return Expression.Call(CreateDelegateMethodInfo, Expression.Constant(delegateType), Target, Expression.Constant(methodInfo)) .Apply(e => Expression.Convert(e, delegateType)); } - public Expression CreateMethodCall(IEnumerable args) + public Expression CreateMethodCall(IEnumerable args, MemberExpressionFactory memberExpressionFactory) { var argsArray = args.ToArray(); if (IsStatic) { - return ExpressionHelper.CallMethod((Target as StaticClassIdentifierExpression).Type, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy, MethodName, TypeArgs, argsArray); + return memberExpressionFactory.CallMethod((Target as StaticClassIdentifierExpression).Type, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy, MethodName, TypeArgs, argsArray); } else { - return ExpressionHelper.CallMethod(Target, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy, MethodName, TypeArgs, argsArray); + return memberExpressionFactory.CallMethod(Target, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy, MethodName, TypeArgs, argsArray); } } diff --git a/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs b/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs index 5300b64ceb..13f421f6c7 100644 --- a/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs +++ b/src/DotVVM.Framework/Compilation/Binding/TypeRegistry.cs @@ -127,10 +127,5 @@ public static Expression CreateStatic(Type type) ImmutableList>.Empty .Add(type => CreateStatic(compiledAssemblyCache.FindType(type + (assemblyName != null ? $", {assemblyName}" : "")))) ); - - public static IEnumerable GetRegisteredTypesForExtensionMethodsLookup() - { - yield return typeof(Enumerable); - } } } diff --git a/src/DotVVM.Framework/Compilation/Binding/UnknownTypeSentinel.cs b/src/DotVVM.Framework/Compilation/Binding/UnknownTypeSentinel.cs new file mode 100644 index 0000000000..8a7af3a60c --- /dev/null +++ b/src/DotVVM.Framework/Compilation/Binding/UnknownTypeSentinel.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotVVM.Framework.Compilation.Binding +{ + internal sealed class UnknownTypeSentinel + { + } +} diff --git a/src/DotVVM.Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs b/src/DotVVM.Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs index a257960cf7..3c2b880f39 100644 --- a/src/DotVVM.Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs +++ b/src/DotVVM.Framework/Compilation/ControlTree/DefaultControlTreeResolver.cs @@ -43,7 +43,7 @@ protected override IDataContextStack CreateDataContextTypeStack(ITypeDescriptor? { return DataContextStack.Create( - ResolvedTypeDescriptor.ToSystemType(viewModelType) ?? typeof(ExpressionHelper.UnknownTypeSentinel), + ResolvedTypeDescriptor.ToSystemType(viewModelType) ?? typeof(UnknownTypeSentinel), parentDataContextStack as DataContextStack, namespaceImports, extensionParameters); } diff --git a/src/DotVVM.Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs b/src/DotVVM.Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs index 9329e5e6d1..89ce337982 100644 --- a/src/DotVVM.Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs +++ b/src/DotVVM.Framework/Compilation/ControlTree/Resolved/ResolvedTreeBuilder.cs @@ -17,11 +17,13 @@ public class ResolvedTreeBuilder : IAbstractTreeBuilder { private readonly BindingCompilationService bindingService; private readonly CompiledAssemblyCache compiledAssemblyCache; + private readonly MemberExpressionFactory memberExpressionFactory; - public ResolvedTreeBuilder(BindingCompilationService bindingService, CompiledAssemblyCache compiledAssemblyCache) + public ResolvedTreeBuilder(BindingCompilationService bindingService, CompiledAssemblyCache compiledAssemblyCache, MemberExpressionFactory memberExpressionFactory) { this.bindingService = bindingService; this.compiledAssemblyCache = compiledAssemblyCache; + this.memberExpressionFactory = memberExpressionFactory; } public IAbstractTreeRoot BuildTreeRoot(IControlTreeResolver controlTreeResolver, IControlResolverMetadata metadata, DothtmlRootNode node, IDataContextStack dataContext, IReadOnlyDictionary> directives) @@ -162,7 +164,7 @@ public IAbstractBaseTypeDirective BuildBaseTypeDirective(DothtmlDirectiveNode di registry = TypeRegistry.DirectivesDefault(compiledAssemblyCache); } - var visitor = new ExpressionBuildingVisitor(registry) { + var visitor = new ExpressionBuildingVisitor(registry, memberExpressionFactory) { ResolveOnlyTypeName = true, Scope = null }; diff --git a/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index 1b2f3628ae..8a88836738 100644 --- a/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -75,6 +75,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); + services.TryAddScoped(); services.TryAddSingleton(s => DotvvmConfiguration.CreateDefault(s)); services.TryAddSingleton(s => s.GetRequiredService().Markup); services.TryAddSingleton(s => s.GetRequiredService().Resources); diff --git a/src/DotVVM.Framework/Utils/ReflectionUtils.cs b/src/DotVVM.Framework/Utils/ReflectionUtils.cs index c220ef24a2..6a0af27375 100644 --- a/src/DotVVM.Framework/Utils/ReflectionUtils.cs +++ b/src/DotVVM.Framework/Utils/ReflectionUtils.cs @@ -64,13 +64,6 @@ public static IEnumerable GetAllMembers(this Type type, BindingFlags return type.GetMembers(flags); } - public static IEnumerable GetAllExtensions(this Type type, BindingFlags flags = BindingFlags.Public | BindingFlags.Static) - { - foreach (var registeredType in TypeRegistry.GetRegisteredTypesForExtensionMethodsLookup()) - foreach (var method in registeredType.GetMethods(flags)) - yield return method; - } - /// /// Gets filesystem path of assembly CodeBase /// http://stackoverflow.com/questions/52797/how-do-i-get-the-path-of-the-assembly-the-code-is-in From 23091ff4ffc8cbc3b53853912a8735c1faf94f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 19 Feb 2021 17:39:04 +0100 Subject: [PATCH 10/15] Fix issue when adding type to ExtensionsProvider --- .../Compilation/Binding/DefaultExtensionsProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs index 5f9e000098..18c014cb09 100644 --- a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs +++ b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs @@ -19,7 +19,7 @@ public DefaultExtensionsProvider() protected void AddTypeForExtensionsLookup(Type type) { - typesLookup.Add(typeof(Enumerable)); + typesLookup.Add(type); } public virtual IEnumerable GetExtensionMethods() From af5f571bbc9a647c21e000a2b09336cffb56c7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Fri, 19 Feb 2021 17:39:56 +0100 Subject: [PATCH 11/15] Added test showcasing custom extension method --- .../Binding/CustomExtensionMethodTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs diff --git a/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs new file mode 100644 index 0000000000..9f803cb1cf --- /dev/null +++ b/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using DotVVM.Framework.Compilation.Binding; +using DotVVM.Framework.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Framework.Tests.Common.Binding +{ + [TestClass] + public class CustomExtensionMethodTests + { + private DotvvmConfiguration configuration; + private MemberExpressionFactory memberExpressionFactory; + + [TestInitialize] + public void INIT() + { + this.configuration = DotvvmTestHelper.CreateConfiguration(services => services.AddScoped()); + this.memberExpressionFactory = configuration.ServiceProvider.GetRequiredService(); + } + + [TestMethod] + public void Call_FindCustomExtensionMethod() + { + var target = new MethodGroupExpression() + { + MethodName = nameof(TestExtensions.Increment), + Target = Expression.Constant(11) + }; + + var expression = memberExpressionFactory.Call(target, Array.Empty()); + var result = Expression.Lambda>(expression).Compile().Invoke(); + Assert.AreEqual(12, result); + } + } + + static class TestExtensions + { + public static int Increment(this int number) + => ++number; + } + + class TestExtensionsProvider : DefaultExtensionsProvider + { + public TestExtensionsProvider() + { + AddTypeForExtensionsLookup(typeof(TestExtensions)); + } + } +} From 1c74da1bd3ae94a0df0309d56a1d085e6bbb24f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 22 Feb 2021 14:45:04 +0100 Subject: [PATCH 12/15] Renamed INIT methods to Init --- .../Binding/BindingCompilationTests.cs | 2 +- .../Binding/CommandResolverTests.cs | 2 +- .../Binding/CustomExtensionMethodTests.cs | 2 +- .../Binding/ExpressionHelperTests.cs | 2 +- .../Binding/GenericPropertyResolverTests.cs | 2 +- .../Binding/JavascriptCompilationTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs index af525e0d6b..591522a6fc 100755 --- a/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/BindingCompilationTests.cs @@ -28,7 +28,7 @@ public class BindingCompilationTests private BindingCompilationService bindingService; [TestInitialize] - public void INIT() + public void Init() { this.configuration = DotvvmTestHelper.DefaultConfig; this.bindingService = configuration.ServiceProvider.GetRequiredService(); diff --git a/src/DotVVM.Framework.Tests.Common/Binding/CommandResolverTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/CommandResolverTests.cs index b485631e3b..01abc3dc4c 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/CommandResolverTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/CommandResolverTests.cs @@ -24,7 +24,7 @@ public class CommandResolverTests private BindingCompilationService bindingService; [TestInitialize] - public void INIT() + public void Init() { this.configuration = DotvvmTestHelper.DefaultConfig; this.bindingService = configuration.ServiceProvider.GetRequiredService(); diff --git a/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs index 9f803cb1cf..9d9474e6f1 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/CustomExtensionMethodTests.cs @@ -16,7 +16,7 @@ public class CustomExtensionMethodTests private MemberExpressionFactory memberExpressionFactory; [TestInitialize] - public void INIT() + public void Init() { this.configuration = DotvvmTestHelper.CreateConfiguration(services => services.AddScoped()); this.memberExpressionFactory = configuration.ServiceProvider.GetRequiredService(); diff --git a/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs index e141d34b93..2c27949dff 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/ExpressionHelperTests.cs @@ -21,7 +21,7 @@ public class ExpressionHelperTests private MemberExpressionFactory memberExpressionFactory; [TestInitialize] - public void INIT() + public void Init() { var configuration = DotvvmTestHelper.CreateConfiguration(); memberExpressionFactory = configuration.ServiceProvider.GetRequiredService(); diff --git a/src/DotVVM.Framework.Tests.Common/Binding/GenericPropertyResolverTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/GenericPropertyResolverTests.cs index 8f5cc0a8d1..c1fe25b7c1 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/GenericPropertyResolverTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/GenericPropertyResolverTests.cs @@ -16,7 +16,7 @@ public class GenericPropertyResolverTests private BindingCompilationService bindingService; [TestInitialize] - public void INIT() + public void Init() { this.configuration = DotvvmTestHelper.DefaultConfig; this.bindingService = configuration.ServiceProvider.GetRequiredService(); diff --git a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs index fbb68f06c4..af865fd74f 100644 --- a/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs +++ b/src/DotVVM.Framework.Tests.Common/Binding/JavascriptCompilationTests.cs @@ -28,7 +28,7 @@ public class JavascriptCompilationTests private BindingCompilationService bindingService; [TestInitialize] - public void INIT() + public void Init() { this.configuration = DotvvmTestHelper.CreateConfiguration(); configuration.RegisterApiClient(typeof(TestApiClient), "http://server/api", "./apiscript.js", "_testApi"); From 84de21c0113f3f7ee2bf24e284f4c806c9bf5ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 22 Feb 2021 14:55:02 +0100 Subject: [PATCH 13/15] Added extensions method cache --- .../Compilation/Binding/DefaultExtensionsProvider.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs index 18c014cb09..586683b63a 100644 --- a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs +++ b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -10,23 +11,26 @@ namespace DotVVM.Framework.Compilation.Binding public class DefaultExtensionsProvider : IExtensionsProvider { private readonly List typesLookup; + private readonly List methodsLookup; public DefaultExtensionsProvider() { typesLookup = new List(); + methodsLookup = new List(); AddTypeForExtensionsLookup(typeof(Enumerable)); } protected void AddTypeForExtensionsLookup(Type type) { + foreach (var method in type.GetMethods().Where(m => m.GetCustomAttribute(typeof(ExtensionAttribute)) != null)) + methodsLookup.Add(method); + typesLookup.Add(type); } public virtual IEnumerable GetExtensionMethods() { - foreach (var registeredType in typesLookup) - foreach (var method in registeredType.GetMethods(BindingFlags.Public | BindingFlags.Static)) - yield return method; + return methodsLookup; } } } From 8d9baf87d7b1bf5490bf0e7ca373b16b624589eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 22 Feb 2021 15:00:14 +0100 Subject: [PATCH 14/15] Registered MemberExpressionFactory as a singleton service Avoid reconstruction of extension method cache --- .../DependencyInjection/DotVVMServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs b/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs index 8a88836738..9c70fca0f2 100644 --- a/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs +++ b/src/DotVVM.Framework/DependencyInjection/DotVVMServiceCollectionExtensions.cs @@ -75,7 +75,7 @@ public static IServiceCollection RegisterDotVVMServices(IServiceCollection servi services.TryAddScoped(); services.TryAddScoped(); services.TryAddScoped(); - services.TryAddScoped(); + services.TryAddSingleton(); services.TryAddSingleton(s => DotvvmConfiguration.CreateDefault(s)); services.TryAddSingleton(s => s.GetRequiredService().Markup); services.TryAddSingleton(s => s.GetRequiredService().Resources); From 72b0f26f6520a83182097fb6e3ab32ddbf17e5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20=C4=8Ci=C5=BEm=C3=A1rik?= Date: Mon, 22 Feb 2021 15:17:17 +0100 Subject: [PATCH 15/15] Added missing BindingFlags --- .../Compilation/Binding/DefaultExtensionsProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs index 586683b63a..155844a41d 100644 --- a/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs +++ b/src/DotVVM.Framework/Compilation/Binding/DefaultExtensionsProvider.cs @@ -22,7 +22,7 @@ public DefaultExtensionsProvider() protected void AddTypeForExtensionsLookup(Type type) { - foreach (var method in type.GetMethods().Where(m => m.GetCustomAttribute(typeof(ExtensionAttribute)) != null)) + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(m => m.GetCustomAttribute(typeof(ExtensionAttribute)) != null)) methodsLookup.Add(method); typesLookup.Add(type);