diff --git a/src/Our.Umbraco.Ditto/Extensions/Internal/PropertyInfoExtensions.cs b/src/Our.Umbraco.Ditto/Extensions/Internal/PropertyInfoExtensions.cs index a00950a..962a568 100644 --- a/src/Our.Umbraco.Ditto/Extensions/Internal/PropertyInfoExtensions.cs +++ b/src/Our.Umbraco.Ditto/Extensions/Internal/PropertyInfoExtensions.cs @@ -66,8 +66,13 @@ public static bool IsMappable(this PropertyInfo source) /// True if a lazy load attempt should be make public static bool ShouldAttemptLazyLoad(this PropertyInfo source) { - return source.HasCustomAttribute() || - ((source.DeclaringType.HasCustomAttribute() || Ditto.LazyLoadStrategy == LazyLoad.AllVirtuals) + if (source.HasCustomAttribute()) + { + return false; + } + + return source.HasCustomAttribute() || + ((source.DeclaringType.HasCustomAttribute() || Ditto.LazyLoadStrategy == LazyLoad.AllVirtuals) && source.IsVirtualAndOverridable()); } } diff --git a/src/Our.Umbraco.Ditto/Extensions/PublishedContentExtensions.cs b/src/Our.Umbraco.Ditto/Extensions/PublishedContentExtensions.cs index cd8eb37..a44d57b 100644 --- a/src/Our.Umbraco.Ditto/Extensions/PublishedContentExtensions.cs +++ b/src/Our.Umbraco.Ditto/Extensions/PublishedContentExtensions.cs @@ -8,7 +8,6 @@ using System.Reflection; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Web; namespace Our.Umbraco.Ditto { @@ -197,7 +196,7 @@ public static class PublishedContentExtensions // Ensure instance is of target type if (instance != null && !type.IsInstanceOfType(instance)) { - throw new ArgumentException($"The instance parameter does not implement Type '{type.Name}'", "instance"); + throw new ArgumentException($"The instance parameter does not implement Type '{type.Name}'", nameof(instance)); } // Get the context accessor (for access to ApplicationContext, UmbracoContext, et al) @@ -206,15 +205,9 @@ public static class PublishedContentExtensions // Check if the culture has been set, otherwise use from Umbraco, or fallback to a default if (culture == null) { - if (contextAccessor.UmbracoContext != null && contextAccessor.UmbracoContext.PublishedContentRequest != null) - { - culture = contextAccessor.UmbracoContext.PublishedContentRequest.Culture; - } - else - { - // Fallback - culture = CultureInfo.CurrentCulture; - } + culture = contextAccessor.UmbracoContext?.PublishedContentRequest != null + ? contextAccessor.UmbracoContext.PublishedContentRequest.Culture + : CultureInfo.CurrentCulture; } // Ensure a chain context @@ -285,99 +278,114 @@ public static class PublishedContentExtensions Action onConverted, DittoChainContext chainContext) { - // Get the default constructor, parameters and create an instance of the type. + // Collect all the properties of the given type and loop through writable ones. + PropertyInfo[] properties; + PropertyCache.TryGetValue(type, out properties); + + if (properties == null) + { + properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.CanWrite && x.GetSetMethod() != null).ToArray(); + + PropertyCache.TryAdd(type, properties); + } + + // Check the validity of the mpped type constructor as early as possible. ParameterInfo[] constructorParams = type.GetConstructorParameters(); + bool validConstructor = false; bool hasParameter = false; + bool isType = false; + bool hasLazy = false; - // If not already an instance, create an instance of the object - if (instance == null) + if (constructorParams != null) { - if (constructorParams != null && constructorParams.Length == 0) - { - // Internally this uses Activator.CreateInstance which is heavily optimized. - instance = type.GetInstance(); - } - else if (constructorParams != null && constructorParams.Length == 1 && constructorParams[0].ParameterType == typeof(IPublishedContent)) + // Is it PublishedContentmModel or similar? + if (constructorParams.Length == 1 && constructorParams[0].ParameterType == typeof(IPublishedContent)) { - // This extension method is about 7x faster than the native implementation. - instance = type.GetInstance(content); hasParameter = true; } - else + + if (constructorParams.Length == 0 || hasParameter) { - // No valid constructor, but see if the value can be cast to the type - if (type.IsInstanceOfType(content)) - { - instance = content; - } - else - { - throw new InvalidOperationException($"Can't convert IPublishedContent to {type} as it has no valid constructor. A valid constructor is either an empty one, or one accepting a single IPublishedContent parameter."); - } + validConstructor = true; } } - // Collect all the properties of the given type and loop through writable ones. - PropertyInfo[] properties; - PropertyCache.TryGetValue(type, out properties); - - if (properties == null) + // No valid constructor, but see if the value can be cast to the type + if (type.IsInstanceOfType(content)) { - properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => x.CanWrite && x.GetSetMethod() != null).ToArray(); + isType = true; + validConstructor = true; + } - PropertyCache.TryAdd(type, properties); + if (!validConstructor) + { + throw new InvalidOperationException( + $"Cannot convert IPublishedContent to {type} as it has no valid constructor. " + + "A valid constructor is either an empty one, or one accepting a single IPublishedContent parameter."); } // Gets the default processor for this conversion. var defaultProcessorType = DittoProcessorRegistry.Instance.GetDefaultProcessorType(type); + PropertyInfo[] lazyProperties = null; - // A dictionary to store lazily invoked values. - var lazyProperties = new Dictionary>(); - - // process lazy load properties first - foreach (var propertyInfo in properties.Where(x => x.ShouldAttemptLazyLoad())) + // If not already an instance, create an instance of the object + if (instance == null) { - // Configure lazy properties - using (DittoDisposableTimer.DebugDuration($"Lazy Property ({content.Id} {propertyInfo.Name})")) - { - // Ensure it's a virtual property (Only relevant to property level lazy loads) - if (!propertyInfo.IsVirtualAndOverridable()) - { - throw new InvalidOperationException($"Lazy property '{propertyInfo.Name}' of type '{type.AssemblyQualifiedName}' must be declared virtual in order to be lazy loadable."); - } + // We can only proxy new instances. + lazyProperties = properties.Where(x => x.ShouldAttemptLazyLoad()).ToArray(); - // Check for the ignore attribute (Only relevant to class level lazy loads). - if (propertyInfo.HasCustomAttribute()) - { - continue; - } + if (lazyProperties.Any()) + { + hasLazy = true; - // Create a Lazy to deferr returning our value. - var deferredPropertyInfo = propertyInfo; - var localInstance = instance; + var factory = new ProxyFactory(); + instance = hasParameter + ? factory.CreateProxy(type, lazyProperties.Select(x => x.Name), content) + : factory.CreateProxy(type, lazyProperties.Select(x => x.Name)); - // ReSharper disable once PossibleMultipleEnumeration - lazyProperties.Add(propertyInfo.Name, new Lazy(() => GetProcessedValue(content, culture, type, deferredPropertyInfo, localInstance, defaultProcessorType, contextAccessor, chainContext))); } - } - - if (lazyProperties.Any()) - { - // Create a proxy instance to replace our object. - var interceptor = new LazyInterceptor(instance, lazyProperties); - var factory = new ProxyFactory(); - - instance = hasParameter - ? factory.CreateProxy(type, interceptor, content) - : factory.CreateProxy(type, interceptor); + else if (isType) + { + instance = content; + } + else + { + // 1: This extension method is about 7x faster than the native implementation. + // 2: Internally this uses Activator.CreateInstance which is heavily optimized. + instance = hasParameter + ? type.GetInstance(content) // 1 + : type.GetInstance(); // 2 + } } // We have the instance object but haven't yet populated properties // so fire the on converting event handlers OnConverting(content, type, culture, instance, onConverting); - // Process any non lazy properties next + if (hasLazy) + { + // A dictionary to store lazily invoked values. + var lazyMappings = new Dictionary>(); + foreach (var propertyInfo in lazyProperties) + { + // Configure lazy properties + using (DittoDisposableTimer.DebugDuration($"Lazy Property ({content.Id} {propertyInfo.Name})")) + { + // Ensure it's a virtual property (Only relevant to property level lazy loads) + if (!propertyInfo.IsVirtualAndOverridable()) + { + throw new InvalidOperationException($"Lazy property '{propertyInfo.Name}' of type '{type.AssemblyQualifiedName}' must be declared virtual in order to be lazy loadable."); + } + + lazyMappings.Add(propertyInfo.Name, new Lazy(() => GetProcessedValue(content, culture, type, propertyInfo, instance, defaultProcessorType, contextAccessor, chainContext))); + } + } + + ((IProxy)instance).Interceptor = new LazyInterceptor(lazyMappings); + } + + // Process any non lazy properties foreach (var propertyInfo in properties.Where(x => !x.ShouldAttemptLazyLoad())) { // Check for the ignore attribute. @@ -387,7 +395,6 @@ public static class PublishedContentExtensions } // Set the value normally. - // ReSharper disable once PossibleMultipleEnumeration var value = GetProcessedValue(content, culture, type, propertyInfo, instance, defaultProcessorType, contextAccessor, chainContext); // This over 4x faster as propertyInfo.SetValue(instance, value, null); diff --git a/src/Our.Umbraco.Ditto/Proxy/LazyInterceptor.cs b/src/Our.Umbraco.Ditto/Proxy/LazyInterceptor.cs index d9e325d..fa1171f 100644 --- a/src/Our.Umbraco.Ditto/Proxy/LazyInterceptor.cs +++ b/src/Our.Umbraco.Ditto/Proxy/LazyInterceptor.cs @@ -9,75 +9,57 @@ namespace Our.Umbraco.Ditto /// internal class LazyInterceptor : IInterceptor { - /// - /// The lazy dictionary. - /// private readonly Dictionary> lazyDictionary = new Dictionary>(); - - /// - /// The base target instance from which the proxy type is derived. - /// - private readonly object target; + private readonly Dictionary nonLazyDictionary = new Dictionary(); /// /// Initializes a new instance of the class. /// - /// - /// The base target instance from which the proxy type is derived. - /// /// /// The dictionary of values containing the property name to replace and the value to replace it with. /// - public LazyInterceptor(object target, Dictionary> values) + public LazyInterceptor(Dictionary> values) { - this.target = target; - foreach (KeyValuePair> pair in values) { this.lazyDictionary.Add(pair.Key, pair.Value); } } - /// - /// Intercepts the in the proxy to return a replaced value. - /// - /// - /// The containing information about the current - /// invoked property. - /// - /// - /// The object to set the to if it is a setter. - /// - /// - /// The replacing the original implementation value. - /// + /// public object Intercept(MethodBase methodBase, object value) { - const string Getter = "get_"; - const string Setter = "set_"; - var name = methodBase.Name; - var key = name.Substring(4); - var parameters = value == null ? new object[] { } : new[] { value }; + const string getter = "get_"; + const string setter = "set_"; + string name = methodBase.Name; + string key = name.Substring(4); // Attempt to get the value from the lazy members. - if (name.StartsWith(Getter)) + if (name.StartsWith(getter)) { if (this.lazyDictionary.ContainsKey(key)) { return this.lazyDictionary[key].Value; } + + if (this.nonLazyDictionary.ContainsKey(key)) + { + return this.nonLazyDictionary[key]; + } } // Set the value, remove the old lazy value. - if (name.StartsWith(Setter)) + if (name.StartsWith(setter)) { if (this.lazyDictionary.ContainsKey(key)) { this.lazyDictionary.Remove(key); } + + this.nonLazyDictionary[key] = value; } - return methodBase.Invoke(this.target, parameters); + return null; } } } \ No newline at end of file diff --git a/src/Our.Umbraco.Ditto/Proxy/PropertyEmitter.cs b/src/Our.Umbraco.Ditto/Proxy/PropertyEmitter.cs index 1b4d52d..a2cb04c 100644 --- a/src/Our.Umbraco.Ditto/Proxy/PropertyEmitter.cs +++ b/src/Our.Umbraco.Ditto/Proxy/PropertyEmitter.cs @@ -13,14 +13,15 @@ internal static class PropertyEmitter /// /// The get interceptor method. /// - private static readonly MethodInfo GetInterceptor = typeof(IProxy).GetProperty("Interceptor").GetGetMethod(); + // ReSharper disable once PossibleNullReferenceException + private static readonly MethodInfo GetInterceptor = typeof(IProxy).GetProperty(nameof(IProxy.Interceptor)).GetGetMethod(); /// /// The intercept handler method. /// private static readonly MethodInfo InterceptorMethod = typeof(IInterceptor).GetMethod( "Intercept", - new[] { typeof(MethodBase), typeof(object) }); + new[] { typeof(MethodBase), typeof(IProxy) }); /// /// The get method from handle method. @@ -28,10 +29,10 @@ internal static class PropertyEmitter private static readonly MethodInfo GetMethodFromHandle = typeof(MethodBase).GetMethod("GetMethodFromHandle", new[] { typeof(RuntimeMethodHandle) }); /// - /// The constructor. + /// The constructor. /// - private static readonly ConstructorInfo NotImplementedConstructor = - typeof(NotImplementedException).GetConstructor(new Type[0]); + private static readonly ConstructorInfo ArgumentNullExceptionConstructor = + typeof(ArgumentNullException).GetConstructor(new Type[0]); /// /// Uses reflection to emit the given body for interception. @@ -52,14 +53,14 @@ public static void Emit(TypeBuilder typeBuilder, MethodInfo method, FieldInfo in ParameterInfo parameter = parameters.FirstOrDefault(); // Define attributes. - const MethodAttributes MethodAttributes = MethodAttributes.Public | + const MethodAttributes methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual; // Define the method. MethodBuilder methodBuilder = typeBuilder.DefineMethod( method.Name, - MethodAttributes, + methodAttributes, CallingConventions.HasThis, method.ReturnType, parameters.Select(param => param.ParameterType).ToArray()); @@ -74,37 +75,46 @@ public static void Emit(TypeBuilder typeBuilder, MethodInfo method, FieldInfo in // IInterceptor interceptor = ((IProxy)this).Interceptor; // if (interceptor == null) // { - // throw new NotImplementedException(); + // throw new ArgumentNullException(); // } - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Callvirt, GetInterceptor); + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Callvirt, GetInterceptor); // .Interceptor Label skipThrow = il.DefineLabel(); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Bne_Un, skipThrow); - il.Emit(OpCodes.Newobj, NotImplementedConstructor); + il.Emit(OpCodes.Newobj, ArgumentNullExceptionConstructor); il.Emit(OpCodes.Throw); il.MarkLabel(skipThrow); - // This is equivalent to: - // For get + // This is equivalent to: // return return (PropertyType) interceptor.Intercept(methodof(BaseType.get_Property), null); - // For set - // interceptor.Intercept(methodof(BaseType.set_Property), value); il.Emit(OpCodes.Ldtoken, method); il.Emit(OpCodes.Call, GetMethodFromHandle); - il.Emit(parameter == null ? OpCodes.Ldnull : OpCodes.Ldarg_1); - il.Emit(OpCodes.Callvirt, InterceptorMethod); - // Setter only. - if (method.ReturnType == typeof(void)) + if (parameter == null) { - il.Emit(OpCodes.Pop); + // Getter + // return interceptor.Intercept(MethodBase.GetMethodFromHandle(typeof(BaseType).GetMethod("get_PropertyName").MethodHandle), null); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Callvirt, InterceptorMethod); + + // Unbox the object back to the correct type. + il.Emit(OpCodes.Unbox_Any, method.ReturnType); } else { - // Unbox the object back to the corrct type. - il.Emit(OpCodes.Unbox_Any, method.ReturnType); + // Setter + // interceptor.Intercept(MethodBase.GetMethodFromHandle(typeof(BaseType).GetMethod("set_PropertyName", new Type[] { typeof(PropertyType) }).MethodHandle), value); + il.Emit(OpCodes.Ldarg_1); + + if (parameter.ParameterType.IsValueType) + { + il.Emit(OpCodes.Box, parameter.ParameterType); + } + + il.Emit(OpCodes.Callvirt, InterceptorMethod); + il.Emit(OpCodes.Pop); // Clear the stack } il.Emit(OpCodes.Ret); diff --git a/src/Our.Umbraco.Ditto/Proxy/ProxyFactory.cs b/src/Our.Umbraco.Ditto/Proxy/ProxyFactory.cs index ff548bb..75918bf 100644 --- a/src/Our.Umbraco.Ditto/Proxy/ProxyFactory.cs +++ b/src/Our.Umbraco.Ditto/Proxy/ProxyFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -28,38 +29,36 @@ public class ProxyFactory /// Creates an instance of the proxy class for the given . /// /// The base to proxy. - /// The to intercept properties with. + /// The collection of property names to map. /// The to pass as a parameter. /// /// The proxy instance. /// - public object CreateProxy(Type baseType, IInterceptor interceptor, IPublishedContent content = null) + public IProxy CreateProxy(Type baseType, IEnumerable properties, IPublishedContent content = null) { - Type proxyType = this.CreateProxyType(baseType); + Type proxyType = this.CreateProxyType(baseType, properties); object result = content == null ? proxyType.GetInstance() : proxyType.GetInstance(content); - IProxy proxy = (IProxy)result; - proxy.Interceptor = interceptor; - - return result; + return (IProxy)result; } /// /// Creates the proxy class or returns already created class from the cache. /// /// The base to proxy. + /// The collection of property names to map. /// /// The proxy . /// - private Type CreateProxyType(Type baseType) + private Type CreateProxyType(Type baseType, IEnumerable properties) { try { // ConcurrentDictionary.GetOrAdd() is not atomic so we'll be doubly sure. Locker.EnterWriteLock(); - return ProxyCache.GetOrAdd(baseType, c => this.CreateUncachedProxyType(baseType)); + return ProxyCache.GetOrAdd(baseType, c => this.CreateUncachedProxyType(baseType, properties)); } finally { @@ -70,38 +69,47 @@ private Type CreateProxyType(Type baseType) /// /// Creates an un-cached proxy class. /// - /// - /// The base to proxy. - /// + /// The base to proxy. + /// The collection of property names to map. /// /// The proxy . /// - private Type CreateUncachedProxyType(Type baseType) + private Type CreateUncachedProxyType(Type baseType, IEnumerable properties) { // Create a dynamic assembly and module to store the proxy. AppDomain currentDomain = AppDomain.CurrentDomain; - string typeName = string.Format("{0}Proxy", baseType.Name); - string assemblyName = string.Format("{0}Assembly", typeName); - string moduleName = string.Format("{0}Module", typeName); + string typeName = $"{baseType.Name}Proxy"; + string assemblyName = $"{typeName}Assembly"; + string moduleName = $"{typeName}Module"; // Define different behaviors for debug and release so that we can make debugging easier. - AssemblyName name = new AssemblyName(assemblyName); + var name = new AssemblyName(assemblyName); + AssemblyBuilder assemblyBuilder; #if DEBUG - AssemblyBuilder assemblyBuilder = currentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave); - ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName, string.Format("{0}.mod", moduleName), true); + assemblyBuilder = currentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName, $"{moduleName}.mod", true); + + // Add a debuggable attribute to the assembly saying to disable optimizations + Type daType = typeof(DebuggableAttribute); + ConstructorInfo daCtor = daType.GetConstructor(new[] { typeof(DebuggableAttribute.DebuggingModes) }); + var daBuilder = new CustomAttributeBuilder( + daCtor, + new object[] { DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.Default }); + assemblyBuilder.SetCustomAttribute(daBuilder); #else - AssemblyBuilder assemblyBuilder = currentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); + assemblyBuilder = currentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName); #endif + // Define type attributes - const TypeAttributes TypeAttributes = TypeAttributes.AutoClass | + const TypeAttributes typeAttributes = TypeAttributes.AutoClass | TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.BeforeFieldInit; // Define the type. - TypeBuilder typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes, baseType); + TypeBuilder typeBuilder = moduleBuilder.DefineType(typeName, typeAttributes, baseType); // Emit the default constructors for this proxy so that classes without parameterless constructors // can be proxied. @@ -114,17 +122,25 @@ private Type CreateUncachedProxyType(Type baseType) // Emit the IProxy IInterceptor property. FieldInfo interceptorField = InterceptorEmitter.Emit(typeBuilder); - // Emit each property that is to be intercepted. + // Collect and filter our list of properties to intercept. MethodInfo[] methods = baseType.GetMethods(BindingFlags.Public | BindingFlags.Instance); - IEnumerable proxyList = this.BuildPropertyList(methods); + IEnumerable proxyList = this.BuildPropertyList(methods) + .Where(m => properties.Contains(m.Name.Substring(4), StringComparer.OrdinalIgnoreCase)); + + // Emit each property that is to be intercepted. foreach (MethodInfo methodInfo in proxyList) { PropertyEmitter.Emit(typeBuilder, methodInfo, interceptorField); } // Create and return. - return typeBuilder.CreateType(); + Type result = typeBuilder.CreateType(); + +#if DEBUG + assemblyBuilder.Save(typeName + ".dll"); +#endif + return result; } /// @@ -138,7 +154,7 @@ private Type CreateUncachedProxyType(Type baseType) /// private IEnumerable BuildPropertyList(MethodInfo[] methodInfos) { - List proxyList = new List(); + var proxyList = new List(); // ReSharper disable once LoopCanBeConvertedToQuery foreach (MethodInfo method in methodInfos) @@ -175,9 +191,7 @@ private IEnumerable BuildPropertyList(MethodInfo[] methodInfos) /// /// Returns the parent if any, for the given . /// - /// - /// The . - /// + /// The . /// /// The , or null. /// @@ -188,10 +202,10 @@ private PropertyInfo GetParentProperty(MethodInfo method) { if (method == null) { - throw new ArgumentNullException("method"); + throw new ArgumentNullException(nameof(method)); } - const BindingFlags PropertyFlags = BindingFlags.Instance | BindingFlags.Public; + const BindingFlags propertyFlags = BindingFlags.Public | BindingFlags.Instance; bool takesArg = method.GetParameters().Length == 1; bool hasReturn = method.ReturnType != typeof(void); @@ -205,14 +219,14 @@ private PropertyInfo GetParentProperty(MethodInfo method) { if (method.DeclaringType != null) { - return method.DeclaringType.GetProperties(PropertyFlags) + return method.DeclaringType.GetProperties(propertyFlags) .FirstOrDefault(p => this.AreMethodsEqualForDeclaringType(p.GetSetMethod(), method)); } } if (method.DeclaringType != null) { - return method.DeclaringType.GetProperties(PropertyFlags) + return method.DeclaringType.GetProperties(propertyFlags) .FirstOrDefault(p => this.AreMethodsEqualForDeclaringType(p.GetGetMethod(), method)); } @@ -220,15 +234,11 @@ private PropertyInfo GetParentProperty(MethodInfo method) } /// - /// Returns a value indicating whether two instances of are equal + /// Returns a value indicating whether two instances of are equal /// for a declaring type. /// - /// - /// The first . - /// - /// - /// The second . - /// + /// The first . + /// The second . /// /// True if the two instances of are equal; otherwise, false. /// @@ -245,10 +255,7 @@ private bool AreMethodsEqualForDeclaringType(MethodInfo first, MethodInfo second first.GetParameters().Select(p => p.ParameterType).ToArray()); MethodBody body = first.GetMethodBody(); - if (body != null) - { - firstBytes = body.GetILAsByteArray(); - } + firstBytes = body.GetILAsByteArray(); } if (second != null && second.ReflectedType != null && second.DeclaringType != null) @@ -260,10 +267,7 @@ private bool AreMethodsEqualForDeclaringType(MethodInfo first, MethodInfo second second.GetParameters().Select(p => p.ParameterType).ToArray()); MethodBody body = second.GetMethodBody(); - if (body != null) - { - secondBytes = body.GetILAsByteArray(); - } + secondBytes = body.GetILAsByteArray(); } return firstBytes.SequenceEqual(secondBytes); diff --git a/tests/Our.Umbraco.Ditto.Tests/Our.Umbraco.Ditto.Tests.csproj b/tests/Our.Umbraco.Ditto.Tests/Our.Umbraco.Ditto.Tests.csproj index 4ab2ef7..0138ef1 100644 --- a/tests/Our.Umbraco.Ditto.Tests/Our.Umbraco.Ditto.Tests.csproj +++ b/tests/Our.Umbraco.Ditto.Tests/Our.Umbraco.Ditto.Tests.csproj @@ -199,6 +199,7 @@ + diff --git a/tests/Our.Umbraco.Ditto.Tests/ProxyFactoryTests.cs b/tests/Our.Umbraco.Ditto.Tests/ProxyFactoryTests.cs new file mode 100644 index 0000000..0b04885 --- /dev/null +++ b/tests/Our.Umbraco.Ditto.Tests/ProxyFactoryTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using NUnit.Framework; + +namespace Our.Umbraco.Ditto.Tests +{ + [TestFixture] + [Category("Proxy Generation")] + public class ProxyFactoryTests + { + [Test] + public void ProxyCanGetSetProperties() + { + var factory = new ProxyFactory(); + IProxy proxy = factory.CreateProxy(typeof(TestClass), new List { "id", "name" }); + proxy.Interceptor = new LazyInterceptor(new Dictionary>()); + + // This is the method we are replicating within the property emitter. + var tc = new TestClass(); + + var idMethod = + MethodBase.GetMethodFromHandle(typeof(TestClass).GetMethod("set_Id", new[] { typeof(int) }).MethodHandle); + + var nameMethod = + MethodBase.GetMethodFromHandle(typeof(TestClass).GetMethod("set_Name", new[] { typeof(string) }).MethodHandle); + + var dateMethod = + MethodBase.GetMethodFromHandle(typeof(TestClass).GetMethod("set_CreateDate", new[] { typeof(DateTime) }).MethodHandle); + + idMethod.Invoke(tc, new object[] { 1 }); + nameMethod.Invoke(tc, new object[] { "Foo" }); + dateMethod.Invoke(tc, new object[] { new DateTime(2017, 1, 1) }); + + Assert.AreEqual(1, tc.Id); + Assert.AreEqual("Foo", tc.Name); + Assert.AreEqual(new DateTime(2017, 1, 1), tc.CreateDate); + + // ReSharper disable once SuspiciousTypeConversion.Global + var testClass = (TestClass)proxy; + + testClass.Id = 1; + testClass.Name = "Foo"; + testClass.CreateDate = new DateTime(2017, 1, 1); + + Assert.AreEqual(1, testClass.Id); + Assert.AreEqual("Foo", testClass.Name); + Assert.AreEqual(new DateTime(2017, 1, 1), testClass.CreateDate); + } + } + + public class TestClass + { + public virtual int Id { get; set; } + + public virtual string Name { get; set; } + + public DateTime CreateDate { get; set; } + } +}