Skip to content
This repository has been archived by the owner on Dec 13, 2021. It is now read-only.

Commit

Permalink
Fix proxy generation
Browse files Browse the repository at this point in the history
  • Loading branch information
JimBobSquarePants committed Sep 4, 2017
1 parent 5c34ee0 commit 254c1e4
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ public static bool IsMappable(this PropertyInfo source)
/// <returns>True if a lazy load attempt should be make</returns>
public static bool ShouldAttemptLazyLoad(this PropertyInfo source)
{
return source.HasCustomAttribute<DittoLazyAttribute>() ||
((source.DeclaringType.HasCustomAttribute<DittoLazyAttribute>() || Ditto.LazyLoadStrategy == LazyLoad.AllVirtuals)
if (source.HasCustomAttribute<DittoIgnoreAttribute>())
{
return false;
}

return source.HasCustomAttribute<DittoLazyAttribute>() ||
((source.DeclaringType.HasCustomAttribute<DittoLazyAttribute>() || Ditto.LazyLoadStrategy == LazyLoad.AllVirtuals)
&& source.IsVirtualAndOverridable());
}
}
Expand Down
158 changes: 82 additions & 76 deletions src/Our.Umbraco.Ditto/Extensions/PublishedContentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.Reflection;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web;

namespace Our.Umbraco.Ditto
{
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -285,99 +278,113 @@ public static class PublishedContentExtensions
Action<DittoConversionHandlerContext> 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.
PropertyCache.TryGetValue(type, out var 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<string, Lazy<object>>();

// 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<Ditto>($"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<DittoIgnoreAttribute>())
{
continue;
}
if (lazyProperties.Any())
{
hasLazy = true;

// Create a Lazy<object> 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<object>(() => 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<string, Lazy<object>>();
foreach (var propertyInfo in lazyProperties)
{
// Configure lazy properties
using (DittoDisposableTimer.DebugDuration<Ditto>($"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<object>(() => 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.
Expand All @@ -387,7 +394,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);
Expand Down
52 changes: 17 additions & 35 deletions src/Our.Umbraco.Ditto/Proxy/LazyInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,57 @@ namespace Our.Umbraco.Ditto
/// </summary>
internal class LazyInterceptor : IInterceptor
{
/// <summary>
/// The lazy dictionary.
/// </summary>
private readonly Dictionary<string, Lazy<object>> lazyDictionary = new Dictionary<string, Lazy<object>>();

/// <summary>
/// The base target instance from which the proxy type is derived.
/// </summary>
private readonly object target;
private readonly Dictionary<string, object> nonLazyDictionary = new Dictionary<string, object>();

/// <summary>
/// Initializes a new instance of the <see cref="LazyInterceptor"/> class.
/// </summary>
/// <param name="target">
/// The base target instance from which the proxy type is derived.
/// </param>
/// <param name="values">
/// The dictionary of values containing the property name to replace and the value to replace it with.
/// </param>
public LazyInterceptor(object target, Dictionary<string, Lazy<object>> values)
public LazyInterceptor(Dictionary<string, Lazy<object>> values)
{
this.target = target;

foreach (KeyValuePair<string, Lazy<object>> pair in values)
{
this.lazyDictionary.Add(pair.Key, pair.Value);
}
}

/// <summary>
/// Intercepts the <see cref="MethodBase"/> in the proxy to return a replaced value.
/// </summary>
/// <param name="methodBase">
/// The <see cref="MethodBase"/> containing information about the current
/// invoked property.
/// </param>
/// <param name="value">
/// The object to set the <see cref="MethodBase"/> to if it is a setter.
/// </param>
/// <returns>
/// The <see cref="object"/> replacing the original implementation value.
/// </returns>
/// <inheritdoc />
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;
}
}
}
Loading

0 comments on commit 254c1e4

Please sign in to comment.