diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index c814461f36..1c928730e2 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -18,6 +18,9 @@ namespace Umbraco.Core /// public static class ObjectExtensions { + private static readonly ConcurrentDictionary> ToObjectTypes + = new ConcurrentDictionary>(); + //private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>(); /// @@ -503,34 +506,34 @@ namespace Umbraco.Core return new Dictionary(); } - private static readonly ConcurrentDictionary> ToObjectTypes - = new ConcurrentDictionary>(); - /// /// Converts an object's properties into a dictionary. /// /// The object to convert. + /// A property namer function. /// A dictionary containing each properties. - public static Dictionary ToObjectDictionary(T obj) + public static Dictionary ToObjectDictionary(T obj, Func namer = null) { - // fixme refactor this! - var d = new Dictionary(); - if (obj == null) return d; - foreach (var p in obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)) + if (obj == null) return new Dictionary(); + + string DefaultNamer(PropertyInfo property) { - var jsonProperty = p.GetCustomAttribute(); - var name = jsonProperty != null ? jsonProperty.PropertyName : p.Name; - d[name] = p.GetValue(obj); + var jsonProperty = property.GetCustomAttribute(); + return jsonProperty?.PropertyName ?? property.Name; } - return d; var t = obj.GetType(); + if (namer == null) namer = DefaultNamer; + if (!ToObjectTypes.TryGetValue(t, out var properties)) { - ToObjectTypes[t] = properties = t - .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) - .ToDictionary(x => x.Name, x => (object) ReflectionUtilities.EmitPropertyGetter(x)); + properties = new Dictionary(); + + foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)) + properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); + + ToObjectTypes[t] = properties; } return properties.ToDictionary(x => x.Key, x => ((Func) x.Value)(obj)); diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs index 165780f606..0479fd0e88 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditor.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.PropertyEditors /// Used to create the actual configuration dictionary from the database value. public virtual object ParseConfiguration(string configurationJson) => string.IsNullOrWhiteSpace(configurationJson) - ? null + ? new Dictionary() : JsonConvert.DeserializeObject>(configurationJson); /// diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs index bc0f371c35..9bb2fac768 100644 --- a/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationEditorOfTConfiguration.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Concurrent; using System.Linq; using System.Collections.Generic; using System.Reflection; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; namespace Umbraco.Core.PropertyEditors @@ -13,6 +15,10 @@ namespace Umbraco.Core.PropertyEditors public abstract class ConfigurationEditor : ConfigurationEditor where TConfiguration : new() { + // ReSharper disable once StaticMemberInGenericType + private static Dictionary _fromObjectTypes + = new Dictionary(); + /// /// Initializes a new instance of the class. /// @@ -96,7 +102,7 @@ namespace Umbraco.Core.PropertyEditors { try { - if (string.IsNullOrWhiteSpace(configuration)) return default; + if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration(); return JsonConvert.DeserializeObject(configuration); } catch (Exception e) @@ -118,7 +124,21 @@ namespace Umbraco.Core.PropertyEditors /// The current configuration object. public virtual TConfiguration FromEditor(Dictionary editorValue, TConfiguration configuration) { - return ToObject(editorValue); + // note - editorValue contains a mix of Clr types (string, int...) and JToken + // turning everything back into a JToken... might not be fastest but is simplest + // for now + + var o = new JObject(); + + foreach (var field in Fields) + { + // only fields - ignore json property + // fixme should we deal with jsonProperty anyways? + if (editorValue.TryGetValue(field.Key, out var value) && value != null) + o[field.PropertyName] = value is JToken jtoken ? jtoken : JToken.FromObject(value); + } + + return o.ToObject(); } /// @@ -134,7 +154,18 @@ namespace Umbraco.Core.PropertyEditors /// The configuration. public virtual Dictionary ToEditor(TConfiguration configuration) { - var dictionary = ObjectExtensions.ToObjectDictionary(configuration); + string FieldNamer(PropertyInfo property) + { + // field first + var field = property.GetCustomAttribute(); + if (field != null) return field.Key; + + // then, json property + var jsonProperty = property.GetCustomAttribute(); + return jsonProperty?.PropertyName ?? property.Name; + } + + var dictionary = ObjectExtensions.ToObjectDictionary(configuration, FieldNamer); if (configuration is ConfigurationWithAdditionalData withAdditionalData) foreach (var kv in withAdditionalData.GetAdditionalValues()) @@ -149,27 +180,49 @@ namespace Umbraco.Core.PropertyEditors /// The type of the object. /// The source dictionary. /// The object corresponding to the dictionary. - protected T ToObject(Dictionary source) + protected T FromObjectDictionary(Dictionary source) // fixme KILL - NOT USED ANYMORE where T : new() { - // fixme cache! see also ToObject in ObjectExtensions - // this is probably very bad, must REAFACTOR! the property setter of course cannot work like this! - //var properties = TypeHelper.CachedDiscoverableProperties(typeof(T)) - // .ToDictionary(x => x.Name, x => (Type: x.PropertyType, Set: ReflectionUtilities.EmitPropertySetter(x))); - var properties = TypeHelper.CachedDiscoverableProperties(typeof(T)) - .ToDictionary(x => x.Name, x => (Type: x.PropertyType, Infos: x)); + // this needs to be here (and not in ObjectExtensions) because it is based on fields + + var t = typeof(T); + + if (_fromObjectTypes == null) + { + var p = t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + var fromObjectTypes = new Dictionary(); + + foreach (var field in Fields) + { + var fp = p.FirstOrDefault(x => x.Name == field.PropertyName); + if (fp == null) continue; + + fromObjectTypes[field.Key] = (fp.PropertyType, ReflectionUtilities.EmitPropertySetter(fp)); + } + + _fromObjectTypes = fromObjectTypes; + } var obj = new T(); foreach (var field in Fields) { - if (!properties.TryGetValue(field.PropertyName, out var property)) continue; + if (!_fromObjectTypes.TryGetValue(field.Key, out var ps)) continue; if (!source.TryGetValue(field.Key, out var value)) continue; - // fixme if value is null? is this what we want? - if (!value.GetType().IsInstanceOfType(property.Type)) - throw new Exception(); - //property.Set(obj, value); - property.Infos.SetValue(obj, value); + + if (ps.PropertyType.IsValueType) + { + if (value == null) + throw new InvalidCastException($"Cannot cast null value to {ps.PropertyType.Name}."); + } + else + { + // ReSharper disable once UseMethodIsInstanceOfType + if (!ps.PropertyType.IsAssignableFrom(value.GetType())) + throw new InvalidCastException($"Cannot cast value of type {value.GetType()} to {ps.PropertyType.Name}."); + } + + ((Action) ps.Setter)(obj, value); } return obj; diff --git a/src/Umbraco.Core/ReflectionUtilities-Unused.cs b/src/Umbraco.Core/ReflectionUtilities-Unused.cs new file mode 100644 index 0000000000..53317f7101 --- /dev/null +++ b/src/Umbraco.Core/ReflectionUtilities-Unused.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; + +namespace Umbraco.Core +{ + /// + /// Provides utilities to simplify reflection. + /// + public static partial class ReflectionUtilities + { + // the code below should NOT be used + // + // keeping all this around as a reference for now - building expressions into dynamic assemblies, + // the resulting methods are fast (as fast as IL-generated methods) whereas the standard compiled + // expressions are slowish - alas, the compiled methods do not have access to eg private members + // and anything that would violate access control - we're not using it anymore - still used in a + // benchmark + + internal static Func GetCtor() + { + var type = typeof (TInstance); + var type0 = typeof (TArg0); + + // get the constructor infos + var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, new[] { type0 }, null); + + if (ctor == null) + throw new InvalidOperationException($"Could not find constructor {type}.ctor({type0})."); + + var exprArgs = ctor.GetParameters().Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToArray(); + // ReSharper disable once CoVariantArrayConversion + var exprNew = Expression.New(ctor, exprArgs); + var expr = Expression.Lambda>(exprNew, exprArgs); + return CompileToDelegate(expr); + } + + internal static Func GetCtor() + { + var type = typeof (TInstance); + var type0 = typeof (TArg0); + var type1 = typeof (TArg1); + + // get the constructor infos + var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, new[] { type0, type1 }, null); + + if (ctor == null) + throw new InvalidOperationException($"Could not find constructor {type}.ctor({type0}, {type1})."); + + var exprArgs = ctor.GetParameters().Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToArray(); + // ReSharper disable once CoVariantArrayConversion + var exprNew = Expression.New(ctor, exprArgs); + var expr = Expression.Lambda>(exprNew, exprArgs); + return CompileToDelegate(expr); + } + + internal static TMethod GetMethod(MethodInfo method) + { + var type = method.DeclaringType; + + GetMethodParms(out var parameterTypes, out var returnType); + return GetStaticMethod(method, method.Name, type, parameterTypes, returnType); + } + + internal static TMethod GetMethod(MethodInfo method) + { + var type = method.DeclaringType; + + GetMethodParms(out var parameterTypes, out var returnType); + return GetMethod(method, method.Name, type, parameterTypes, returnType); + } + + internal static TMethod GetMethod(string methodName) + { + var type = typeof (TInstance); + + GetMethodParms(out var parameterTypes, out var returnType); + + var method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, + null, parameterTypes, null); + + return GetMethod(method, methodName, type, parameterTypes, returnType); + } + + private static void GetMethodParms(out Type[] parameterTypes, out Type returnType) + { + var typeM = typeof (TMethod); + var typeList = new List(); + returnType = typeof (void); + + if (!typeof (MulticastDelegate).IsAssignableFrom(typeM) || typeM == typeof (MulticastDelegate)) + throw new InvalidOperationException("Invalid TMethod, must be a Func or Action."); + + var typeName = typeM.FullName; + if (typeName == null) + throw new InvalidOperationException($"Could not get {typeM} type name."); + if (typeName.StartsWith("System.Func`")) + { + var i = 0; + while (i < typeM.GenericTypeArguments.Length - 1) + typeList.Add(typeM.GenericTypeArguments[i++]); + returnType = typeM.GenericTypeArguments[i]; + } + else if (typeName.StartsWith("System.Action`")) + { + var i = 0; + while (i < typeM.GenericTypeArguments.Length) + typeList.Add(typeM.GenericTypeArguments[i++]); + } + else if (typeName == "System.Action") + { + // no args + } + else + throw new InvalidOperationException(typeName); + + parameterTypes = typeList.ToArray(); + } + + private static void GetMethodParms(out Type[] parameterTypes, out Type returnType) + { + var type = typeof (TInstance); + + var typeM = typeof (TMethod); + var typeList = new List(); + returnType = typeof (void); + + if (!typeof (MulticastDelegate).IsAssignableFrom(typeM) || typeM == typeof (MulticastDelegate)) + throw new InvalidOperationException("Invalid TMethod, must be a Func or Action."); + + var typeName = typeM.FullName; + if (typeName == null) + throw new InvalidOperationException($"Could not get {typeM} type name."); + if (!typeM.IsGenericType) + throw new InvalidOperationException($"Type {typeName} is not generic."); + if (typeM.GenericTypeArguments[0] != type) + throw new InvalidOperationException($"Invalid type {typeName}, the first generic argument must be {type.FullName}."); + if (typeName.StartsWith("System.Func`")) + { + var i = 1; + while (i < typeM.GenericTypeArguments.Length - 1) + typeList.Add(typeM.GenericTypeArguments[i++]); + returnType = typeM.GenericTypeArguments[i]; + } + else if (typeName.StartsWith("System.Action`")) + { + var i = 1; + while (i < typeM.GenericTypeArguments.Length) + typeList.Add(typeM.GenericTypeArguments[i++]); + } + else + throw new InvalidOperationException(typeName); + + parameterTypes = typeList.ToArray(); + } + + private static TMethod GetStaticMethod(MethodInfo method, string methodName, Type type, Type[] parameterTypes, Type returnType) + { + if (method == null || method.ReturnType != returnType) + throw new InvalidOperationException($"Could not find static method {type}.{methodName}({string.Join(",", parameterTypes.Select(x => x.ToString()))}) : {returnType}"); + + var e = new List(); + foreach (var p in method.GetParameters()) + e.Add(Expression.Parameter(p.ParameterType, p.Name)); + var exprCallArgs = e.ToArray(); + var exprLambdaArgs = exprCallArgs; + + // ReSharper disable once CoVariantArrayConversion + var exprCall = Expression.Call(method, exprCallArgs); + var expr = Expression.Lambda(exprCall, exprLambdaArgs); + return CompileToDelegate(expr); + } + + private static TMethod GetMethod(MethodInfo method, string methodName, Type type, Type[] parameterTypes, Type returnType) + { + if (method == null || method.ReturnType != returnType) + throw new InvalidOperationException($"Could not find method {type}.{methodName}({string.Join(",", parameterTypes.Select(x => x.ToString()))}) : {returnType}"); + + var e = new List(); + foreach (var p in method.GetParameters()) + e.Add(Expression.Parameter(p.ParameterType, p.Name)); + var exprCallArgs = e.ToArray(); + + var exprThis = Expression.Parameter(type, "this"); + e.Insert(0, exprThis); + var exprLambdaArgs = e.ToArray(); + + // ReSharper disable once CoVariantArrayConversion + var exprCall = Expression.Call(exprThis, method, exprCallArgs); + var expr = Expression.Lambda(exprCall, exprLambdaArgs); + return expr.Compile(); + } + + internal const AssemblyBuilderAccess DefaultAssemblyBuilderAccess = AssemblyBuilderAccess.Run; + internal const AssemblyBuilderAccess NoAssembly = 0; + + internal static TLambda Compile(Expression expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + return access == NoAssembly + ? expr.Compile() + : CompileToDelegate(expr, access); + } + + internal static Action CompileToDelegate(Expression expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static); // CompileToMethod requires a static method + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Action CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (void), new[] { typeof (T1) }); + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Action CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (void), new[] { typeof (T1), typeof (T2) }); + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Action CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (void), new[] { typeof (T1), typeof (T2), typeof (T3) }); + + expr.CompileToMethod(builder); + + return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, + typeof (TResult), Array.Empty()); // CompileToMethod requires a static method + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (TResult), new[] { typeof (T1) }); + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (TResult), new[] { typeof (T1), typeof (T2) }); + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + typeof (TResult), new[] { typeof (T1), typeof (T2), typeof (T3) }); + + expr.CompileToMethod(builder); + + return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static TMethod CompileToDelegate(Expression expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var typeBuilder = CreateTypeBuilder(access); + + GetMethodParms(out var parameterTypes, out var returnType); + + var builder = typeBuilder.DefineMethod("Method", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + returnType, parameterTypes); + + expr.CompileToMethod(builder); + + return (TMethod) (object) Delegate.CreateDelegate(typeof (TMethod), typeBuilder.CreateType().GetMethod("Method")); + } + + internal static TMethod[] CompileToDelegates(params Expression[] exprs) + => CompileToDelegates(AssemblyBuilderAccess.RunAndCollect, exprs); + + internal static TMethod[] CompileToDelegates(AssemblyBuilderAccess access, params Expression[] exprs) + { + var typeBuilder = CreateTypeBuilder(access); + + GetMethodParms(out var parameterTypes, out var returnType); + + var i = 0; + foreach (var expr in exprs) + { + var builder = typeBuilder.DefineMethod($"Method_{i++}", + MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method + returnType, parameterTypes); + + expr.CompileToMethod(builder); + } + + var type = typeBuilder.CreateType(); + + var methods = new TMethod[exprs.Length]; + for (i = 0; i < exprs.Length; i++) + methods[i] = (TMethod) (object) Delegate.CreateDelegate(typeof (TMethod), type.GetMethod($"Method_{i++}")); + return methods; + } + + private static TypeBuilder CreateTypeBuilder(AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) + { + var assemblyName = new AssemblyName("Umbraco.Core.DynamicAssemblies." + Guid.NewGuid().ToString("N")); + var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, access); + + var module = (access & AssemblyBuilderAccess.Save) > 0 + ? assembly.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll") + : assembly.DefineDynamicModule(assemblyName.Name); // has to be transient + + return module.DefineType("Class", TypeAttributes.Public | TypeAttributes.Abstract); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/ReflectionUtilities.cs b/src/Umbraco.Core/ReflectionUtilities.cs index d628896eac..4b08f181d4 100644 --- a/src/Umbraco.Core/ReflectionUtilities.cs +++ b/src/Umbraco.Core/ReflectionUtilities.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using Umbraco.Core.Exceptions; @@ -20,8 +19,10 @@ namespace Umbraco.Core /// Supports emitting constructors, instance and static methods, instance property getters and /// setters. Does not support static properties yet. /// - public static class ReflectionUtilities + public static partial class ReflectionUtilities { + #region Properties + /// /// Emits a property getter. /// @@ -35,18 +36,17 @@ namespace Umbraco.Core /// Occurs when does not match the type of the property. public static Func EmitPropertyGetter(string propertyName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(propertyName)) - throw new ArgumentNullOrEmptyException(nameof(propertyName)); + if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName)); - var property = typeof (TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (property == null || property.GetMethod == null) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); - } + if (property?.GetMethod != null) + return EmitMethod>(property.GetMethod); - return EmitMethod>(property.GetMethod); + if (!mustExist) + return default; + + throw new InvalidOperationException($"Could not find getter for {typeof(TDeclaring)}.{propertyName}."); } /// @@ -62,18 +62,17 @@ namespace Umbraco.Core /// Occurs when does not match the type of the property. public static Action EmitPropertySetter(string propertyName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(propertyName)) - throw new ArgumentNullOrEmptyException(nameof(propertyName)); + if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName)); var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (property == null || property.SetMethod == null) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); - } + if (property?.SetMethod != null) + return EmitMethod>(property.SetMethod); - return EmitMethod>(property.SetMethod); + if (!mustExist) + return default; + + throw new InvalidOperationException($"Could not find setter for {typeof(TDeclaring)}.{propertyName}."); } /// @@ -89,20 +88,19 @@ namespace Umbraco.Core /// Occurs when does not match the type of the property. public static (Func, Action) EmitPropertyGetterAndSetter(string propertyName, bool mustExist = true) { - if (string.IsNullOrWhiteSpace(propertyName)) - throw new ArgumentNullOrEmptyException(nameof(propertyName)); + if (string.IsNullOrWhiteSpace(propertyName)) throw new ArgumentNullOrEmptyException(nameof(propertyName)); var property = typeof(TDeclaring).GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - if (property == null || property.GetMethod == null || property.SetMethod == null) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); - } + if (property?.GetMethod != null && property.SetMethod != null) + return ( + EmitMethod>(property.GetMethod), + EmitMethod>(property.SetMethod)); - return ( - EmitMethod>(property.GetMethod), - EmitMethod>(property.SetMethod)); + if (!mustExist) + return default; + + throw new InvalidOperationException($"Could not find getter and/or setter for {typeof(TDeclaring)}.{propertyName}."); } /// @@ -117,13 +115,12 @@ namespace Umbraco.Core /// Occurs when does not match the type of the property. public static Func EmitPropertyGetter(PropertyInfo propertyInfo) { - if (propertyInfo == null) - throw new ArgumentNullException(nameof(propertyInfo)); + if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); if (propertyInfo.GetMethod == null) throw new ArgumentException("Property has no getter.", nameof(propertyInfo)); - return EmitMethod> (propertyInfo.GetMethod); + return EmitMethod>(propertyInfo.GetMethod); } /// @@ -138,8 +135,7 @@ namespace Umbraco.Core /// Occurs when does not match the type of the property. public static Action EmitPropertySetter(PropertyInfo propertyInfo) { - if (propertyInfo == null) - throw new ArgumentNullException(nameof(propertyInfo)); + if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); if (propertyInfo.SetMethod == null) throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); @@ -159,8 +155,7 @@ namespace Umbraco.Core /// Occurs when does not match the type of the property. public static (Func, Action) EmitPropertyGetterAndSetter(PropertyInfo propertyInfo) { - if (propertyInfo == null) - throw new ArgumentNullException(nameof(propertyInfo)); + if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); if (propertyInfo.GetMethod == null || propertyInfo.SetMethod == null) throw new ArgumentException("Property has no getter and/or no setter.", nameof(propertyInfo)); @@ -170,6 +165,30 @@ namespace Umbraco.Core EmitMethod>(propertyInfo.SetMethod)); } + /// + /// Emits a property setter. + /// + /// The declaring type. + /// The property type. + /// The property info. + /// A property setter function. + /// Occurs when is null. + /// Occurs when the property has no setter. + /// Occurs when does not match the type of the property. + public static Action EmitPropertySetterUnsafe(PropertyInfo propertyInfo) + { + if (propertyInfo == null) throw new ArgumentNullException(nameof(propertyInfo)); + + if (propertyInfo.SetMethod == null) + throw new ArgumentException("Property has no setter.", nameof(propertyInfo)); + + return EmitMethodUnsafe>(propertyInfo.SetMethod); + } + + #endregion + + #region Constructors + /// /// Emits a constructor. /// @@ -188,33 +207,23 @@ namespace Umbraco.Core /// is specified and does not match the function's returned type. public static TLambda EmitCtor(bool mustExist = true, Type declaring = null) { - // validate lambda type - ValidateCtorLambda(); + (_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(true, true); - // get instance and arguments types - var genericArgs = typeof(TLambda).GetGenericArguments(); - var args = new Type[genericArgs.Length - 1]; - for (var i = 0; i < args.Length; i++) - args[i] = genericArgs[i]; - var returned = genericArgs[args.Length]; - - if (declaring == null) - declaring = returned; - else if (!returned.IsAssignableFrom(declaring)) - throw new ArgumentException($"Type {returned} is not assignable from type {declaring}.", nameof(declaring)); + // determine returned / declaring type + if (declaring == null) declaring = lambdaReturned; + else if (!lambdaReturned.IsAssignableFrom(declaring)) + throw new ArgumentException($"Type {lambdaReturned} is not assignable from type {declaring}.", nameof(declaring)); // get the constructor infos - var ctor = declaring.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, - null, args, null); - + var ctor = declaring.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); if (ctor == null) { if (!mustExist) return default; - throw new InvalidOperationException($"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable) args)})."); + throw new InvalidOperationException($"Could not find constructor {declaring}.ctor({string.Join(", ", (IEnumerable) lambdaParameters)})."); } // emit - return EmitCtor(declaring, args, ctor); + return EmitCtorSafe(lambdaParameters, lambdaReturned, ctor); } /// @@ -228,28 +237,30 @@ namespace Umbraco.Core /// Occurs when is null. public static TLambda EmitCtor(ConstructorInfo ctor) { - if (ctor == null) - throw new ArgumentNullException(nameof(ctor)); + if (ctor == null) throw new ArgumentNullException(nameof(ctor)); + (_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(true, true); + + return EmitCtorSafe(lambdaParameters, lambdaReturned, ctor); + } + + private static TLambda EmitCtorSafe(Type[] lambdaParameters, Type returned, ConstructorInfo ctor) + { // get type and args - var declaring = ctor.DeclaringType; - var args = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); - - // validate lambda type - ValidateCtorLambda(); + var ctorDeclaring = ctor.DeclaringType; + var ctorParameters = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); // validate arguments - var genericArgs = typeof(TLambda).GetGenericArguments(); - if (genericArgs.Length != args.Length + 1) - ThrowInvalidLambda("ctor", declaring, args); - for (var i = 0; i < args.Length; i++) - if (args[i] != genericArgs[i]) - ThrowInvalidLambda("ctor", declaring, args); - if (!genericArgs[args.Length].IsAssignableFrom(declaring)) - ThrowInvalidLambda("ctor", declaring, args); + if (lambdaParameters.Length != ctorParameters.Length) + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + for (var i = 0; i < lambdaParameters.Length; i++) + if (lambdaParameters[i] != ctorParameters[i]) // fixme cast? relax? + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); + if (!returned.IsAssignableFrom(ctorDeclaring)) + ThrowInvalidLambda("ctor", ctorDeclaring, ctorParameters); // emit - return EmitCtor(declaring, args, ctor); + return EmitCtor(ctorDeclaring, ctorParameters, ctor); } /// @@ -268,67 +279,81 @@ namespace Umbraco.Core /// Occurs when is null. public static TLambda EmitCtorUnsafe(ConstructorInfo ctor) { - if (ctor == null) - throw new ArgumentNullException(nameof(ctor)); + if (ctor == null) throw new ArgumentNullException(nameof(ctor)); - // get type and args - var declaring = ctor.DeclaringType; - var module = declaring?.Module; - if (module == null) - throw new ArgumentException("Failed to get ctor's declaring type module.", nameof(ctor)); + (_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(true, true); - // validate lambda type - ValidateCtorLambda(); + // emit - unsafe - use lambda's args and assume they are correct + return EmitCtor(lambdaReturned, lambdaParameters, ctor); + } - // unsafe - use lambda's args and assume they are correct - //var args = ctor.GetParameters().Select(x => x.ParameterType).ToArray(); - var genArgs = typeof(TLambda).GetGenericArguments(); - var args = new Type[genArgs.Length - 1]; - Array.Copy(genArgs, 0, args, 0, args.Length); + private static TLambda EmitCtor(Type declaring, Type[] lambdaParameters, ConstructorInfo ctor) + { + // gets the method argument types + var ctorParameters = GetParameters(ctor); // emit - var dm = new DynamicMethod(string.Empty, declaring, args, module, true); - var ilgen = dm.GetILGenerator(); - EmitLdargs(ilgen, args.Length); + (var dm, var ilgen) = CreateIlGenerator(ctor.DeclaringType?.Module, lambdaParameters, declaring); + EmitLdargs(ilgen, lambdaParameters, ctorParameters); ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects ilgen.Emit(OpCodes.Ret); + return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); } - private static void ValidateCtorLambda() + #endregion + + #region Methods + + /// + /// Emits a static method. + /// + /// The declaring type. + /// A lambda representing the method. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// The method. If is false, returns null when the method does not exist. + /// + /// The method arguments are determined by generic arguments. + /// + /// Occurs when is null or empty. + /// Occurs when no proper method with name could be found. + /// Occurs when Occurs when does not match the method signature. + public static TLambda EmitMethod(string methodName, bool mustExist = true) { - var typeLambda = typeof(TLambda); - var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; - if (genericDefinition == null || genericDefinition.FullName == null || !genericDefinition.FullName.StartsWith("System.Func`")) - throw new ArgumentException($"Lambda {typeLambda} is not a Func.", nameof(TLambda)); + return EmitMethod(typeof(TDeclaring), methodName, mustExist); } - private static TLambda EmitCtor(Type declaring, Type[] args, ConstructorInfo ctor) + /// + /// Emits a static method. + /// + /// A lambda representing the method. + /// The declaring type. + /// The name of the method. + /// A value indicating whether the constructor must exist. + /// The method. If is false, returns null when the method does not exist. + /// + /// The method arguments are determined by generic arguments. + /// + /// Occurs when is null or empty. + /// Occurs when no proper method with name could be found. + /// Occurs when Occurs when does not match the method signature. + public static TLambda EmitMethod(Type declaring, string methodName, bool mustExist = true) { - var dm = new DynamicMethod(string.Empty, declaring, args, declaring.Module, true); - var ilgen = dm.GetILGenerator(); - EmitLdargs(ilgen, args.Length); - ilgen.Emit(OpCodes.Newobj, ctor); // ok to just return, it's only objects - ilgen.Emit(OpCodes.Ret); - return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); - } + if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentNullOrEmptyException(nameof(methodName)); - private static void ValidateMethodLambda(out bool isFunction) - { - isFunction = false; + (_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(true, out var isFunction); - var typeLambda = typeof(TLambda); - var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; + // get the method infos + var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, lambdaParameters, null); + if (method == null || isFunction && !lambdaReturned.IsAssignableFrom(method.ReturnType)) + { + if (!mustExist) return default; + throw new InvalidOperationException($"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)})."); + } - if (typeLambda.FullName == "System.Action") - return; - - if (genericDefinition == null - || genericDefinition.FullName == null - || !genericDefinition.FullName.StartsWith("System.Func`") && !genericDefinition.FullName.StartsWith("System.Action`")) - throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); - - isFunction = genericDefinition.FullName.StartsWith("System.Func`"); + // emit + return EmitMethod(lambdaReturned, lambdaParameters, method); } /// @@ -341,54 +366,52 @@ namespace Umbraco.Core /// Occurs when Occurs when does not match the method signature. public static TLambda EmitMethod(MethodInfo method) { - if (method == null) - throw new ArgumentNullException(nameof(method)); + if (method == null) throw new ArgumentNullException(nameof(method)); // get type and args - var type = method.DeclaringType; - var returned = method.ReturnType; - var args = method.GetParameters().Select(x => x.ParameterType).ToArray(); + var methodDeclaring = method.DeclaringType; + var methodReturned = method.ReturnType; + var methodParameters = method.GetParameters().Select(x => x.ParameterType).ToArray(); - // validate lambda type - ValidateMethodLambda(out var isFunction); - - var genericArgs = typeof(TLambda).GetGenericArguments(); var isStatic = method.IsStatic; - var ax = 0; - var gx = 0; + (var lambdaDeclaring, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(isStatic, out var isFunction); - // must match the expected number of args - var expectedCount = (isStatic ? 0 : 1) + args.Length + (isFunction ? 1 : 0); - if (expectedCount != genericArgs.Length) - ThrowInvalidLambda(method.Name, returned, args); + // if not static, then the first lambda arg must be the method declaring type + if (!isStatic && (methodDeclaring == null || !methodDeclaring.IsAssignableFrom(lambdaDeclaring))) + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - // if not static then the first generic arg must be the declaring type - if (!isStatic && genericArgs[gx++] != type) - ThrowInvalidLambda(method.Name, returned, args); + if (methodParameters.Length != lambdaParameters.Length) + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - // all other generic args must match parameters - // except the last one, if it's a function, 'cos then it's the returned type - while (gx < genericArgs.Length - (isFunction ? 1 : 0)) - if (genericArgs[gx++] != args[ax++]) - ThrowInvalidLambda(method.Name, returned, args); + for (var i = 0; i < methodParameters.Length; i++) + if (!methodParameters[i].IsAssignableFrom(lambdaParameters[i])) + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); - // if it's a function then the last one must match the returned type - if (isFunction) - { - if (genericArgs[gx] != returned) - { - if (genericArgs[gx].IsAssignableFrom(returned)) - { - returned = genericArgs[gx]; // FIXME cast etc?! - } - else ThrowInvalidLambda(method.Name, returned, args); - } - } - //if (isFunction && !genericArgs[gx].IsAssignableFrom(returned)) - // ThrowInvalidLambda(method.Name, returned, args); + // if it's a function then the last lambda arg must match the method returned type + if (isFunction && !lambdaReturned.IsAssignableFrom(methodReturned)) + ThrowInvalidLambda(method.Name, methodReturned, methodParameters); // emit - return EmitMethod(returned, args, method); + return EmitMethod(lambdaReturned, lambdaParameters, method); + } + + /// + /// Emits a method. + /// + /// A lambda representing the method. + /// The method info. + /// The method. + /// Occurs when is null. + /// Occurs when Occurs when does not match the method signature. + public static TLambda EmitMethodUnsafe(MethodInfo method) + { + if (method == null) throw new ArgumentNullException(nameof(method)); + + var isStatic = method.IsStatic; + (_, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(isStatic, out _); + + // emit - unsafe - use lambda's args and assume they are correct + return EmitMethod(lambdaReturned, lambdaParameters, method); } /// @@ -410,130 +433,240 @@ namespace Umbraco.Core throw new ArgumentNullOrEmptyException(nameof(methodName)); // validate lambda type - ValidateMethodLambda(out var isFunction); - - // get instance and arguments types - var genericArgs = typeof(TLambda).GetGenericArguments(); - var gx = 0; - var declaring = genericArgs[gx++]; - var args = new Type[genericArgs.Length - 1 - (isFunction ? 1 : 0)]; - for (var i = 0; i < args.Length; i++) - args[i] = genericArgs[gx++]; - var returned = isFunction ? genericArgs[gx] : typeof (void); + (var declaring, var lambdaParameters, var lambdaReturned) = AnalyzeLambda(false, out var isFunction); // get the method infos - var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, - null, args, null); - - if (method == null || isFunction && method.ReturnType != returned) + var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, lambdaParameters, null); + if (method == null || isFunction && method.ReturnType != lambdaReturned) { if (!mustExist) return default; - throw new InvalidOperationException($"Could not find method {declaring}.{methodName}({string.Join(", ", (IEnumerable) args)})."); + throw new InvalidOperationException($"Could not find method {declaring}.{methodName}({string.Join(", ", (IEnumerable) lambdaParameters)})."); } // emit - return EmitMethod(returned, args, method); + return EmitMethod(lambdaReturned, lambdaParameters, method); } - /// - /// Emits a static method. - /// - /// The declaring type. - /// A lambda representing the method. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// The method. If is false, returns null when the method does not exist. - /// - /// The method arguments are determined by generic arguments. - /// - /// Occurs when is null or empty. - /// Occurs when no proper method with name could be found. - /// Occurs when Occurs when does not match the method signature. - public static TLambda EmitMethod(string methodName, bool mustExist = true) + // lambdaReturned = the lambda returned type (can be void) + // lambdaArgTypes = the lambda argument types + private static TLambda EmitMethod(Type lambdaReturned, Type[] lambdaParameters, MethodInfo method) { - return EmitMethod(typeof (TDeclaring), methodName, mustExist); - } - - /// - /// Emits a static method. - /// - /// A lambda representing the method. - /// The declaring type. - /// The name of the method. - /// A value indicating whether the constructor must exist. - /// The method. If is false, returns null when the method does not exist. - /// - /// The method arguments are determined by generic arguments. - /// - /// Occurs when is null or empty. - /// Occurs when no proper method with name could be found. - /// Occurs when Occurs when does not match the method signature. - public static TLambda EmitMethod(Type declaring, string methodName, bool mustExist = true) - { - if (string.IsNullOrWhiteSpace(methodName)) - throw new ArgumentNullOrEmptyException(nameof(methodName)); - - // validate lambda type - ValidateMethodLambda(out var isFunction); - - // get instance and arguments types - var genericArgs = typeof(TLambda).GetGenericArguments(); - var args = new Type[genericArgs.Length - (isFunction ? 1 : 0)]; - for (var i = 0; i < args.Length; i++) - args[i] = genericArgs[i]; - var returned = isFunction ? genericArgs[args.Length] : typeof (void); - - // get the method infos - var method = declaring.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, - null, args, null); - - if (method == null || isFunction && method.ReturnType != returned) - { - if (!mustExist) return default; - throw new InvalidOperationException($"Could not find static method {declaring}.{methodName}({string.Join(", ", (IEnumerable) args)})."); - } - - // emit - return EmitMethod(returned, args, method); - } - - private static TLambda EmitMethod(Type returned, Type[] args, MethodInfo method) - { - var args2 = args; + // non-static methods need the declaring type as first arg + var parameters = lambdaParameters; if (!method.IsStatic) { - args2 = new Type[args.Length + 1]; - args2[0] = method.DeclaringType; - Array.Copy(args, 0, args2, 1, args.Length); + parameters = new Type[lambdaParameters.Length + 1]; + parameters[0] = method.DeclaringType; + Array.Copy(lambdaParameters, 0, parameters, 1, lambdaParameters.Length); } - var module = method.DeclaringType?.Module; - if (module == null) - throw new ArgumentException("Failed to get method's declaring type module.", nameof(method)); - var dm = new DynamicMethod(string.Empty, returned, args2, module, true); - var ilgen = dm.GetILGenerator(); - EmitLdargs(ilgen, args2.Length); + + // gets the method argument types + var methodArgTypes = GetParameters(method, withDeclaring: !method.IsStatic); + + // emit IL + (var dm, var ilgen) = CreateIlGenerator(method.DeclaringType?.Module, parameters, lambdaReturned); + EmitLdargs(ilgen, parameters, methodArgTypes); ilgen.Emit(method.IsStatic ? OpCodes.Call : OpCodes.Callvirt, method); + EmitOutputAdapter(ilgen, lambdaReturned, method.ReturnType); ilgen.Emit(OpCodes.Ret); + + // create return (TLambda) (object) dm.CreateDelegate(typeof(TLambda)); } - private static void EmitLdargs(ILGenerator ilgen, int count) + #endregion + + #region Utilities + + // when !isStatic, the first generic argument of the lambda is the declaring type + // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument + // when isFunction, the last generic argument of the lambda is the returned type + // everything in between is parameters + private static (Type Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, bool isFunction) { - if (count < 5) + var typeLambda = typeof(TLambda); + + (var declaring, var parameters, var returned) = AnalyzeLambda(isStatic, out var maybeFunction); + + if (isFunction) { - if (count > 0) - ilgen.Emit(OpCodes.Ldarg_0); - if (count > 1) - ilgen.Emit(OpCodes.Ldarg_1); - if (count > 2) - ilgen.Emit(OpCodes.Ldarg_2); - if (count > 3) - ilgen.Emit(OpCodes.Ldarg_3); + if (!maybeFunction) + throw new ArgumentException($"Lambda {typeLambda} is an Action, a Func was expected.", nameof(TLambda)); } else { - for (var i = 0; i < count; i++) + if (maybeFunction) + throw new ArgumentException($"Lambda {typeLambda} is a Func, an Action was expected.", nameof(TLambda)); + } + + return (declaring, parameters, returned); + } + + // when !isStatic, the first generic argument of the lambda is the declaring type + // hence, when !isStatic, the lambda cannot be a simple Action, as it requires at least one generic argument + // when isFunction, the last generic argument of the lambda is the returned type + // everything in between is parameters + private static (Type Declaring, Type[] Parameters, Type Returned) AnalyzeLambda(bool isStatic, out bool isFunction) + { + isFunction = false; + + var typeLambda = typeof(TLambda); + + var isAction = typeLambda.FullName == "System.Action"; + if (isAction) + { + if (!isStatic) + throw new ArgumentException($"Lambda {typeLambda} is an Action and can be used for static methods exclusively.", nameof(TLambda)); + + return (null, Array.Empty(), typeof(void)); + } + + var genericDefinition = typeLambda.IsGenericType ? typeLambda.GetGenericTypeDefinition() : null; + var name = genericDefinition?.FullName; + + if (name == null) + throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + + var isActionOf = name.StartsWith("System.Action`"); + isFunction = name.StartsWith("System.Func`"); + + if (!isActionOf && !isFunction) + throw new ArgumentException($"Lambda {typeLambda} is not a Func nor an Action.", nameof(TLambda)); + + var genericArgs = typeLambda.GetGenericArguments(); + if (genericArgs.Length == 0) + throw new Exception("Panic: Func<> or Action<> has zero generic arguments."); + + var i = 0; + var declaring = isStatic ? typeof(void) : genericArgs[i++]; + + var parameterCount = genericArgs.Length - (isStatic ? 0 : 1) - (isFunction ? 1 : 0); + if (parameterCount < 0) + throw new ArgumentException($"Lambda {typeLambda} is a Func and requires at least two arguments (declaring type and returned type).", nameof(TLambda)); + + var parameters = new Type[parameterCount]; + for (var j = 0; j < parameterCount; j++) + parameters[j] = genericArgs[i++]; + + var returned = isFunction ? genericArgs[i] : typeof(void); + + return (declaring, parameters, returned); + } + + private static (DynamicMethod, ILGenerator) CreateIlGenerator(Module module, Type[] arguments, Type returned) + { + if (module == null) throw new ArgumentNullException(nameof(module)); + var dm = new DynamicMethod(string.Empty, returned, arguments, module, true); + return (dm, dm.GetILGenerator()); + } + + private static Type[] GetParameters(ConstructorInfo ctor) + { + var parameters = ctor.GetParameters(); + var types = new Type[parameters.Length]; + var i = 0; + foreach (var parameter in parameters) + types[i++] = parameter.ParameterType; + return types; + } + + private static Type[] GetParameters(MethodInfo method, bool withDeclaring) + { + var parameters = method.GetParameters(); + var types = new Type[parameters.Length + (withDeclaring ? 1 : 0)]; + var i = 0; + if (withDeclaring) + types[i++] = method.DeclaringType; + foreach (var parameter in parameters) + types[i++] = parameter.ParameterType; + return types; + } + + // emits args + private static void EmitLdargs(ILGenerator ilgen, Type[] lambdaArgTypes, Type[] methodArgTypes) + { + var ldargOpCodes = new[] { OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3 }; + + if (lambdaArgTypes.Length != methodArgTypes.Length) + throw new Exception("Panic: inconsistent number of args."); + + for (var i = 0; i < lambdaArgTypes.Length; i++) + { + if (lambdaArgTypes.Length < 5) + ilgen.Emit(ldargOpCodes[i]); + else ilgen.Emit(OpCodes.Ldarg, i); + + //EmitInputAdapter(ilgen, lambdaArgTypes[i], methodArgTypes[i]); + } + } + + // emits adapter opcodes after each OpCode.Ldarg + private static void EmitInputAdapter(ILGenerator ilgen, Type inputType, Type methodParamType) + { + if (inputType == methodParamType) return; + + if (methodParamType.IsValueType) + { + if (inputType.IsValueType) + { + // both input and parameter are value types + // not supported, use proper input + // (otherwise, would require converting) + throw new NotSupportedException("ValueTypes conversion."); + } + + // parameter is value type, but input is reference type + // unbox the input to the parameter value type + // fixme - conversions? + ilgen.Emit(OpCodes.Unbox_Any, inputType); + } + else + { + // parameter is reference type, but input is value type + // not supported, input should always be less constrained + // (otherwise, would require boxing and converting) + if (inputType.IsValueType) + throw new NotSupportedException("ValueType boxing."); + + // both input and parameter are reference types + // cast the input to the parameter type + ilgen.Emit(OpCodes.Castclass, methodParamType); + } + } + + // emits adapter opcodes before OpCodes.Ret + private static void EmitOutputAdapter(ILGenerator ilgen, Type outputType, Type methodReturnedType) + { + if (outputType == methodReturnedType) return; + + if (methodReturnedType.IsValueType) + { + if (outputType.IsValueType) + { + // both returned and output are value types + // not supported, use proper output + // (otherwise, would require converting) + throw new NotSupportedException("ValueTypes conversion."); + } + + // returned is value type, but output is reference type + // box the returned value + // fixme - conversions? + ilgen.Emit(OpCodes.Box, methodReturnedType); + } + else + { + // returned is reference type, but output is value type + // not supported, output should always be less constrained + // (otherwise, would require boxing and converting) + if (outputType.IsValueType) + throw new NotSupportedException("ValueType boxing."); + + // both output and returned are reference types + // as long as returned can be assigned to output, good + if (!outputType.IsAssignableFrom(methodReturnedType)) + throw new NotSupportedException("Invalid cast."); } } @@ -542,355 +675,6 @@ namespace Umbraco.Core throw new ArgumentException($"Lambda {typeof(TLambda)} does not match {methodName}({string.Join(", ", (IEnumerable) args)}):{returned}.", nameof(TLambda)); } - // the code below should NOT be used - // - // keeping all this around as a reference for now - building expressions into dynamic assemblies, - // the resulting methods are fast (as fast as IL-generated methods) whereas the standard compiled - // expressions are slowish - alas, the compiled methods do not have access to eg private members - // and anything that would violate access control - we're not using it anymore - still used in a - // benchmark - - internal static Func GetCtor() - { - var type = typeof (TInstance); - var type0 = typeof (TArg0); - - // get the constructor infos - var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, - null, new[] { type0 }, null); - - if (ctor == null) - throw new InvalidOperationException($"Could not find constructor {type}.ctor({type0})."); - - var exprArgs = ctor.GetParameters().Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToArray(); - // ReSharper disable once CoVariantArrayConversion - var exprNew = Expression.New(ctor, exprArgs); - var expr = Expression.Lambda>(exprNew, exprArgs); - return CompileToDelegate(expr); - } - - internal static Func GetCtor() - { - var type = typeof (TInstance); - var type0 = typeof (TArg0); - var type1 = typeof (TArg1); - - // get the constructor infos - var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, - null, new[] { type0, type1 }, null); - - if (ctor == null) - throw new InvalidOperationException($"Could not find constructor {type}.ctor({type0}, {type1})."); - - var exprArgs = ctor.GetParameters().Select(x => Expression.Parameter(x.ParameterType, x.Name)).ToArray(); - // ReSharper disable once CoVariantArrayConversion - var exprNew = Expression.New(ctor, exprArgs); - var expr = Expression.Lambda>(exprNew, exprArgs); - return CompileToDelegate(expr); - } - - internal static TMethod GetMethod(MethodInfo method) - { - var type = method.DeclaringType; - - GetMethodParms(out var parameterTypes, out var returnType); - return GetStaticMethod(method, method.Name, type, parameterTypes, returnType); - } - - internal static TMethod GetMethod(MethodInfo method) - { - var type = method.DeclaringType; - - GetMethodParms(out var parameterTypes, out var returnType); - return GetMethod(method, method.Name, type, parameterTypes, returnType); - } - - internal static TMethod GetMethod(string methodName) - { - var type = typeof (TInstance); - - GetMethodParms(out var parameterTypes, out var returnType); - - var method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, - null, parameterTypes, null); - - return GetMethod(method, methodName, type, parameterTypes, returnType); - } - - private static void GetMethodParms(out Type[] parameterTypes, out Type returnType) - { - var typeM = typeof (TMethod); - var typeList = new List(); - returnType = typeof (void); - - if (!typeof (MulticastDelegate).IsAssignableFrom(typeM) || typeM == typeof (MulticastDelegate)) - throw new InvalidOperationException("Invalid TMethod, must be a Func or Action."); - - var typeName = typeM.FullName; - if (typeName == null) - throw new InvalidOperationException($"Could not get {typeM} type name."); - if (typeName.StartsWith("System.Func`")) - { - var i = 0; - while (i < typeM.GenericTypeArguments.Length - 1) - typeList.Add(typeM.GenericTypeArguments[i++]); - returnType = typeM.GenericTypeArguments[i]; - } - else if (typeName.StartsWith("System.Action`")) - { - var i = 0; - while (i < typeM.GenericTypeArguments.Length) - typeList.Add(typeM.GenericTypeArguments[i++]); - } - else if (typeName == "System.Action") - { - // no args - } - else - throw new InvalidOperationException(typeName); - - parameterTypes = typeList.ToArray(); - } - - private static void GetMethodParms(out Type[] parameterTypes, out Type returnType) - { - var type = typeof (TInstance); - - var typeM = typeof (TMethod); - var typeList = new List(); - returnType = typeof (void); - - if (!typeof (MulticastDelegate).IsAssignableFrom(typeM) || typeM == typeof (MulticastDelegate)) - throw new InvalidOperationException("Invalid TMethod, must be a Func or Action."); - - var typeName = typeM.FullName; - if (typeName == null) - throw new InvalidOperationException($"Could not get {typeM} type name."); - if (!typeM.IsGenericType) - throw new InvalidOperationException($"Type {typeName} is not generic."); - if (typeM.GenericTypeArguments[0] != type) - throw new InvalidOperationException($"Invalid type {typeName}, the first generic argument must be {type.FullName}."); - if (typeName.StartsWith("System.Func`")) - { - var i = 1; - while (i < typeM.GenericTypeArguments.Length - 1) - typeList.Add(typeM.GenericTypeArguments[i++]); - returnType = typeM.GenericTypeArguments[i]; - } - else if (typeName.StartsWith("System.Action`")) - { - var i = 1; - while (i < typeM.GenericTypeArguments.Length) - typeList.Add(typeM.GenericTypeArguments[i++]); - } - else - throw new InvalidOperationException(typeName); - - parameterTypes = typeList.ToArray(); - } - - private static TMethod GetStaticMethod(MethodInfo method, string methodName, Type type, Type[] parameterTypes, Type returnType) - { - if (method == null || method.ReturnType != returnType) - throw new InvalidOperationException($"Could not find static method {type}.{methodName}({string.Join(",", parameterTypes.Select(x => x.ToString()))}) : {returnType}"); - - var e = new List(); - foreach (var p in method.GetParameters()) - e.Add(Expression.Parameter(p.ParameterType, p.Name)); - var exprCallArgs = e.ToArray(); - var exprLambdaArgs = exprCallArgs; - - // ReSharper disable once CoVariantArrayConversion - var exprCall = Expression.Call(method, exprCallArgs); - var expr = Expression.Lambda(exprCall, exprLambdaArgs); - return CompileToDelegate(expr); - } - - private static TMethod GetMethod(MethodInfo method, string methodName, Type type, Type[] parameterTypes, Type returnType) - { - if (method == null || method.ReturnType != returnType) - throw new InvalidOperationException($"Could not find method {type}.{methodName}({string.Join(",", parameterTypes.Select(x => x.ToString()))}) : {returnType}"); - - var e = new List(); - foreach (var p in method.GetParameters()) - e.Add(Expression.Parameter(p.ParameterType, p.Name)); - var exprCallArgs = e.ToArray(); - - var exprThis = Expression.Parameter(type, "this"); - e.Insert(0, exprThis); - var exprLambdaArgs = e.ToArray(); - - // ReSharper disable once CoVariantArrayConversion - var exprCall = Expression.Call(exprThis, method, exprCallArgs); - var expr = Expression.Lambda(exprCall, exprLambdaArgs); - return expr.Compile(); - } - - internal const AssemblyBuilderAccess DefaultAssemblyBuilderAccess = AssemblyBuilderAccess.Run; - internal const AssemblyBuilderAccess NoAssembly = 0; - - internal static TLambda Compile(Expression expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - return access == NoAssembly - ? expr.Compile() - : CompileToDelegate(expr, access); - } - - internal static Action CompileToDelegate(Expression expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static); // CompileToMethod requires a static method - - expr.CompileToMethod(builder); - - return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Action CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - typeof (void), new[] { typeof (T1) }); - - expr.CompileToMethod(builder); - - return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Action CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - typeof (void), new[] { typeof (T1), typeof (T2) }); - - expr.CompileToMethod(builder); - - return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Action CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - typeof (void), new[] { typeof (T1), typeof (T2), typeof (T3) }); - - expr.CompileToMethod(builder); - - return (Action) Delegate.CreateDelegate(typeof (Action), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, - typeof (TResult), Array.Empty()); // CompileToMethod requires a static method - - expr.CompileToMethod(builder); - - return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - typeof (TResult), new[] { typeof (T1) }); - - expr.CompileToMethod(builder); - - return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - typeof (TResult), new[] { typeof (T1), typeof (T2) }); - - expr.CompileToMethod(builder); - - return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static Func CompileToDelegate(Expression> expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - typeof (TResult), new[] { typeof (T1), typeof (T2), typeof (T3) }); - - expr.CompileToMethod(builder); - - return (Func) Delegate.CreateDelegate(typeof (Func), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static TMethod CompileToDelegate(Expression expr, AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var typeBuilder = CreateTypeBuilder(access); - - GetMethodParms(out var parameterTypes, out var returnType); - - var builder = typeBuilder.DefineMethod("Method", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - returnType, parameterTypes); - - expr.CompileToMethod(builder); - - return (TMethod) (object) Delegate.CreateDelegate(typeof (TMethod), typeBuilder.CreateType().GetMethod("Method")); - } - - internal static TMethod[] CompileToDelegates(params Expression[] exprs) - => CompileToDelegates(AssemblyBuilderAccess.RunAndCollect, exprs); - - internal static TMethod[] CompileToDelegates(AssemblyBuilderAccess access, params Expression[] exprs) - { - var typeBuilder = CreateTypeBuilder(access); - - GetMethodParms(out var parameterTypes, out var returnType); - - var i = 0; - foreach (var expr in exprs) - { - var builder = typeBuilder.DefineMethod($"Method_{i++}", - MethodAttributes.Public | MethodAttributes.Static, // CompileToMethod requires a static method - returnType, parameterTypes); - - expr.CompileToMethod(builder); - } - - var type = typeBuilder.CreateType(); - - var methods = new TMethod[exprs.Length]; - for (i = 0; i < exprs.Length; i++) - methods[i] = (TMethod) (object) Delegate.CreateDelegate(typeof (TMethod), type.GetMethod($"Method_{i++}")); - return methods; - } - - private static TypeBuilder CreateTypeBuilder(AssemblyBuilderAccess access = DefaultAssemblyBuilderAccess) - { - var assemblyName = new AssemblyName("Umbraco.Core.DynamicAssemblies." + Guid.NewGuid().ToString("N")); - var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, access); - - var module = (access & AssemblyBuilderAccess.Save) > 0 - ? assembly.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll") - : assembly.DefineDynamicModule(assemblyName.Name); // has to be transient - - return module.DefineType("Class", TypeAttributes.Public | TypeAttributes.Abstract); - } + #endregion } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c8ee01f031..90715766de 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -343,6 +343,7 @@ + diff --git a/src/Umbraco.Tests/Clr/ReflectionTests.cs b/src/Umbraco.Tests/Clr/ReflectionTests.cs index af721ae3d8..f3704c8552 100644 --- a/src/Umbraco.Tests/Clr/ReflectionTests.cs +++ b/src/Umbraco.Tests/Clr/ReflectionTests.cs @@ -38,6 +38,8 @@ namespace Umbraco.Tests.Clr Assert.Contains(typeof(IInterface2), interfaces); } + #region Test Objects + interface IInterface1 { } @@ -53,5 +55,7 @@ namespace Umbraco.Tests.Clr class Class2 : Class1 { } + + #endregion } } diff --git a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs index f255f096b5..cddb039db5 100644 --- a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs +++ b/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs @@ -1,8 +1,9 @@ using System; using System.Reflection; -using Lucene.Net.Index; using NUnit.Framework; using Umbraco.Core; +using System.Linq; +using Newtonsoft.Json; namespace Umbraco.Tests.Clr { @@ -267,8 +268,261 @@ namespace Umbraco.Tests.Clr Assert.IsNull(ReflectionUtilities.EmitPropertyGetter("Value2", false)); } + [Test] + public void SetterCanCastValue() + { + // test that we can emit property setters that cast from eg 'object' + + // well - no - we don't do this + + /* + + var type4 = typeof(Class4); + var propString4 = type4.GetProperty("StringValue"); + var propClassA4 = type4.GetProperty("ClassAValue"); + + var object4 = new Class4(); + var object2A = new Class2A(); + + // works with a string property + Assert.IsNotNull(propString4); + var setterString4 = ReflectionUtilities.EmitPropertySetter(propString4); + Assert.IsNotNull(setterString4); + setterString4(object4, "foo"); + Assert.IsNotNull(object4.StringValue); + Assert.AreEqual("foo", object4.StringValue); + + setterString4(object4, new Class2()); + + // works with a reference property + Assert.IsNotNull(propClassA4); + var setterClassA4 = ReflectionUtilities.EmitPropertySetter(propClassA4); + Assert.IsNotNull(setterClassA4); + setterClassA4(object4, object2A); + Assert.IsNotNull(object4.ClassAValue); + Assert.AreEqual(object2A, object4.ClassAValue); + + */ + } + + [Test] + public void SetterCanCastObject() + { + var type5 = typeof(Class5); + var propClass4 = type5.GetProperty("ClassValue"); + + var object2 = new Class2(); + var object4 = new Class5(); + + // can cast the object type from Class5 to Class4 + var setterClass4 = ReflectionUtilities.EmitPropertySetter(propClass4); + + setterClass4(object4, object2); + Assert.IsNotNull(object4.ClassValue); + Assert.AreSame(object2, object4.ClassValue); + } + + [Test] + public void GetterCanCastValue() + { + var type4 = typeof(Class4); + var propClassA4 = type4.GetProperty("ClassAValue"); + var propInt4 = type4.GetProperty("IntValue"); + + var object2A = new Class2A(); + var object4 = new Class4 { ClassAValue = object2A, IntValue = 159 }; + + // can cast the return type from Class2A to Class2 + var getterClassA4 = ReflectionUtilities.EmitPropertyGetter(propClassA4); + + var valueClass4A = getterClassA4(object4); + Assert.IsNotNull(valueClass4A); + Assert.AreSame(object2A, valueClass4A); + + // cannot cast the return type from Class2A to Class3! + Assert.Throws(() + => ReflectionUtilities.EmitPropertyGetter(propClassA4)); + + // can cast and box the return type from int to object + var getterInt4 = ReflectionUtilities.EmitPropertyGetter(propInt4); + + var valueInt4 = getterInt4(object4); + Assert.IsTrue(valueInt4 is int); + Assert.AreEqual(159, valueInt4); + + // cannot cast the return type from int to Class3! + Assert.Throws(() + => ReflectionUtilities.EmitPropertyGetter(propInt4)); + } + + [Test] + public void GetterCanCastObject() + { + var type5 = typeof(Class5); + var propClass4 = type5.GetProperty("ClassValue"); + + var object2 = new Class2(); + var object4 = new Class5 { ClassValue = object2 }; + + // can cast the object type from Class5 to Class4 + var getterClass4 = ReflectionUtilities.EmitPropertyGetter(propClass4); + + var valueClass4 = getterClass4(object4); + Assert.IsNotNull(valueClass4); + Assert.AreSame(object2, valueClass4); + + // cannot cast the object type from Class3 to Class4! + Assert.Throws(() + => ReflectionUtilities.EmitPropertyGetter(propClass4)); + } + + [Test] + public void CanEmitCastGetters() + { + // test that we can emit property getters that cast the returned value to 'object' + + // test simple class + + var type4 = typeof(Class4); + + var object4 = new Class4 + { + IntValue = 1, + StringValue = "foo", + ClassValue = new Class2(), + }; + + // works with a string property + var propString4 = type4.GetProperty("StringValue"); + Assert.IsNotNull(propString4); + var getterString4 = ReflectionUtilities.EmitPropertyGetter(propString4); + Assert.IsNotNull(getterString4); + var valueString4 = getterString4(object4); + Assert.IsNotNull(valueString4); + Assert.AreEqual("foo", valueString4); + + // works with a reference property + var propClass4 = type4.GetProperty("ClassValue"); + Assert.IsNotNull(propClass4); + var getterClass4 = ReflectionUtilities.EmitPropertyGetter(propClass4); + Assert.IsNotNull(getterClass4); + var valueClass4 = getterClass4(object4); + Assert.IsNotNull(valueClass4); + Assert.IsInstanceOf(valueClass4); + + // works with a value type property + var propInt4 = type4.GetProperty("IntValue"); + Assert.IsNotNull(propInt4); + + // ... if explicitely getting a value type + var getterInt4T = ReflectionUtilities.EmitPropertyGetter(propInt4); + Assert.IsNotNull(getterInt4T); + var valueInt4T = getterInt4T(object4); + Assert.AreEqual(1, valueInt4T); + + // ... if using a compiled getter + var valueInt4D = GetIntValue(object4); + Assert.IsNotNull(valueInt4D); + Assert.IsTrue(valueInt4D is int); + Assert.AreEqual(1, valueInt4D); + + // ... if getting a non-value type (emit adds a box) + var getterInt4 = ReflectionUtilities.EmitPropertyGetter(propInt4); + Assert.IsNotNull(getterInt4); + var valueInt4 = getterInt4(object4); + Assert.IsNotNull(valueInt4); + Assert.IsTrue(valueInt4 is int); + Assert.AreEqual(1, valueInt4); + + var getters4 = type4 + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) + .ToDictionary(x => x.Name, x => (object) ReflectionUtilities.EmitPropertyGetter(x)); + + Console.WriteLine("Getting object4 values..."); + var values4 = getters4.ToDictionary(kvp => kvp.Key, kvp => ((Func) kvp.Value)(object4)); + + Console.WriteLine("Writing object4 values..."); + foreach ((var name, var value) in values4) + Console.WriteLine($"{name}: {value}"); + Assert.AreEqual(4, values4.Count); + Assert.AreEqual("foo", values4["StringValue"]); + Assert.IsInstanceOf(values4["ClassValue"]); + Assert.AreEqual(1, values4["IntValue"]); + + // test hierarchy + + var type5 = typeof(Class5); + + var getters5 = type5 + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy) + .ToDictionary(x => x.Name, x => (object) ReflectionUtilities.EmitPropertyGetter(x)); + + var object5 = new Class5 + { + IntValue = 1, + IntValue2 = 1, + StringValue = "foo", + StringValue2 = "foo", + ClassValue = new Class2(), + ClassValue2 = new Class2() + }; + + Console.WriteLine("Getting object5 values..."); + var values5 = getters5.ToDictionary(kvp => kvp.Key, kvp => ((Func) kvp.Value)(object5)); + + Console.WriteLine("Writing object5 values..."); + foreach ((var name, var value) in values5) + Console.WriteLine($"{name}: {value}"); + Assert.AreEqual(7, values5.Count); + Assert.AreEqual("foo", values5["StringValue"]); + Assert.IsInstanceOf(values5["ClassValue"]); + Assert.AreEqual(1, values5["IntValue"]); + Assert.AreEqual("foo", values5["StringValue2"]); + Assert.IsInstanceOf(values5["ClassValue2"]); + Assert.AreEqual(1, values5["IntValue2"]); + + // test object extensions + + Console.WriteLine("Getting object5D values..."); + var values5D = ObjectExtensions.ToObjectDictionary(object5); + + Console.WriteLine("Writing object5D values..."); + foreach ((var name, var value) in values5) + Console.WriteLine($"{name}: {value}"); + Assert.AreEqual(7, values5.Count); + Assert.AreEqual("foo", values5D["StringValue"]); + Assert.IsInstanceOf(values5D["ClassValue"]); + Assert.AreEqual(1, values5D["IntValue"]); + Assert.AreEqual("foo", values5D["StringValue2"]); + Assert.IsInstanceOf(values5D["ClassValue2"]); + Assert.AreEqual(1, values5D["intValue2"]); // JsonProperty changes property name + } + // fixme - missing tests specifying 'returned' on method, property + #region IL Code + + // these functions can be examined in eg DotPeek to understand IL works + + // box [mscorlib]System.Int32 + public object GetIntValue(Class4 object4) => object4.IntValue; + + // unbox.any [mscorlib]System.Int32 + public void SetIntValue(Class4 object4, object i) => object4.IntValue = (int) i; + + // castclass [mscorlib]System.String + public void SetStringValue(Class4 object4, object s) => object4.StringValue = (string) s; + + // conv.i4 + public void SetIntValue(Class4 object4, double d) => object4.IntValue = (int) d; + + // conv.i4 + public void SetIntValue2(Class4 object4, object d) => object4.IntValue = (int) (double) d; + + #endregion + + #region Test Objects + public static class StaticClass1 { public static void Method() { } @@ -302,11 +556,31 @@ namespace Umbraco.Tests.Clr public class Class2 { } + public class Class2A : Class2 { } + public class Class3 { public Class3(int i) { } private Class3(string s) { } } + + public class Class4 + { + public int IntValue { get; set; } + public string StringValue { get; set; } + public Class2 ClassValue { get;set; } + public Class2A ClassAValue { get; set; } + } + + public class Class5 : Class4 + { + [JsonProperty("intValue2")] + public int IntValue2 { get; set; } + public string StringValue2 { get; set; } + public Class2 ClassValue2 { get;set; } + } + + #endregion } } diff --git a/src/Umbraco.Tests/Composing/ActionCollectionTests.cs b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs new file mode 100644 index 0000000000..04bd0a2e1e --- /dev/null +++ b/src/Umbraco.Tests/Composing/ActionCollectionTests.cs @@ -0,0 +1,73 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Web; +using Umbraco.Web.UI.Pages; +using Umbraco.Web._Legacy.Actions; + +namespace Umbraco.Tests.Composing +{ + [TestFixture] + public class ActionCollectionTests : ComposingTestBase + { + [Test] + public void ActionCollectionBuilderWorks() + { + var collectionBuilder = new ActionCollectionBuilder(); + collectionBuilder.SetProducer(() => TypeLoader.GetActions()); + + var actions = collectionBuilder.CreateCollection(); + Assert.AreEqual(2, actions.Count()); + + // order is unspecified, but both must be there + var hasAction1 = actions.ElementAt(0) is SingletonAction || actions.ElementAt(1) is SingletonAction; + var hasAction2 = actions.ElementAt(0) is NonSingletonAction || actions.ElementAt(1) is NonSingletonAction; + Assert.IsTrue(hasAction1); + Assert.IsTrue(hasAction2); + + var singletonAction = (SingletonAction) (actions.ElementAt(0) is SingletonAction ? actions.ElementAt(0) : actions.ElementAt(1)); + + // ensure it is a singleton + Assert.AreSame(SingletonAction.Instance, singletonAction); + } + + #region Test Objects + + public class SingletonAction : IAction + { + public static SingletonAction Instance { get; } = new SingletonAction(); + + public char Letter => 'I'; + + public string JsFunctionName => $"{ClientTools.Scripts.GetAppActions}.actionAssignDomain()"; + + public string JsSource => null; + + public string Alias => "assignDomain"; + + public string Icon => ".sprDomain"; + + public bool ShowInNotifier => false; + + public bool CanBePermissionAssigned => true; + } + + public class NonSingletonAction : IAction + { + public char Letter => 'Q'; + + public string JsFunctionName => $"{ClientTools.Scripts.GetAppActions}.actionAssignDomain()"; + + public string JsSource => null; + + public string Alias => "asfasdf"; + + public string Icon => ".sprDomain"; + + public bool ShowInNotifier => false; + + public bool CanBePermissionAssigned => true; + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/DependencyInjection/CollectionBuildersTests.cs b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs similarity index 89% rename from src/Umbraco.Tests/DependencyInjection/CollectionBuildersTests.cs rename to src/Umbraco.Tests/Composing/CollectionBuildersTests.cs index 92c81b4429..87b0cd5173 100644 --- a/src/Umbraco.Tests/DependencyInjection/CollectionBuildersTests.cs +++ b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs @@ -5,7 +5,7 @@ using LightInject; using NUnit.Framework; using Umbraco.Core.Composing; -namespace Umbraco.Tests.DI +namespace Umbraco.Tests.Composing { [TestFixture] public class CollectionBuildersTests @@ -30,73 +30,6 @@ namespace Umbraco.Tests.DI _container = null; } - #region Test objects - - public abstract class Resolved - { } - - public class Resolved1 : Resolved - { } - - [Weight(5)] // default is 10 - public class Resolved2 : Resolved - { } - - public class Resolved3 : Resolved - { } - - public class Resolved4 // not! : Resolved - { } - - private class TestCollectionBuilder : OrderedCollectionBuilderBase - { - public TestCollectionBuilder(IServiceContainer container) - : base(container) - { } - - protected override TestCollectionBuilder This => this; - } - - private class TestCollectionBuilderTransient : OrderedCollectionBuilderBase - { - public TestCollectionBuilderTransient(IServiceContainer container) - : base(container) - { } - - protected override TestCollectionBuilderTransient This => this; - - protected override ILifetime CollectionLifetime => null; // transient - } - - private class TestCollectionBuilderScope : OrderedCollectionBuilderBase - { - public TestCollectionBuilderScope(IServiceContainer container) - : base(container) - { } - - protected override TestCollectionBuilderScope This => this; - - protected override ILifetime CollectionLifetime => new PerScopeLifetime(); - } - - private class TestCollectionBuilderWeighted : WeightedCollectionBuilderBase - { - public TestCollectionBuilderWeighted(IServiceContainer container) - : base(container) - { } - - protected override TestCollectionBuilderWeighted This => this; - } - - private class TestCollection : BuilderCollectionBase - { - public TestCollection(IEnumerable items) - : base(items) - { } - } - - #endregion - [Test] public void ContainsTypes() { @@ -114,7 +47,7 @@ namespace Umbraco.Tests.DI } [Test] - public void Clear() + public void CanClearBuilderBeforeCollectionIsCreated() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -129,7 +62,7 @@ namespace Umbraco.Tests.DI } [Test] - public void ClearOnceResolved() + public void CannotClearBuilderOnceCollectionIsCreated() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -141,7 +74,7 @@ namespace Umbraco.Tests.DI } [Test] - public void Append() + public void CanAppendToBuilder() { var builder = _container.RegisterCollectionBuilder(); builder.Append(); @@ -156,7 +89,7 @@ namespace Umbraco.Tests.DI } [Test] - public void AppendOnceResolved() + public void CannotAppendToBuilderOnceCollectionIsCreated() { var builder = _container.RegisterCollectionBuilder(); @@ -168,7 +101,7 @@ namespace Umbraco.Tests.DI } [Test] - public void AppendDuplicate() + public void CanAppendDuplicateToBuilderAndDeDuplicate() { var builder = _container.RegisterCollectionBuilder(); builder.Append(); @@ -179,7 +112,7 @@ namespace Umbraco.Tests.DI } [Test] - public void AppendInvalid() + public void CannotAppendInvalidTypeToBUilder() { var builder = _container.RegisterCollectionBuilder(); //builder.Append(); // does not compile @@ -189,7 +122,7 @@ namespace Umbraco.Tests.DI } [Test] - public void Remove() + public void CanRemoveFromBuilder() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -205,7 +138,7 @@ namespace Umbraco.Tests.DI } [Test] - public void RemoveAbsent() + public void CanRemoveMissingFromBuilder() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -217,7 +150,7 @@ namespace Umbraco.Tests.DI } [Test] - public void RemoveOnceResolved() + public void CannotRemoveFromBuilderOnceCollectionIsCreated() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -230,7 +163,7 @@ namespace Umbraco.Tests.DI } [Test] - public void Insert() + public void CanInsertIntoBuilder() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -246,7 +179,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertOnceResolved() + public void CannotInsertIntoBuilderOnceCollectionIsCreated() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -259,7 +192,7 @@ namespace Umbraco.Tests.DI } [Test] - public void CanInsertDuplicate() + public void CanInsertDuplicateIntoBuilderAndDeDuplicate() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -271,7 +204,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertInEmpty() + public void CanInsertIntoEmptyBuilder() { var builder = _container.RegisterCollectionBuilder(); builder.Insert(); @@ -281,7 +214,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertAtWrongIndex1() + public void CannotInsertIntoBuilderAtWrongIndex() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -290,14 +223,6 @@ namespace Umbraco.Tests.DI Assert.Throws(() => builder.Insert(99) // throws ); - } - - [Test] - public void InsertAtWrongIndex2() - { - var builder = _container.RegisterCollectionBuilder() - .Append() - .Append(); Assert.Throws(() => builder.Insert(-1) // throws @@ -305,7 +230,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertBefore() + public void CanInsertIntoBuilderBefore() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -321,7 +246,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertBeforeOnceResolved() + public void CannotInsertIntoBuilderBeforeOnceCollectionIsCreated() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -334,7 +259,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertBeforeDuplicate() + public void CanInsertDuplicateIntoBuilderBeforeAndDeDuplicate() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -346,7 +271,7 @@ namespace Umbraco.Tests.DI } [Test] - public void InsertBeforeAbsent() + public void CannotInsertIntoBuilderBeforeMissing() { var builder = _container.RegisterCollectionBuilder() .Append(); @@ -357,7 +282,7 @@ namespace Umbraco.Tests.DI } [Test] - public void ScopeIsApplication() + public void ScopeBuilderCreatesScopedCollection() { _container.RegisterCollectionBuilder() .Append() @@ -377,7 +302,7 @@ namespace Umbraco.Tests.DI } [Test] - public void ScopeIsTransient() + public void TransientBuilderCreatesTransientCollection() { _container.RegisterCollectionBuilder() .Append() @@ -397,7 +322,7 @@ namespace Umbraco.Tests.DI } [Test] - public void OrderOfTypes() + public void BuilderRespectsTypesOrder() { var builder = _container.RegisterCollectionBuilder() .Append() @@ -409,7 +334,7 @@ namespace Umbraco.Tests.DI } [Test] - public void ScopeIsScope() + public void ScopeBuilderRespectsContainerScope() { _container.RegisterCollectionBuilder() .Append() @@ -440,7 +365,7 @@ namespace Umbraco.Tests.DI } [Test] - public void Weights() + public void WeightedBuilderCreatesWeightedCollection() { var builder = _container.RegisterCollectionBuilder() .Add() @@ -450,6 +375,8 @@ namespace Umbraco.Tests.DI AssertCollection(col, typeof(Resolved2), typeof(Resolved1)); } + #region Assertions + private static void AssertCollection(IEnumerable col, params Type[] expected) { var colA = col.ToArray(); @@ -481,5 +408,79 @@ namespace Umbraco.Tests.DI for (var i = 0; i < col1A.Length; i++) Assert.AreNotSame(col1A[i], col2A[i]); } + + #endregion + + #region Test Objects + + public abstract class Resolved + { } + + public class Resolved1 : Resolved + { } + + [Weight(5)] // default is 10 + public class Resolved2 : Resolved + { } + + public class Resolved3 : Resolved + { } + + public class Resolved4 // not! : Resolved + { } + + // ReSharper disable once ClassNeverInstantiated.Local + private class TestCollectionBuilder : OrderedCollectionBuilderBase + { + public TestCollectionBuilder(IServiceContainer container) + : base(container) + { } + + protected override TestCollectionBuilder This => this; + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class TestCollectionBuilderTransient : OrderedCollectionBuilderBase + { + public TestCollectionBuilderTransient(IServiceContainer container) + : base(container) + { } + + protected override TestCollectionBuilderTransient This => this; + + protected override ILifetime CollectionLifetime => null; // transient + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class TestCollectionBuilderScope : OrderedCollectionBuilderBase + { + public TestCollectionBuilderScope(IServiceContainer container) + : base(container) + { } + + protected override TestCollectionBuilderScope This => this; + + protected override ILifetime CollectionLifetime => new PerScopeLifetime(); + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class TestCollectionBuilderWeighted : WeightedCollectionBuilderBase + { + public TestCollectionBuilderWeighted(IServiceContainer container) + : base(container) + { } + + protected override TestCollectionBuilderWeighted This => this; + } + + // ReSharper disable once ClassNeverInstantiated.Local + private class TestCollection : BuilderCollectionBase + { + public TestCollection(IEnumerable items) + : base(items) + { } + } + + #endregion } } diff --git a/src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs b/src/Umbraco.Tests/Composing/ComposingTestBase.cs similarity index 85% rename from src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs rename to src/Umbraco.Tests/Composing/ComposingTestBase.cs index dc769e8be0..37f4df268c 100644 --- a/src/Umbraco.Tests/DependencyInjection/ResolverBaseTest.cs +++ b/src/Umbraco.Tests/Composing/ComposingTestBase.cs @@ -6,11 +6,12 @@ using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Logging; -namespace Umbraco.Tests.DI +namespace Umbraco.Tests.Composing { - public abstract class ResolverBaseTest // fixme rename, do something! + public abstract class ComposingTestBase { protected TypeLoader TypeLoader { get; private set; } + protected ProfilingLogger ProfilingLogger { get; private set; } [SetUp] @@ -18,9 +19,7 @@ namespace Umbraco.Tests.DI { ProfilingLogger = new ProfilingLogger(Mock.Of(), Mock.Of()); - TypeLoader = new TypeLoader(NullCacheProvider.Instance, - ProfilingLogger, - false) + TypeLoader = new TypeLoader(NullCacheProvider.Instance, ProfilingLogger, detectChanges: false) { AssembliesToScan = AssembliesToScan }; diff --git a/src/Umbraco.Tests/DependencyInjection/LazyCollectionBuilderTests.cs b/src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs similarity index 92% rename from src/Umbraco.Tests/DependencyInjection/LazyCollectionBuilderTests.cs rename to src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs index cf1ebbabaa..7a39186fea 100644 --- a/src/Umbraco.Tests/DependencyInjection/LazyCollectionBuilderTests.cs +++ b/src/Umbraco.Tests/Composing/LazyCollectionBuilderTests.cs @@ -6,7 +6,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Composing; -namespace Umbraco.Tests.DI +namespace Umbraco.Tests.Composing { [TestFixture] public class LazyCollectionBuilderTests @@ -28,7 +28,7 @@ namespace Umbraco.Tests.DI // so we don't have a test for duplicates as we had with resolvers in v7 [Test] - public void LazyCollectionBuilderTypes() + public void LazyCollectionBuilderHandlesTypes() { var container = new ServiceContainer(); container.ConfigureUmbracoCore(); @@ -52,7 +52,7 @@ namespace Umbraco.Tests.DI } [Test] - public void LazyCollectionBuilderProducers() + public void LazyCollectionBuilderHandlesProducers() { var container = new ServiceContainer(); container.ConfigureUmbracoCore(); @@ -75,7 +75,7 @@ namespace Umbraco.Tests.DI } [Test] - public void LazyCollectionBuilderBoth() + public void LazyCollectionBuilderHandlesTypesAndProducers() { var container = new ServiceContainer(); container.ConfigureUmbracoCore(); @@ -99,7 +99,7 @@ namespace Umbraco.Tests.DI } [Test] - public void LazyCollectionBuilderThrows() + public void LazyCollectionBuilderThrowsOnIllegalTypes() { var container = new ServiceContainer(); container.ConfigureUmbracoCore(); @@ -121,7 +121,7 @@ namespace Umbraco.Tests.DI } [Test] - public void LazyCollectionBuilderExclude() + public void LazyCollectionBuilderCanExcludeTypes() { var container = new ServiceContainer(); container.ConfigureUmbracoCore(); @@ -145,7 +145,7 @@ namespace Umbraco.Tests.DI Assert.IsFalse(values.Contains(o1)); // transient } - #region Test classes + #region Test Objects private interface ITestInterface { } @@ -162,6 +162,7 @@ namespace Umbraco.Tests.DI private class TransientObject4 { } + // ReSharper disable once ClassNeverInstantiated.Local private class TestCollectionBuilder : LazyCollectionBuilderBase { public TestCollectionBuilder(IServiceContainer container) @@ -173,6 +174,7 @@ namespace Umbraco.Tests.DI protected override ILifetime CollectionLifetime => null; // transient } + // ReSharper disable once ClassNeverInstantiated.Local private class TestCollection : BuilderCollectionBase { public TestCollection(IEnumerable items) diff --git a/src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs b/src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs similarity index 82% rename from src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs rename to src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs index 0e109d0217..e2145f557a 100644 --- a/src/Umbraco.Tests/DependencyInjection/PackageActionCollectionTests.cs +++ b/src/Umbraco.Tests/Composing/PackageActionCollectionTests.cs @@ -6,17 +6,13 @@ using NUnit.Framework; using Umbraco.Core.Composing; using Umbraco.Core._Legacy.PackageActions; -namespace Umbraco.Tests.DI +namespace Umbraco.Tests.Composing { [TestFixture] - public class PackageActionCollectionTests : ResolverBaseTest + public class PackageActionCollectionTests : ComposingTestBase { - // NOTE - // ManyResolverTests ensure that we'll get our actions back and PackageActionResolver works, - // so all we're testing here is that plugin manager _does_ find our package actions - // which should be ensured by PlugingManagerTests anyway, so this is useless? [Test] - public void FindAllPackageActions() + public void PackageActionCollectionBuilderWorks() { var container = new ServiceContainer(); container.ConfigureUmbracoCore(); @@ -34,7 +30,7 @@ namespace Umbraco.Tests.DI Assert.IsTrue(hasAction2); } - #region Classes for tests + #region Test Objects public class PackageAction1 : IPackageAction { diff --git a/src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs b/src/Umbraco.Tests/Composing/XsltExtensionCollectionTests.cs similarity index 62% rename from src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs rename to src/Umbraco.Tests/Composing/XsltExtensionCollectionTests.cs index e86d641d12..399b1df7bb 100644 --- a/src/Umbraco.Tests/DependencyInjection/XsltExtensionsResolverTests.cs +++ b/src/Umbraco.Tests/Composing/XsltExtensionCollectionTests.cs @@ -4,18 +4,13 @@ using NUnit.Framework; using Umbraco.Core.Macros; using Umbraco.Web; -namespace Umbraco.Tests.DI +namespace Umbraco.Tests.Composing { [TestFixture] - public class XsltExtensionsResolverTests : ResolverBaseTest + public class XsltExtensionCollectionTests : ComposingTestBase { - // NOTE - // ManyResolverTests ensure that we'll get our actions back and ActionsResolver works, - // so all we're testing here is that plugin manager _does_ find our actions - // which should be ensured by PlugingManagerTests anyway, so this is useless? - // maybe not as it seems to handle the "instance" thing... so we test that we respect the singleton? [Test] - public void Find_All_Extensions() + public void XsltExtensionsCollectionBuilderWorks() { var container = new ServiceContainer(); var builder = new XsltExtensionCollectionBuilder(container); @@ -30,19 +25,16 @@ namespace Umbraco.Tests.DI Assert.AreEqual("test2", extensions.Single(x => x.ExtensionObject.GetType() == typeof(XsltEx2)).Namespace); } - #region Classes for tests + #region Test Objects - [Umbraco.Core.Macros.XsltExtension("test1")] + [XsltExtension("test1")] public class XsltEx1 - { - - } + { } //test with legacy one [umbraco.XsltExtension("test2")] public class XsltEx2 - { - } + { } #endregion } diff --git a/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs b/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs deleted file mode 100644 index 9ae1ddb6b6..0000000000 --- a/src/Umbraco.Tests/DependencyInjection/ActionCollectionTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System.Linq; -using NUnit.Framework; -using Umbraco.Web; -using Umbraco.Web.UI.Pages; -using Umbraco.Web._Legacy.Actions; - -namespace Umbraco.Tests.DI -{ - [TestFixture] - public class ActionCollectionTests : ResolverBaseTest - { - // NOTE - // ManyResolverTests ensure that we'll get our actions back and ActionsResolver works, - // so all we're testing here is that plugin manager _does_ find our actions - // which should be ensured by PlugingManagerTests anyway, so this is useless? - // maybe not as it seems to handle the "instance" thing... so we test that we respect the singleton? - [Test] - public void FindAllActions() - { - var collectionBuilder = new ActionCollectionBuilder(); - collectionBuilder.SetProducer(() => TypeLoader.GetActions()); - - var actions = collectionBuilder.CreateCollection(); - Assert.AreEqual(2, actions.Count()); - - // order is unspecified, but both must be there - var hasAction1 = actions.ElementAt(0) is SingletonAction || actions.ElementAt(1) is SingletonAction; - var hasAction2 = actions.ElementAt(0) is NonSingletonAction || actions.ElementAt(1) is NonSingletonAction; - Assert.IsTrue(hasAction1); - Assert.IsTrue(hasAction2); - - var action = (SingletonAction)(actions.ElementAt(0) is SingletonAction ? actions.ElementAt(0) : actions.ElementAt(1)); - - // ensure we respect the singleton - Assert.AreSame(SingletonAction.Instance, action); - } - - #region Classes for tests - - public class SingletonAction : IAction - { - //create singleton - private static readonly SingletonAction instance = new SingletonAction(); - - public static SingletonAction Instance - { - get { return instance; } - } - - #region IAction Members - - public char Letter - { - get - { - return 'I'; - } - } - - public string JsFunctionName - { - get - { - return string.Format("{0}.actionAssignDomain()", ClientTools.Scripts.GetAppActions); - } - } - - public string JsSource - { - get - { - return null; - } - } - - public string Alias - { - get - { - return "assignDomain"; - } - } - - public string Icon - { - get - { - return ".sprDomain"; - } - } - - public bool ShowInNotifier - { - get - { - return false; - } - } - public bool CanBePermissionAssigned - { - get - { - return true; - } - } - #endregion - } - - public class NonSingletonAction : IAction - { - #region IAction Members - - public char Letter - { - get - { - return 'Q'; - } - } - - public string JsFunctionName - { - get - { - return string.Format("{0}.actionAssignDomain()", ClientTools.Scripts.GetAppActions); - } - } - - public string JsSource - { - get - { - return null; - } - } - - public string Alias - { - get - { - return "asfasdf"; - } - } - - public string Icon - { - get - { - return ".sprDomain"; - } - } - - public bool ShowInNotifier - { - get - { - return false; - } - } - public bool CanBePermissionAssigned - { - get - { - return true; - } - } - #endregion - } - - #endregion - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index e8c5361a48..0ca9803269 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -319,7 +319,7 @@ - + @@ -428,7 +428,7 @@ - + @@ -508,7 +508,7 @@ - + @@ -521,8 +521,8 @@ - - + + @@ -581,7 +581,7 @@ - + diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 289a7b19bf..981c026ed9 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -201,7 +201,7 @@ namespace Umbraco.Web.Editors // get the new configuration as a dictionary (this is how we get it from model) // and map it to an actual configuration object var currentConfiguration = dataType.PersistedDataType.Configuration; - var configurationDictionary = dataType.ConfigurationFields.ToDictionary(x => x.Key, x => x.Value); + var configurationDictionary = dataType.ConfigurationFields.ToDictionary(x => x.Key, x => x.Value); // fixme tokens! var configuration = dataType.PropertyEditor.ConfigurationEditor.FromEditor(configurationDictionary, currentConfiguration); dataType.PersistedDataType.Configuration = configuration; diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeConfigurationFieldSave.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeConfigurationFieldSave.cs index a1887326fe..85984df281 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DataTypeConfigurationFieldSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeConfigurationFieldSave.cs @@ -1,4 +1,5 @@ using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; namespace Umbraco.Web.Models.ContentEditing { @@ -9,15 +10,15 @@ namespace Umbraco.Web.Models.ContentEditing public class DataTypeConfigurationFieldSave { /// - /// Gets the configuration field key. + /// Gets or sets the configuration field key. /// [DataMember(Name = "key", IsRequired = true)] public string Key { get; set; } /// - /// Gets the configuration field value. + /// Gets or sets the configuration field value. /// [DataMember(Name = "value", IsRequired = true)] - public object Value { get; set; } // fixme - what's a value? + public object Value { get; set; } } } diff --git a/src/Umbraco.Web/PropertyEditors/DateConfiguration.cs b/src/Umbraco.Web/PropertyEditors/DateConfiguration.cs index 874af6f58b..5739e40150 100644 --- a/src/Umbraco.Web/PropertyEditors/DateConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/DateConfiguration.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.PropertyEditors; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperConfiguration.cs index 3f1c90bbc4..b230f48f81 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperConfiguration.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.PropertyEditors; +using Newtonsoft.Json; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { @@ -7,8 +8,19 @@ namespace Umbraco.Web.PropertyEditors /// public class ImageCropperConfiguration { - // fixme should not be a string! [ConfigurationField("crops", "Crop sizes", "views/propertyeditors/imagecropper/imagecropper.prevalues.html")] - public string Crops { get; set; } + public ImageCropperCrop[] Crops { get; set; } + + public class ImageCropperCrop + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 8dfb99422a..f6327a70c2 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -168,8 +168,7 @@ namespace Umbraco.Web.PropertyEditors // more magic here ;-( var configuration = dataTypeService.GetDataType(propertyType.DataTypeId).ConfigurationAs(); - var crops = configuration?.Crops; // fixme but Crops should not be a string and then we'd serialize them - if (string.IsNullOrWhiteSpace(crops)) crops = "[]"; + var crops = configuration?.Crops ?? Array.Empty(); return "{src: '" + val + "', crops: " + crops + "}"; } } diff --git a/src/Umbraco.Web/PropertyEditors/LabelConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/LabelConfigurationEditor.cs index 24eb14ef3b..f5347beb38 100644 --- a/src/Umbraco.Web/PropertyEditors/LabelConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/LabelConfigurationEditor.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.PropertyEditors; @@ -19,12 +18,10 @@ namespace Umbraco.Web.PropertyEditors // not simply deserializing Json because we want to validate the valueType if (editorValue.TryGetValue(Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObj) - && valueTypeObj is JToken jtoken - && jtoken.Type == JTokenType.String) + && valueTypeObj is string stringValue) { - var valueType = jtoken.Value(); - if (!string.IsNullOrWhiteSpace(valueType) && ValueTypes.IsValue(valueType)) // validate - newConfiguration.ValueType = valueType; + if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) // validate + newConfiguration.ValueType = stringValue; } return newConfiguration; diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index b4ac776d7b..cec8f232d3 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -9,39 +9,30 @@ namespace Umbraco.Web.PropertyEditors public class ListViewConfiguration { [ConfigurationField("pageSize", "Page Size", "number", Description = "Number of items per page")] - [JsonProperty("pageSize")] public int PageSize { get; set; } [ConfigurationField("displayAtTabNumber", "Display At Tab Number", "number", Description = "Which tab position that the list of child items will be displayed")] - [JsonProperty("displayAtNumber")] public int DisplayAtTabNumber { get; set; } [ConfigurationField("orderBy", "Order By", "views/propertyeditors/listview/sortby.prevalues.html", Description = "The default sort order for the list")] - [JsonProperty("orderBy")] public string OrderBy { get; set; } [ConfigurationField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderdirection.prevalues.html")] - [JsonProperty("orderDirection")] public string OrderDirection { get; set; } - [ConfigurationField("includeProperties", "Columns Displayed", "views/propertyeditors/listview/includeproperties.prevalues.html", Description = "The properties that will be displayed for each column")] - [JsonProperty("includeProperties")] public Property[] IncludeProperties { get; set; } [ConfigurationField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] - [JsonProperty("layouts")] public Layout[] Layouts { get; set; } [ConfigurationField("bulkActionPermissions", "Bulk Action Permissions", "views/propertyeditors/listview/bulkactionpermissions.prevalues.html", Description = "The bulk actions that are allowed from the list view")] - [JsonProperty("bulkActionPermissions")] public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new BulkActionPermissionSettings(); // fixme managing defaults? [ConfigurationField("tabName", "Tab Name", "textstring", Description = "The name of the listview tab (default if empty: 'Child Items')")] - [JsonProperty("tabName")] public int TabName { get; set; } public class Property diff --git a/src/Umbraco.Web/PropertyEditors/TagConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/TagConfigurationEditor.cs index 136afbb90a..fc8c8100eb 100644 --- a/src/Umbraco.Web/PropertyEditors/TagConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagConfigurationEditor.cs @@ -38,7 +38,9 @@ namespace Umbraco.Web.PropertyEditors var dictionary = base.ToEditor(configuration); // the front-end editor expects the string value of the storage type - dictionary["storageType"] = dictionary["storageType"].ToString(); + if (!dictionary.TryGetValue("storageType", out var storageType)) + storageType = TagsStorageType.Csv; + dictionary["storageType"] = storageType.ToString(); return dictionary; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs index 3ffbaeaee9..e8c429198b 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueListConfigurationEditor.cs @@ -49,7 +49,11 @@ namespace Umbraco.Web.PropertyEditors /// public override Dictionary ToEditor(ValueListConfiguration configuration) { - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (configuration == null) + return new Dictionary + { + { "items", new object() } + }; // map to what the (still v7) editor expects // {"item":{"169":{"value":"a","sortOrder":1},"170":{"value":"b","sortOrder":2},"171":{"value":"c","sortOrder":3}}}