PublishedContent - implement a model factory

This commit is contained in:
Stephan
2013-09-13 21:15:40 +02:00
parent ac19ac7a6b
commit 85cb7fadfc
8 changed files with 288 additions and 2 deletions

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.ObjectResolution;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Mappers;
@@ -282,6 +283,9 @@ namespace Umbraco.Core
UrlSegmentProviderResolver.Current = new UrlSegmentProviderResolver(
typeof (DefaultUrlSegmentProvider));
PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(
new PublishedContentModelFactoryImpl());
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Represents a strongly-typed published content.
/// </summary>
/// <remarks>Every strongly-typed published content class should inherit from <c>PublishedContentModel</c>
/// (or inherit from a class that inherits from... etc.) so they are picked by the factory.</remarks>
public abstract class PublishedContentModel : PublishedContentExtended
{
/// <summary>
/// Initializes a new instance of the <see cref="PublishedContentModel"/> class with
/// an original <see cref="IPublishedContent"/> instance.
/// </summary>
/// <param name="content">The original content.</param>
protected PublishedContentModel(IPublishedContent content)
: base(content)
{ }
}
}

View File

@@ -0,0 +1,29 @@
using System;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Indicates that the class is a published content model for a specified content type.
/// </summary>
/// <remarks>By default, the name of the class is assumed to be the content type alias. The
/// <c>PublishedContentModelAttribute</c> can be used to indicate a different alias.</remarks>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class PublishedContentModelAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PublishedContentModelAttribute"/> class with a content type alias.
/// </summary>
/// <param name="contentTypeAlias">The content type alias.</param>
public PublishedContentModelAttribute(string contentTypeAlias)
{
if (string.IsNullOrWhiteSpace(contentTypeAlias))
throw new ArgumentException("Argument cannot be null nor empty.", "contentTypeAlias");
ContentTypeAlias = contentTypeAlias;
}
/// <summary>
/// Gets or sets the content type alias.
/// </summary>
public string ContentTypeAlias { get; private set; }
}
}

View File

@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Implements a strongly typed content model factory
/// </summary>
internal class PublishedContentModelFactoryImpl : IPublishedContentModelFactory
{
//private readonly Dictionary<string, ConstructorInfo> _constructors
// = new Dictionary<string, ConstructorInfo>();
private readonly Dictionary<string, Func<IPublishedContent, IPublishedContent>> _constructors
= new Dictionary<string, Func<IPublishedContent, IPublishedContent>>();
public PublishedContentModelFactoryImpl()
{
var types = PluginManager.Current.ResolveTypes<PublishedContentModel>();
var ctorArgTypes = new[] { typeof(IPublishedContent) };
foreach (var type in types)
{
if (type.Inherits<PublishedContentModel>() == false)
throw new InvalidOperationException(string.Format("Type {0} is marked with PublishedContentModel attribute but does not inherit from PublishedContentExtended.", type.FullName));
var constructor = type.GetConstructor(ctorArgTypes);
if (constructor == null)
throw new InvalidOperationException(string.Format("Type {0} is missing a public constructor with one argument of type IPublishedContent.", type.FullName));
var attribute = type.GetCustomAttribute<PublishedContentModelAttribute>(false);
var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias;
typeName = typeName.ToLowerInvariant();
if (_constructors.ContainsKey(typeName))
throw new InvalidOperationException(string.Format("More that one type want to be a model for content type {0}.", typeName));
// should work everywhere, potentially slow?
//_constructors[typeName] = constructor;
// note: would it be even faster with a dynamic method?
// here http://stackoverflow.com/questions/16363838/how-do-you-call-a-constructor-via-an-expression-tree-on-an-existing-object
// but MediumTrust issue?
// fixme - must make sure that works in medium trust
// read http://boxbinary.com/2011/10/how-to-run-a-unit-test-in-medium-trust-with-nunitpart-three-umbraco-framework-testing/
var exprArg = Expression.Parameter(typeof(IPublishedContent), "content");
var exprNew = Expression.New(constructor, exprArg);
var expr = Expression.Lambda<Func<IPublishedContent, IPublishedContent>>(exprNew, exprArg);
var func = expr.Compile();
_constructors[typeName] = func;
}
}
public IPublishedContent CreateModel(IPublishedContent content)
{
// be case-insensitive
var contentTypeAlias = content.DocumentTypeAlias.ToLowerInvariant();
//ConstructorInfo constructor;
//return _constructors.TryGetValue(contentTypeAlias, out constructor)
// ? (IPublishedContent) constructor.Invoke(new object[] { content })
// : content;
Func<IPublishedContent, IPublishedContent> constructor;
return _constructors.TryGetValue(contentTypeAlias, out constructor)
? constructor(content)
: content;
}
}
}

