Skip to content

Commit

Permalink
Merge pull request #34 from kluhman/proxy-writing
Browse files Browse the repository at this point in the history
Proxy Writing and Recursive Support for IContent
  • Loading branch information
NerderyMGlanzer authored Oct 25, 2016
2 parents abfb615 + 1b785d1 commit de8b7e8
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 61 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## v1.3.1

* Fixes issue where proxied models created from IContent models did not support recursive hydration.
* Fixes issue where if a proxy property was overwritten with some value not from CMS, it would not persist and would still use CMS value.
* Fixes issue where preview content breaks due to use of a singleton UmbracoHelper. [#34]

## v1.3.0
Expand Down
6 changes: 6 additions & 0 deletions ReferenceWebsite/App_Start/RouteConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public static void RegisterRoutes(RouteCollection routes)
"Performance/{action}",
new { controller = "Performance", action = "RunProxyPerfTests" }
);

routes.MapRoute(
"Proxy Tests",
"ProxyTests/{action}",
new { controller = "ProxyTests", action = "ValueShouldMatchCmsContent" }
);
}
}
}
60 changes: 60 additions & 0 deletions ReferenceWebsite/Controllers/ProxyTestsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

using ReferenceWebsite.Models;

using UmbracoVault;
using UmbracoVault.Proxy;
using UmbracoVault.Reflection;

