PublishedContent - implement a model factory
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user