View File

@@ -184,7 +184,9 @@
<Compile Include="Models\ContentTypeSort.cs" />
<Compile Include="Models\PublishedContent\IPublishedContentExtended.cs" />
<Compile Include="Models\PublishedContent\PublishedPropertyBase.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelFactoryImpl.cs" />
<Compile Include="Models\PublishedContent\IPublishedContentModelFactory.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModel.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelFactoryResolver.cs" />
<Compile Include="PropertyEditors\PropertyCacheValue.cs" />
<Compile Include="PropertyEditors\PropertyValueCacheAttribute.cs" />
@@ -216,6 +218,7 @@
<Compile Include="Models\IFile.cs" />
<Compile Include="Models\ILanguage.cs" />
<Compile Include="Models\PublishedContent\PublishedContentExtended.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelAttribute.cs" />
<Compile Include="Models\PublishedContent\PublishedContentModelFactory.cs" />
<Compile Include="Models\PublishedContent\PublishedContentWrapped.cs" />
<Compile Include="Models\PublishedContent\PublishedContentType.cs" />

View File

@@ -1,6 +1,5 @@
using System.Linq;
using System.Collections.ObjectModel;
using Lucene.Net.Documents;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
@@ -36,7 +35,7 @@ namespace Umbraco.Tests.PublishedContent
PropertyValueConvertersResolver.Current =
new PropertyValueConvertersResolver();
PublishedContentModelFactoryResolver.Current =
new PublishedContentModelFactoryResolver();
new PublishedContentModelFactoryResolver(new PublishedContentModelFactoryImpl());
Resolution.Freeze();
var caches = CreatePublishedContent();
@@ -122,6 +121,44 @@ namespace Umbraco.Tests.PublishedContent
Assert.IsTrue(content.IsLast());
}
[Test]
public void OfType1()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot()
.OfType<ContentType2>()
.Distinct()
.ToArray();
Assert.AreEqual(2, content.Count());
Assert.IsInstanceOf<ContentType2>(content.First());
var set = content.ToContentSet();
Assert.IsInstanceOf<ContentType2>(set.First());
Assert.AreSame(set, set.First().ContentSet);
Assert.IsInstanceOf<ContentType2Sub>(set.First().Next());
}
[Test]
public void OfType2()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot()
.OfType<ContentType2Sub>()
.Distinct()
.ToArray();
Assert.AreEqual(1, content.Count());
Assert.IsInstanceOf<ContentType2Sub>(content.First());
var set = content.ToContentSet();
Assert.IsInstanceOf<ContentType2Sub>(set.First());
}
[Test]
public void OfType()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot()
.OfType<ContentType2>()
.First(x => x.Prop1 == 1234);
Assert.AreEqual("Content 2", content.Name);
Assert.AreEqual(1234, content.Prop1);
}
[Test]
public void Position()
{
@@ -138,6 +175,28 @@ namespace Umbraco.Tests.PublishedContent
Assert.IsTrue(content.First().Next().Next().IsLast());
}
[Test]
public void Issue()
{
var content = UmbracoContext.Current.ContentCache.GetAtRoot()
.Distinct()
.OfType<ContentType2>();
var where = content.Where(x => x.Prop1 == 1234);
var first = where.First();
Assert.AreEqual(1234, first.Prop1);
var content2 = UmbracoContext.Current.ContentCache.GetAtRoot()
.OfType<ContentType2>()
.First(x => x.Prop1 == 1234);
Assert.AreEqual(1234, content2.Prop1);
var content3 = UmbracoContext.Current.ContentCache.GetAtRoot()
.OfType<ContentType2>()
.First();
Assert.AreEqual(1234, content3.Prop1);
}
static SolidPublishedCaches CreatePublishedContent()
{
var caches = new SolidPublishedCaches();

View File

@@ -227,6 +227,36 @@ namespace Umbraco.Tests.PublishedContent
public object XPathValue { get; set; }
}
[PublishedContentModel("ContentType2")]
public class ContentType2 : PublishedContentModel
{
#region Plumbing
public ContentType2(IPublishedContent content)
: base(content)
{ }
#endregion
// fast, if you know that the appropriate IPropertyEditorValueConverter is wired
public int Prop1 { get { return (int)this["prop1"]; } }
// almost as fast, not sure I like it as much, though
//public int Prop1 { get { return this.GetPropertyValue<int>("prop1"); } }
}
[PublishedContentModel("ContentType2Sub")]
public class ContentType2Sub : ContentType2
{
#region Plumbing
public ContentType2Sub(IPublishedContent content)
: base(content)
{ }
#endregion
}
class PublishedContentStrong1 : PublishedContentExtended
{
public PublishedContentStrong1(IPublishedContent content)

View File

@@ -23,6 +23,8 @@ namespace Umbraco.Tests.PublishedContent
get { return DatabaseBehavior.NoDatabasePerFixture; }
}
private PluginManager _pluginManager;
public override void Initialize()
{
// required so we can access property.Value
@@ -30,6 +32,16 @@ namespace Umbraco.Tests.PublishedContent
base.Initialize();
// this is so the model factory looks into the test assembly
_pluginManager = PluginManager.Current;
PluginManager.Current = new PluginManager(false)
{
AssembliesToScan = _pluginManager.AssembliesToScan
.Union(new[] { typeof(PublishedContentTests).Assembly })
};
ApplicationContext.Current = new ApplicationContext(false) { IsReady = true };
// need to specify a custom callback for unit tests
// AutoPublishedContentTypes generates properties automatically
// when they are requested, but we must declare those that we
@@ -48,6 +60,20 @@ namespace Umbraco.Tests.PublishedContent
PublishedContentType.GetPublishedContentTypeCallback = (alias) => type;
}
public override void TearDown()
{
PluginManager.Current = _pluginManager;
ApplicationContext.Current.DisposeIfDisposable();
ApplicationContext.Current = null;
}
protected override void FreezeResolution()
{
PublishedContentModelFactoryResolver.Current = new PublishedContentModelFactoryResolver(
new PublishedContentModelFactoryImpl());
base.FreezeResolution();
}
protected override string GetXmlContent(int templateId)
{
return @"<?xml version=""1.0"" encoding=""utf-8""?>
@@ -186,6 +212,47 @@ namespace Umbraco.Tests.PublishedContent
}
}
[PublishedContentModel("Home")]
public class Home : PublishedContentModel
{
public Home(IPublishedContent content)
: base(content)
{}
}
[Test]
public void Is_Last_From_Where_Filter2()
{
var doc = GetNode(1173);
var items = doc.Children
.Select(PublishedContentModelFactory.CreateModel) // linq, returns IEnumerable<IPublishedContent>
// only way around this is to make sure every IEnumerable<T> extension
// explicitely returns a PublishedContentSet, not an IEnumerable<T>
.OfType<Home>() // ours, return IEnumerable<Home> (actually a PublishedContentSet<Home>)
.Where(x => x.IsVisible()) // so, here it's linq again :-(
.ToContentSet() // so, we need that one for the test to pass
.ToArray();
Assert.AreEqual(1, items.Count());
foreach (var d in items)
{
switch (d.Id)
{
case 1174:
Assert.IsTrue(d.IsFirst());
Assert.IsTrue(d.IsLast());
break;
default:
Assert.Fail("Invalid id.");
break;
}
}
}
[Test]
public void Is_Last_From_Take()
{