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

Fix proxy generation #225

Merged
merged 3 commits into from
Sep 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
159 changes: 83 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,114 @@ 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.
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<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 +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);
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