namespace ReferenceWebsite.Controllers
{
public class ProxyTestsController : Controller
{
public ActionResult ValueShouldMatchCmsContent()
{
ClassConstructor.SetInstanceFactory(new DefaultInstanceFactory());
var referenceModel = Vault.Context.GetByDocumentType<BlogEntryViewModel>().FirstOrDefault();

ClassConstructor.SetInstanceFactory(new ProxyInstanceFactory());
var proxyModel = Vault.Context.GetContentById<BlogEntryViewModel>(referenceModel.CmsContent.Id);

var result = Content(FormatResult(referenceModel.Content, referenceModel.Content, proxyModel.Content), "text/html");

ClassConstructor.SetInstanceFactory(new DefaultInstanceFactory());

return result;
}

public ActionResult ValueShouldBeOverWritten()
{
const string OverrideContent = "This content has been overwritten";

ClassConstructor.SetInstanceFactory(new DefaultInstanceFactory());
var referenceModel = Vault.Context.GetByDocumentType<BlogEntryViewModel>().FirstOrDefault();

ClassConstructor.SetInstanceFactory(new ProxyInstanceFactory());
var proxyModel = Vault.Context.GetContentById<BlogEntryViewModel>(referenceModel.CmsContent.Id);
proxyModel.Content = OverrideContent;

var result = Content(FormatResult(referenceModel.Content, OverrideContent, proxyModel.Content), "text/html");

ClassConstructor.SetInstanceFactory(new DefaultInstanceFactory());

return result;
}

private static string FormatResult(string cmsContent, string expectedContent, string actualContent)
{
return string.Format("{0}{1}<br/><br/>{2}{3}<br/><br/>{4}{5}", FormatTitle("CMS Content:"), cmsContent, FormatTitle("Expected Content:"), expectedContent, FormatTitle("Actual Content:"), actualContent);
}

private static string FormatTitle(string title)
{
return string.Format("<p><strong>{0}</strong></p><br/>", title);
}
}
}
2 changes: 1 addition & 1 deletion ReferenceWebsite/Models/Blog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public class BlogEntryViewModel : CmsViewModelBase, IBlogEntryViewModel
public virtual string Title { get; set; }
public virtual DateTime? PostDate { get; set; }
[UmbracoRichTextProperty]
string IBlogEntryViewModel.Content { get; set; }
public string Content { get; set; }
[UmbracoProperty(Alias = "image")]
public virtual Image PostImage { get; set; }
}
Expand Down
1 change: 1 addition & 0 deletions ReferenceWebsite/ReferenceWebsite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@
<Compile Include="App_Start\RouteConfig.cs" />
<Compile Include="App_Start\VaultRegistrationEventHandler.cs" />
<Compile Include="Controllers\PerformanceController.cs" />
<Compile Include="Controllers\ProxyTestsController.cs" />
<Compile Include="Global.asax.cs">
<DependentUpon>Global.asax</DependentUpon>
</Compile>
Expand Down
1 change: 1 addition & 0 deletions UmbracoVault/Proxy/ILazyResolverMixin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ namespace UmbracoVault.Proxy
public interface ILazyResolverMixin
{
object GetOrResolve(string alias, PropertyInfo propertyInfo);
void Set(string alias, object value);
}
}
33 changes: 19 additions & 14 deletions UmbracoVault/Proxy/LazyContentResolverMixin.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand All @@ -10,8 +11,8 @@ namespace UmbracoVault.Proxy
public class LazyContentResolverMixin : ILazyResolverMixin
{
private readonly IContent _content;
private readonly Dictionary<string, object> _valueCache = new Dictionary<string, object>();
private readonly IUmbracoContext _umbracoContext;
private readonly ConcurrentDictionary<string, object> _valueCache = new ConcurrentDictionary<string, object>();

public LazyContentResolverMixin(IContent content)
{
Expand All @@ -21,33 +22,37 @@ public LazyContentResolverMixin(IContent content)

public object GetOrResolve(string alias, PropertyInfo propertyInfo)
{
object value;
if (!_valueCache.TryGetValue(alias, out value))
return _valueCache.GetOrAdd(alias, key =>
{
object value;
_umbracoContext.TryGetValueForProperty(
(propAlias, recursive) => ResolveValue(propAlias, recursive, _content),
propertyInfo,
out value);
_valueCache.Add(alias, value);
}
else
{
Console.WriteLine("From cache");
}
return value;
});
}

return value;
public void Set(string alias, object value)
{
_valueCache.AddOrUpdate(alias, key => value, (key, oldValue) => value);
}

private static object ResolveValue(string alias, bool recursive, IContent node)
{
if (node.HasProperty(alias))
var value = node.HasProperty(alias) ? node.GetValue(alias) : null;
if (recursive)
{
var value = node.GetValue(alias);
return value;
node = node.Parent();
while (node != null && value == null)
{
value = node.HasProperty(alias) ? node.GetValue(alias) : null;
node = node.Parent();
}
}

return null;
return value;
}
}
}
35 changes: 14 additions & 21 deletions UmbracoVault/Proxy/LazyResolverMixin.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand All @@ -15,8 +16,7 @@ public class LazyResolverMixin : ILazyResolverMixin
{
private readonly IPublishedContent _node;
private readonly IUmbracoContext _umbracoContext;
private readonly object _cacheLock = new object();
private readonly Dictionary<string, object> _valueCache = new Dictionary<string, object>();
private readonly ConcurrentDictionary<string, object> _valueCache = new ConcurrentDictionary<string, object>();

public LazyResolverMixin(IPublishedContent node)
{
Expand All @@ -26,28 +26,21 @@ public LazyResolverMixin(IPublishedContent node)

public object GetOrResolve(string alias, PropertyInfo propertyInfo)
{
object value;
if (!_valueCache.TryGetValue(alias, out value))
return _valueCache.GetOrAdd(alias, key =>
{
lock (_cacheLock)
{
if (!_valueCache.TryGetValue(alias, out value))
{
_umbracoContext.TryGetValueForProperty(
(propAlias, recursive) => ResolveValue(propAlias, recursive, _node),
propertyInfo,
out value);
object value;
_umbracoContext.TryGetValueForProperty(
(propAlias, recursive) => ResolveValue(propAlias, recursive, _node),
propertyInfo,
out value);
_valueCache.Add(alias, value);
}
}
}
else
{
Console.WriteLine("From cache");
}
return value;
});
}

return value;
public void Set(string alias, object value)
{
_valueCache.AddOrUpdate(alias, key => value, (key, oldValue) => value);
}

private static object ResolveValue(string alias, bool recursive, IPublishedContent node)
Expand Down
104 changes: 79 additions & 25 deletions UmbracoVault/Proxy/ProxyInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,62 +13,92 @@ namespace UmbracoVault.Proxy
public class ProxyInterceptor : IInterceptor
{
private const string GetterMethodPrefix = "get_";
private const string SetterMethodPrefix = "set_";

public void Intercept(IInvocation invocation)
{
PropertyInfo property;
ILazyResolverMixin lazyResolverMixin;

if (!ShouldInterceptMethod(invocation, out property) || !ShouldInterceptClass(invocation, out lazyResolverMixin))
if (!ShouldInterceptClass(invocation, out lazyResolverMixin))
{
invocation.Proceed();
return;
}
else if (ShouldInterceptGetMethod(invocation, out property))
{
var alias = GetPropertyAlias(invocation);
var value = lazyResolverMixin.GetOrResolve(alias, property);
invocation.ReturnValue = value;
}
else if (ShouldInterceptSetMethod(invocation, out property))
{
var alias = GetPropertyAlias(invocation);
var value = invocation.GetArgumentValue(0);

var alias = PropertyAlias(invocation);
var value = lazyResolverMixin.GetOrResolve(alias, property);
invocation.ReturnValue = value;
lazyResolverMixin.Set(alias, value);
invocation.ReturnValue = value;
}
else
{
invocation.Proceed();
}
}

private static PropertyInfo GetPropertyInfo(Type targetType, MethodInfo method)
private static bool ShouldInterceptClass(IInvocation invocation, out ILazyResolverMixin lazyResolverMixin)
{
return targetType.GetProperty(method.Name.Substring(GetterMethodPrefix.Length),
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
lazyResolverMixin = invocation.Proxy as ILazyResolverMixin;
return lazyResolverMixin != null;
}

public static string PropertyAlias(IInvocation invocation)
private static bool ShouldInterceptGetMethod(IInvocation invocation, out PropertyInfo property)
{
var umbracoProperty = invocation.Method.GetCustomAttributes(true)
.FirstOrDefault(o => o is UmbracoPropertyAttribute) as UmbracoPropertyAttribute;

if (umbracoProperty != null)
return ShouldInterceptMethod(invocation, out property, prop =>
{
return umbracoProperty.Alias;
}
var getter = prop.GetMethod;
var isGetter = getter != null && getter.Name == invocation.Method.Name;
var isVirtual = getter != null && getter.IsVirtual && !getter.IsFinal;
// Return "Foo" as property name for "get_Foo" getter method name
return invocation.Method.Name.Substring(GetterMethodPrefix.Length);
return isGetter && isVirtual;
});
}

private static bool ShouldInterceptClass(IInvocation invocation, out ILazyResolverMixin lazyResolverMixin)
private static bool ShouldInterceptSetMethod(IInvocation invocation, out PropertyInfo property)
{
lazyResolverMixin = invocation.Proxy as ILazyResolverMixin;
return lazyResolverMixin != null;
return ShouldInterceptMethod(invocation, out property, prop =>
{
var setter = prop.SetMethod;
var isSetter = setter != null && setter.Name == invocation.Method.Name;
var isVirtual = setter != null && setter.IsVirtual && !setter.IsFinal;
return isSetter && isVirtual;
});
}

private static bool ShouldInterceptMethod(IInvocation invocation, out PropertyInfo property)
private static bool ShouldInterceptMethod(IInvocation invocation, out PropertyInfo property, Func<PropertyInfo, bool> shouldIntercept)
{
property = GetPropertyInfo(invocation.TargetType, invocation.Method);
if (property == null || !PropertyIsOptedIn(property, invocation.TargetType) || property.GetCustomAttribute<UmbracoIgnorePropertyAttribute>() != null)
{
return false;
}

var getter = property.GetMethod;
var isGetter = getter != null && getter.Name == invocation.Method.Name;
var isVirtual = getter != null && getter.IsVirtual && !getter.IsFinal;
return shouldIntercept(property);
}

return isGetter && isVirtual;
private static PropertyInfo GetPropertyInfo(Type targetType, MethodInfo method)
{
if (method.Name.StartsWith(GetterMethodPrefix, StringComparison.Ordinal))
{
return targetType.GetProperty(method.Name.Substring(GetterMethodPrefix.Length),
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
}
else if (method.Name.StartsWith(SetterMethodPrefix, StringComparison.Ordinal))
{
return targetType.GetProperty(method.Name.Substring(SetterMethodPrefix.Length),
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
}

return null;
}

private static bool PropertyIsOptedIn(MemberInfo property, Type topmostType)
Expand All @@ -77,5 +107,29 @@ private static bool PropertyIsOptedIn(MemberInfo property, Type topmostType)

return entityAttribute != null && (entityAttribute.AutoMap || property.GetCustomAttribute<UmbracoPropertyAttribute>() != null);
}

public static string GetPropertyAlias(IInvocation invocation)
{
var umbracoProperty = invocation.Method.GetCustomAttributes(true)
.FirstOrDefault(o => o is UmbracoPropertyAttribute) as UmbracoPropertyAttribute;

if (umbracoProperty != null)
{
return umbracoProperty.Alias;
}

if (invocation.Method.Name.StartsWith(GetterMethodPrefix, StringComparison.Ordinal))
{
// Return "Foo" as property name for "get_Foo" getter method name
return invocation.Method.Name.Substring(GetterMethodPrefix.Length);
}
else if (invocation.Method.Name.StartsWith(SetterMethodPrefix, StringComparison.Ordinal))
{
// Return "Foo" as property name for "set_Foo" getter method name
return invocation.Method.Name.Substring(SetterMethodPrefix.Length);
}

return null;
}
}
}
2 changes: 2 additions & 0 deletions UmbracoVault/UmbracoVault.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<description>Vault for Umbraco is an easy-to-use, extensible ORM to quickly and easily get strongly-typed Umbraco CMS data into your views. It allows you to create lightly-decorated classes that Vault will understand how to hydrate. This gives you the full view model-style experience in Umbraco that you are accustomed to in MVC, complete with strongly-typed view properties (no more magic strings in your views).</description>
<summary>Vault for Umbraco is an easy-to-use, extensible ORM to quickly and easily get strongly-typed Umbraco CMS data into your views.</summary>
<releaseNotes>
* Fixes issue where proxied models created from IContent models did not support recursive hydration.
* Fixes issue where if a proxy property was overwritten with some value not from CMS, it would not persist and would still use CMS value.
* Fixes issue where preview content breaks due to use of a singleton UmbracoHelper. [#34]
</releaseNotes>
<copyright>(c) The Nerdery LLC 2016. All Rights Reserved.</copyright>
Expand Down

0 comments on commit de8b7e8

Please sign in to comment.