PropertyValueConverter - more WIP

This commit is contained in:
Stephan
2017-07-27 11:09:53 +02:00
parent 76569d58fe
commit 4bfea6044a
10 changed files with 551 additions and 148 deletions

View File

@@ -14,11 +14,13 @@ namespace Umbraco.Core.Models.PublishedContent
private readonly PublishedPropertyType[] _propertyTypes;
// fast alias-to-index xref containing both the raw alias and its lowercase version
// fixme - benchmark this!
private readonly Dictionary<string, int> _indexes = new Dictionary<string, int>();
// internal so it can be used by PublishedNoCache which does _not_ want to cache anything and so will never
// use the static cache getter PublishedContentType.GetPublishedContentType(alias) below - anything else
// should use it.
// fixme - not true anymore internal and all?!
internal PublishedContentType(IContentType contentType)
: this(PublishedItemType.Content, contentType)
{ }
@@ -88,7 +90,7 @@ namespace Umbraco.Core.Models.PublishedContent
// NOTE: code below defines and add custom, built-in, Umbraco properties for members
// unless they are already user-defined in the content type, then they are skipped
// not sure it's needed really - this is here for safety purposes
static readonly Dictionary<string, Tuple<int, string>> BuiltinMemberProperties = new Dictionary<string, Tuple<int, string>>
private static readonly Dictionary<string, Tuple<int, string>> BuiltinMemberProperties = new Dictionary<string, Tuple<int, string>>
{
// see also PublishedMember class - exposing special properties as properties
{ "Email", Tuple.Create(Constants.DataTypes.Textbox, Constants.PropertyEditors.TextboxAlias) },
@@ -115,7 +117,7 @@ namespace Umbraco.Core.Models.PublishedContent
foreach (var propertyType in BuiltinMemberProperties
.Where(kvp => aliases.Contains(kvp.Key) == false)
.Select(kvp => new PublishedPropertyType(kvp.Key, kvp.Value.Item1, kvp.Value.Item2, true)))
.Select(kvp => new PublishedPropertyType(kvp.Key, kvp.Value.Item1, kvp.Value.Item2, umbraco: true)))
{
if (contentType != null) propertyType.ContentType = contentType;
yield return propertyType;

View File

@@ -13,6 +13,23 @@ namespace Umbraco.Core.Models.PublishedContent
{
PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
ReferenceCacheLevel = referenceCacheLevel;
ValidateCacheLevel(ReferenceCacheLevel);
ValidateCacheLevel(PropertyType.CacheLevel);
}
private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel)
{
switch (cacheLevel)
{
case PropertyCacheLevel.Content:
case PropertyCacheLevel.Snapshot:
case PropertyCacheLevel.Facade:
case PropertyCacheLevel.None:
break;
default:
throw new Exception("Invalid cache level.");
}
}
public PublishedPropertyType PropertyType { get; }

View File

@@ -13,6 +13,15 @@ namespace Umbraco.Core.Models.PublishedContent
/// if the property type changes, then a new class needs to be created.</remarks>
public class PublishedPropertyType
{
private readonly PropertyValueConverterCollection _converters;
private readonly object _locker = new object();
private volatile bool _initialized;
private IPropertyValueConverter _converter;
private PropertyCacheLevel _cacheLevel;
private Type _modelClrType;
private Type _clrType;
#region Constructors
/// <summary>
@@ -26,6 +35,8 @@ namespace Umbraco.Core.Models.PublishedContent
{
// PropertyEditor [1:n] DataTypeDefinition [1:n] PropertyType
_converters = Current.PropertyValueConverters; // fixme really?
ContentType = contentType;
PropertyTypeAlias = propertyType.Alias;
@@ -46,8 +57,8 @@ namespace Umbraco.Core.Models.PublishedContent
/// to make decisions, fetch prevalues, etc.</para>
/// <para>The value of <paramref name="propertyEditorAlias"/> is assumed to be valid.</para>
/// </remarks>
internal PublishedPropertyType(string propertyTypeAlias, string propertyEditorAlias)
: this(propertyTypeAlias, 0, propertyEditorAlias)
internal PublishedPropertyType(string propertyTypeAlias, string propertyEditorAlias, PropertyValueConverterCollection converters = null)
: this(propertyTypeAlias, 0, propertyEditorAlias, converters)
{ }
/// <summary>
@@ -63,11 +74,13 @@ namespace Umbraco.Core.Models.PublishedContent
/// <para>The values of <paramref name="dataTypeDefinitionId"/> and <paramref name="propertyEditorAlias"/> are
/// assumed to be valid and consistent.</para>
/// </remarks>
internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, string propertyEditorAlias, bool umbraco = false)
internal PublishedPropertyType(string propertyTypeAlias, int dataTypeDefinitionId, string propertyEditorAlias, PropertyValueConverterCollection converters = null, bool umbraco = false)
{
// ContentType
// - in unit tests, to be set by PublishedContentType when creating it
_converters = converters ?? Current.PropertyValueConverters; // fixme really?
PropertyTypeAlias = propertyTypeAlias;
DataTypeId = dataTypeDefinitionId;
@@ -109,32 +122,23 @@ namespace Umbraco.Core.Models.PublishedContent
#region Converters
private readonly object _locker = new object();
private volatile bool _initialized;
private IPropertyValueConverter _converter;
private PropertyCacheLevel _cacheLevel;
private Type _modelClrType;
private Type _clrType;
private void EnsureInitialized()
private void Initialize()
{
if (_initialized) return;
lock (_locker)
{
if (_initialized) return;
InitializeConverters();
InitializeLocked();
_initialized = true;
}
}
private void InitializeConverters()
private void InitializeLocked()
{
_converter = null;
var isdefault = false;
var converterCollection = Current.PropertyValueConverters;
foreach (var converter in converterCollection)
foreach (var converter in _converters)
{
if (converter.IsConverter(this) == false)
continue;
@@ -142,20 +146,20 @@ namespace Umbraco.Core.Models.PublishedContent
if (_converter == null)
{
_converter = converter;
isdefault = converterCollection.IsDefault(converter);
isdefault = _converters.IsDefault(converter);
continue;
}
if (isdefault)
{
if (converterCollection.IsDefault(converter))
if (_converters.IsDefault(converter))
{
// previous was default, and got another default
if (converterCollection.Shadows(_converter, converter))
if (_converters.Shadows(_converter, converter))
{
// previous shadows, ignore
}
else if (converterCollection.Shadows(converter, _converter))
else if (_converters.Shadows(converter, _converter))
{
// shadows previous, replace
_converter = converter;
@@ -179,7 +183,7 @@ namespace Umbraco.Core.Models.PublishedContent
}
else
{
if (converterCollection.IsDefault(converter))
if (_converters.IsDefault(converter))
{
// previous was non-default, ignore default
}
@@ -204,7 +208,7 @@ namespace Umbraco.Core.Models.PublishedContent
{
get
{
EnsureInitialized();
if (!_initialized) Initialize();
return _cacheLevel;
}
}
@@ -215,7 +219,7 @@ namespace Umbraco.Core.Models.PublishedContent
// preview: whether we are previewing or not
public object ConvertSourceToInter(IPropertySet owner, object source, bool preview)
{
EnsureInitialized();
if (!_initialized) Initialize();
// use the converter if any, else just return the source value
return _converter != null
@@ -229,7 +233,7 @@ namespace Umbraco.Core.Models.PublishedContent
// preview: whether we are previewing or not
public object ConvertInterToObject(IPropertySet owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
EnsureInitialized();
if (!_initialized) Initialize();
// use the converter if any, else just return the inter value
return _converter != null
@@ -244,7 +248,7 @@ namespace Umbraco.Core.Models.PublishedContent
// preview: whether we are previewing or not
public object ConvertInterToXPath(IPropertySet owner, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
EnsureInitialized();
if (!_initialized) Initialize();
// use the converter if any
if (_converter != null)
@@ -264,7 +268,7 @@ namespace Umbraco.Core.Models.PublishedContent
{
get
{
EnsureInitialized();
if (!_initialized) Initialize();
return _modelClrType;
}
}
@@ -276,7 +280,7 @@ namespace Umbraco.Core.Models.PublishedContent
{
get
{
EnsureInitialized();
if (!_initialized) Initialize();
return _clrType ?? (_clrType = ModelType.Map(_modelClrType, Current.PublishedContentModelFactory.ModelTypeMap));
}
}

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using LightInject;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
@@ -17,6 +16,17 @@ namespace Umbraco.Tests.PublishedContent
[TestFixture]
public class ModelsAndConvertersTests
{
// fixme
// naming: IPublishedProperty is IPropertySetProperty or IFacadeProperty of some sort
// naming: general naming sucks at the moment
// caching: re-think how properties are cached
// - for true NuCache content it probably is OK (but needs explanation)
// - for true Xml cache content it probably is OK (but needs explanation)
// - for pure sets, I have no idea - should at least cache at content level?
// hold on - PropertySetPropertyBase probably handles it - need to check
#region ModelType
[Test]
public void ModelTypeEqualityTests()
{
@@ -61,16 +71,132 @@ namespace Umbraco.Tests.PublishedContent
ModelType.Map(typeof(IEnumerable<>).MakeGenericType(ModelType.For("alias1").MakeArrayType()), map).ToString());
}
#endregion
#region SimpleConverter1
[Test]
public void ConverterTest1()
public void SimpleConverter1Test()
{
var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
{
new SimpleConverter1(),
});
var setType1 = new PublishedContentType(1000, "set1", new[]
{
new PublishedPropertyType("prop1", "editor1", converters),
});
var set1 = new PropertySet(setType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
Assert.AreEqual(1234, set1.Value("prop1"));
}
private class SimpleConverter1 : IPropertyValueConverter
{
public bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias.InvariantEquals("editor1");
public Type GetPropertyValueType(PublishedPropertyType propertyType)
=> typeof (int);
public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> PropertyCacheLevel.Content;
public object ConvertSourceToInter(IPropertySet owner, PublishedPropertyType propertyType, object source, bool preview)
=> int.TryParse(source as string, out int i) ? i : 0;
public object ConvertInterToObject(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> (int) inter;
public object ConvertInterToXPath(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> ((int) inter).ToString();
}
#endregion
#region SimpleConverter2
[Test]
public void SimpleConverter2Test()
{
var cacheMock = new Mock<IPublishedContentCache>();
var cacheContent = new Dictionary<int, IPublishedContent>();
cacheMock.Setup(x => x.GetById(It.IsAny<int>())).Returns<int>(id => cacheContent.TryGetValue(id, out IPublishedContent content) ? content : null);
var facadeMock = new Mock<IFacade>();
facadeMock.Setup(x => x.ContentCache).Returns(cacheMock.Object);
var facadeAccessorMock = new Mock<IFacadeAccessor>();
facadeAccessorMock.Setup(x => x.Facade).Returns(facadeMock.Object);
var facadeAccessor = facadeAccessorMock.Object;
var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
{
new SimpleConverter2(facadeAccessor),
});
var setType1 = new PublishedContentType(1000, "set1", new[]
{
new PublishedPropertyType("prop1", "editor2", converters),
});
var set1 = new PropertySet(setType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
var cntType1 = new PublishedContentType(1001, "cnt1", Array.Empty<PublishedPropertyType>());
var cnt1 = new TestPublishedContent(cntType1, 1234, Guid.NewGuid(), new Dictionary<string, object>(), false);
cacheContent[cnt1.Id] = cnt1;
Assert.AreSame(cnt1, set1.Value("prop1"));
}
private class SimpleConverter2 : IPropertyValueConverter
{
private readonly IFacadeAccessor _facadeAccessor;
private readonly PropertyCacheLevel _cacheLevel;
public SimpleConverter2(IFacadeAccessor facadeAccessor, PropertyCacheLevel cacheLevel = PropertyCacheLevel.None)
{
_facadeAccessor = facadeAccessor;
_cacheLevel = cacheLevel;
}
public bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias.InvariantEquals("editor2");
public Type GetPropertyValueType(PublishedPropertyType propertyType)
// the first version would be the "generic" version, but say we want to be more precise
// and return: whatever Clr type is generated for content type with alias "cnt1" -- which
// we cannot really typeof() at the moment because it has not been generated, hence ModelType.
// => typeof (IPublishedContent);
=> ModelType.For("cnt1");
public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> _cacheLevel;
public object ConvertSourceToInter(IPropertySet owner, PublishedPropertyType propertyType, object source, bool preview)
=> int.TryParse(source as string, out int i) ? i : -1;
public object ConvertInterToObject(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> _facadeAccessor.Facade.ContentCache.GetById((int) inter);
public object ConvertInterToXPath(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> ((int) inter).ToString();
}
#endregion
#region SimpleConverter3
[Test]
public void SimpleConverter3Test()
{
Current.Reset();
var container = new ServiceContainer();
container.ConfigureUmbracoCore();
Current.Container.RegisterCollectionBuilder<PropertyValueConverterCollectionBuilder>()
.Append<TestConverter1>()
.Append<TestConverter2>();
.Append<SimpleConverter3A>()
.Append<SimpleConverter3B>();
IPublishedContentModelFactory factory = new PublishedContentModelFactory(new[]
{
@@ -79,6 +205,15 @@ namespace Umbraco.Tests.PublishedContent
});
Current.Container.Register(f => factory);
var cacheMock = new Mock<IPublishedContentCache>();
var cacheContent = new Dictionary<int, IPublishedContent>();
cacheMock.Setup(x => x.GetById(It.IsAny<int>())).Returns<int>(id => cacheContent.TryGetValue(id, out IPublishedContent content) ? content : null);
var facadeMock = new Mock<IFacade>();
facadeMock.Setup(x => x.ContentCache).Returns(cacheMock.Object);
var facadeAccessorMock = new Mock<IFacadeAccessor>();
facadeAccessorMock.Setup(x => x.Facade).Returns(facadeMock.Object);
Current.Container.Register(f => facadeAccessorMock.Object);
var setType1 = new PublishedContentType(1000, "set1", new[]
{
new PublishedPropertyType("prop1", "editor1"),
@@ -101,41 +236,31 @@ namespace Umbraco.Tests.PublishedContent
var set1 = new PropertySet(setType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "val1" } }, false);
var set2 = new PropertySet(setType2, Guid.NewGuid(), new Dictionary<string, object> { { "prop2", "1003" } }, false);
var cnt1 = new TestPublishedContent(contentType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "val1" } }, false);
var cnt2 = new TestPublishedContent(contentType2, Guid.NewGuid(), new Dictionary<string, object> { { "prop2", "1003" } }, false);
var cnt1 = new TestPublishedContent(contentType1, 1003, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "val1" } }, false);
var cnt2 = new TestPublishedContent(contentType2, 1004, Guid.NewGuid(), new Dictionary<string, object> { { "prop2", "1003" } }, false);
var cache = new Dictionary<int, IPublishedContent>
{
{ 1003, cnt1.CreateModel() },
{ 1004, cnt2.CreateModel() },
};
var facadeMock = new Mock<IFacade>();
var cacheMock = new Mock<IPublishedContentCache>();
cacheMock.Setup(x => x.GetById(It.IsAny<int>())).Returns<int>(id => cache.TryGetValue(id, out IPublishedContent content) ? content : null);
facadeMock.Setup(x => x.ContentCache).Returns(cacheMock.Object);
var facade = facadeMock.Object;
Current.Container.Register(f => facade);
cacheContent[cnt1.Id] = cnt1.CreateModel();
cacheContent[cnt2.Id] = cnt2.CreateModel();
// can get the actual property Clr type
// ie ModelType gets properly mapped by IPublishedContentModelFactory
// must test ModelClrType with special equals 'cos they are not ref-equals
Assert.IsTrue(ModelType.Equals(typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
Assert.AreEqual(typeof (IEnumerable<TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
Assert.IsTrue(ModelType.Equals(typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType));
Assert.AreEqual(typeof(IEnumerable<TestContentModel1>), contentType2.GetPropertyType("prop2").ClrType);
// can create a model for a property set
var model1 = factory.CreateModel(set1);
Assert.IsInstanceOf<TestSetModel1>(model1);
Assert.AreEqual("val1", ((TestSetModel1) model1).Prop1);
Assert.AreEqual("val1", ((TestSetModel1)model1).Prop1);
// can create a model for a published content
var model2 = factory.CreateModel(set2);
Assert.IsInstanceOf<TestSetModel2>(model2);
var mmodel2 = (TestSetModel2) model2;
var mmodel2 = (TestSetModel2)model2;
// and get direct property
Assert.IsInstanceOf<TestContentModel1[]>(model2.Value("prop2"));
Assert.AreEqual(1, ((TestContentModel1[]) model2.Value("prop2")).Length);
Assert.AreEqual(1, ((TestContentModel1[])model2.Value("prop2")).Length);
// and get model property
Assert.IsInstanceOf<IEnumerable<TestContentModel1>>(mmodel2.Prop2);
@@ -143,42 +268,299 @@ namespace Umbraco.Tests.PublishedContent
var mmodel1 = mmodel2.Prop2.First();
// and we get what we want
Assert.AreSame(cache[1003], mmodel1);
Assert.AreSame(cacheContent[mmodel1.Id], mmodel1);
}
internal class TestPublishedContent : PropertySet, IPublishedContent
public class SimpleConverter3A : PropertyValueConverterBase
{
public TestPublishedContent(PublishedContentType contentType, Guid key, Dictionary<string, object> values, bool previewing)
: base(contentType, key, values, previewing)
{ }
public override bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias == "editor1";
public int Id { get; }
public int TemplateId { get; }
public int SortOrder { get; }
public string Name { get; }
public string UrlName { get; }
public string DocumentTypeAlias { get; }
public int DocumentTypeId { get; }
public string WriterName { get; }
public string CreatorName { get; }
public int WriterId { get; }
public int CreatorId { get; }
public string Path { get; }
public DateTime CreateDate { get; }
public DateTime UpdateDate { get; }
public Guid Version { get; }
public int Level { get; }
public string Url { get; }
public PublishedItemType ItemType { get; }
public bool IsDraft { get; }
public IPublishedContent Parent { get; }
public IEnumerable<IPublishedContent> Children { get; }
public IPublishedProperty GetProperty(string alias, bool recurse)
public override Type GetPropertyValueType(PublishedPropertyType propertyType)
=> typeof (string);
public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> PropertyCacheLevel.Content;
}
public class SimpleConverter3B : PropertyValueConverterBase
{
private readonly IFacadeAccessor _facadeAccessor;
public SimpleConverter3B(IFacadeAccessor facadeAccessor)
{
throw new NotImplementedException();
_facadeAccessor = facadeAccessor;
}
public override bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias == "editor2";
public override Type GetPropertyValueType(PublishedPropertyType propertyType)
=> typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1"));
public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> PropertyCacheLevel.Snapshot;
public override object ConvertSourceToInter(IPropertySet owner, PublishedPropertyType propertyType, object source, bool preview)
{
var s = source as string;
return s?.Split(',').Select(int.Parse).ToArray() ?? Array.Empty<int>();
}
public override object ConvertInterToObject(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
return ((int[]) inter).Select(x => (TestContentModel1) _facadeAccessor.Facade.ContentCache.GetById(x)).ToArray();
}
}
#endregion
#region ConversionCache
[TestCase(PropertyCacheLevel.None, 2)]
[TestCase(PropertyCacheLevel.Content, 1)]
[TestCase(PropertyCacheLevel.Snapshot, 1)]
[TestCase(PropertyCacheLevel.Facade, 1)]
public void CacheLevelTest(PropertyCacheLevel cacheLevel, int interConverts)
{
var converter = new CacheConverter1(cacheLevel);
var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
{
converter,
});
var setType1 = new PublishedContentType(1000, "set1", new[]
{
new PublishedPropertyType("prop1", "editor1", converters),
});
// PropertySetPropertyBase.GetCacheLevels:
//
// if property level is > reference level, or both are None
// use None for property & new reference
// else
// use Content for property, & keep reference
//
// PropertySet creates properties with reference being None
// if converter specifies None, keep using None
// anything else is not > None, use Content
//
// for standalone property sets, it's only None or Content
var set1 = new PropertySet(setType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
Assert.AreEqual(1234, set1.Value("prop1"));
Assert.AreEqual(1, converter.SourceConverts);
Assert.AreEqual(1, converter.InterConverts);
// source is always converted once and cached per content
// inter conversion depends on the specified cache level
Assert.AreEqual(1234, set1.Value("prop1"));
Assert.AreEqual(1, converter.SourceConverts);
Assert.AreEqual(interConverts, converter.InterConverts);
}
// property is not cached, converted cached at Content, exept
// /None = not cached at all
[TestCase(PropertyCacheLevel.None, PropertyCacheLevel.None, 2, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Content, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Snapshot, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.None, PropertyCacheLevel.Facade, 1, 0, 0, 0, 0)]
// property is cached at content level, converted cached at
// /None = not at all
// /Content = in content
// /Facade = in facade
// /Snapshot = in snapshot
[TestCase(PropertyCacheLevel.Content, PropertyCacheLevel.None, 2, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Content, PropertyCacheLevel.Content, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Content, PropertyCacheLevel.Snapshot, 1, 1, 0, 1, 0)]
[TestCase(PropertyCacheLevel.Content, PropertyCacheLevel.Facade, 1, 0, 1, 0, 1)]
// property is cached at snapshot level, converted cached at Content, exept
// /None = not cached at all
// /Facade = cached in facade
[TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.None, 2, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.Content, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.Snapshot, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Snapshot, PropertyCacheLevel.Facade, 1, 0, 1, 0, 1)]
// property is cached at facade level, converted cached at Content, exept
// /None = not cached at all
[TestCase(PropertyCacheLevel.Facade, PropertyCacheLevel.None, 2, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Facade, PropertyCacheLevel.Content, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Facade, PropertyCacheLevel.Snapshot, 1, 0, 0, 0, 0)]
[TestCase(PropertyCacheLevel.Facade, PropertyCacheLevel.Facade, 1, 0, 0, 0, 0)]
public void CacheFacadeTest(PropertyCacheLevel referenceCacheLevel, PropertyCacheLevel converterCacheLevel, int interConverts,
int snapshotCount1, int facadeCount1, int snapshotCount2, int facadeCount2)
{
var converter = new CacheConverter1(converterCacheLevel);
var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
{
converter,
});
var setType1 = new PublishedContentType(1000, "set1", new[]
{
new PublishedPropertyType("prop1", "editor1", converters),
});
var snapshotCache = new Dictionary<string, object>();
var facadeCache = new Dictionary<string, object>();
var facadeServiceMock = new Mock<IFacadeService>();
facadeServiceMock
.Setup(x => x.CreateSetProperty(It.IsAny<PublishedPropertyType>(), It.IsAny<IPropertySet>(), It.IsAny<bool>(), It.IsAny<PropertyCacheLevel>(), It.IsAny<object>()))
.Returns<PublishedPropertyType, IPropertySet, bool, PropertyCacheLevel, object>((propertyType, set, previewing, refCacheLevel, value) =>
{
return new TestPropertySetProperty(propertyType, set, previewing, refCacheLevel, value, () => snapshotCache, () => facadeCache);
});
var facadeService = facadeServiceMock.Object;
// pretend we're creating this set as a value for a property
// referenceCacheLevel is the cache level for this fictious property
// converterCacheLevel is the cache level specified by the converter
var set1 = new PropertySet(setType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false, facadeService, referenceCacheLevel);
Assert.AreEqual(1234, set1.Value("prop1"));
Assert.AreEqual(1, converter.SourceConverts);
Assert.AreEqual(1, converter.InterConverts);
Assert.AreEqual(snapshotCount1, snapshotCache.Count);
Assert.AreEqual(facadeCount1, facadeCache.Count);
Assert.AreEqual(1234, set1.Value("prop1"));
Assert.AreEqual(1, converter.SourceConverts);
Assert.AreEqual(interConverts, converter.InterConverts);
Assert.AreEqual(snapshotCount2, snapshotCache.Count);
Assert.AreEqual(facadeCount2, facadeCache.Count);
var oldFacadeCache = facadeCache;
facadeCache = new Dictionary<string, object>();
Assert.AreEqual(1234, set1.Value("prop1"));
Assert.AreEqual(1, converter.SourceConverts);
Assert.AreEqual(snapshotCount2, snapshotCache.Count);
Assert.AreEqual(facadeCount2, facadeCache.Count);
Assert.AreEqual((interConverts == 1 ? 1 : 3) + facadeCache.Count, converter.InterConverts);
var oldSnapshotCache = snapshotCache;
snapshotCache = new Dictionary<string, object>();
Assert.AreEqual(1234, set1.Value("prop1"));
Assert.AreEqual(1, converter.SourceConverts);
Assert.AreEqual(snapshotCount2, snapshotCache.Count);
Assert.AreEqual(facadeCount2, facadeCache.Count);
Assert.AreEqual((interConverts == 1 ? 1 : 4) + facadeCache.Count + snapshotCache.Count, converter.InterConverts);
}
[Test]
public void CacheUnknownTest()
{
var converter = new CacheConverter1(PropertyCacheLevel.Unknown);
var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
{
converter,
});
var setType1 = new PublishedContentType(1000, "set1", new[]
{
new PublishedPropertyType("prop1", "editor1", converters),
});
Assert.Throws<Exception>(() =>
{
var set1 = new PropertySet(setType1, Guid.NewGuid(), new Dictionary<string, object> { { "prop1", "1234" } }, false);
});
}
private class CacheConverter1 : IPropertyValueConverter
{
private readonly PropertyCacheLevel _cacheLevel;
public CacheConverter1(PropertyCacheLevel cacheLevel)
{
_cacheLevel = cacheLevel;
}
public int SourceConverts { get; private set; }
public int InterConverts { get; private set; }
public bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias.InvariantEquals("editor1");
public Type GetPropertyValueType(PublishedPropertyType propertyType)
=> typeof(int);
public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> _cacheLevel;
public object ConvertSourceToInter(IPropertySet owner, PublishedPropertyType propertyType, object source, bool preview)
{
SourceConverts++;
return int.TryParse(source as string, out int i) ? i : 0;
}
public object ConvertInterToObject(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
InterConverts++;
return (int) inter;
}
public object ConvertInterToXPath(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
=> ((int) inter).ToString();
}
private class TestPropertySetProperty : PropertySetPropertyBase
{
private readonly Func<Dictionary<string, object>> _getSnapshotCache;
private readonly Func<Dictionary<string, object>> _getFacadeCache;
private string _valuesCacheKey;
public TestPropertySetProperty(PublishedPropertyType propertyType, IPropertySet set, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue,
Func<Dictionary<string, object>> getSnapshotCache, Func<Dictionary<string, object>> getFacadeCache)
: base(propertyType, set, previewing, referenceCacheLevel, sourceValue)
{
_getSnapshotCache = getSnapshotCache;
_getFacadeCache = getFacadeCache;
}
private string ValuesCacheKey => _valuesCacheKey ?? (_valuesCacheKey = $"CacheValues[{(IsPreviewing ? "D" : "P")}{Set.Key}:{PropertyType.PropertyTypeAlias}");
protected override CacheValues GetSnapshotCacheValues()
{
var snapshotCache = _getSnapshotCache();
if (snapshotCache.TryGetValue(ValuesCacheKey, out object cacheValues))
return (CacheValues) cacheValues;
snapshotCache[ValuesCacheKey] = cacheValues = new CacheValues();
return (CacheValues) cacheValues;
}
protected override CacheValues GetFacadeCacheValues()
{
var facadeCache = _getFacadeCache();
if (facadeCache.TryGetValue(ValuesCacheKey, out object cacheValues))
return (CacheValues)cacheValues;
facadeCache[ValuesCacheKey] = cacheValues = new CacheValues();
return (CacheValues)cacheValues;
}
}
#endregion
#region Model classes
[PublishedContentModel("set1")]
public class TestSetModel1 : PropertySetModel
{
@@ -219,47 +601,64 @@ namespace Umbraco.Tests.PublishedContent
public IEnumerable<TestContentModel1> Prop2 => this.Value<IEnumerable<TestContentModel1>>("prop2");
}
public class TestConverter1 : PropertyValueConverterBase
#endregion
#region Support classes
internal class TestPublishedContent : PropertySet, IPublishedContent
{
public override bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias == "editor1";
public TestPublishedContent(PublishedContentType contentType, int id, Guid key, Dictionary<string, object> values, bool previewing)
: base(contentType, key, values, previewing)
{
Id = id;
}
public override Type GetPropertyValueType(PublishedPropertyType propertyType)
=> typeof (string);
public int Id { get; }
public int TemplateId { get; set; }
public int SortOrder { get; set; }
public string Name { get; set; }
public string UrlName { get; set; }
public string DocumentTypeAlias => ContentType.Alias;
public int DocumentTypeId { get; set; }
public string WriterName { get; set; }
public string CreatorName { get; set; }
public int WriterId { get; set; }
public int CreatorId { get; set; }
public string Path { get; set; }
public DateTime CreateDate { get; set; }
public DateTime UpdateDate { get; set; }
public Guid Version { get; set; }
public int Level { get; set; }
public string Url { get; set; }
public PublishedItemType ItemType => ContentType.ItemType;
public bool IsDraft { get; set; }
public IPublishedContent Parent { get; set; }
public IEnumerable<IPublishedContent> Children { get; set; }
public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> PropertyCacheLevel.Content;
// copied from PublishedContentBase
public IPublishedProperty GetProperty(string alias, bool recurse)
{
var property = GetProperty(alias);
if (recurse == false) return property;
IPublishedContent content = this;
var firstNonNullProperty = property;
while (content != null && (property == null || property.HasValue == false))
{
content = content.Parent;
property = content?.GetProperty(alias);
if (firstNonNullProperty == null && property != null) firstNonNullProperty = property;
}
// if we find a content with the property with a value, return that property
// if we find no content with the property, return null
// if we find a content with the property without a value, return that property
// have to save that first property while we look further up, hence firstNonNullProperty
return property != null && property.HasValue ? property : firstNonNullProperty;
}
}
public class TestConverter2 : PropertyValueConverterBase
{
private readonly IFacade _facade;
public TestConverter2(IFacade facade)
{
_facade = facade;
}
public override bool IsConverter(PublishedPropertyType propertyType)
=> propertyType.PropertyEditorAlias == "editor2";
// pretend ... when writing the converter, the model type for alias "set1" does not exist yet
public override Type GetPropertyValueType(PublishedPropertyType propertyType)
=> typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1"));
public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
=> PropertyCacheLevel.Snapshot;
public override object ConvertSourceToInter(IPropertySet owner, PublishedPropertyType propertyType, object source, bool preview)
{
var s = source as string;
return s?.Split(',').Select(int.Parse).ToArray() ?? Array.Empty<int>();
}
public override object ConvertInterToObject(IPropertySet owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
return ((int[]) inter).Select(x => (TestContentModel1) _facade.ContentCache.GetById(x)).ToArray();
}
}
#endregion
}
}

View File

@@ -179,7 +179,7 @@ namespace Umbraco.Web.Models
while (content != null && (property == null || property.HasValue == false))
{
content = content.Parent;
property = content == null ? null : content.GetProperty(alias);
property = content?.GetProperty(alias);
if (firstNonNullProperty == null && property != null) firstNonNullProperty = property;
}

View File

@@ -55,7 +55,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
public override bool HasValue => _sourceValue != null
&& ((_sourceValue is string) == false || string.IsNullOrWhiteSpace((string)_sourceValue) == false);
&& (!(_sourceValue is string) || string.IsNullOrWhiteSpace((string) _sourceValue) == false);
private class CacheValues
{

View File

@@ -27,13 +27,13 @@ namespace Umbraco.Web.PublishedCache.NuCache
// snapshot cache (if we don't want to pollute the snapshot cache with short-lived
// data) depending on settings
// for members, always cache in the facade cache - never pollute snapshot cache
var facade = (Facade)_facadeAccessor.Facade;
var facade = (Facade) _facadeAccessor.Facade;
var cache = facade == null
? null
: ((IsPreviewing == false || FacadeService.FullCacheWhenPreviewing) && (IsMember == false)
? facade.SnapshotCache
: facade.FacadeCache);
return GetCacheValues(cache);
return GetCacheValues(cache);
}
protected override CacheValues GetFacadeCacheValues()

View File

@@ -4,7 +4,7 @@ using Umbraco.Core.PropertyEditors;
namespace Umbraco.Web.PublishedCache
{
class PropertySetProperty : PropertySetPropertyBase
internal class PropertySetProperty : PropertySetPropertyBase
{
public PropertySetProperty(PublishedPropertyType propertyType, IPropertySet set, bool previewing, PropertyCacheLevel cacheLevel, object sourceValue = null)
: base(propertyType, set, previewing, cacheLevel, sourceValue)

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Web.PublishedCache
}
public override bool HasValue => _sourceValue != null
&& ((_sourceValue is string) == false || string.IsNullOrWhiteSpace((string)_sourceValue) == false);
&& (!(_sourceValue is string) || string.IsNullOrWhiteSpace((string) _sourceValue) == false);
protected class CacheValues
{
@@ -38,30 +38,12 @@ namespace Umbraco.Web.PublishedCache
public object XPathValue;
}
private static void ValidateCacheLevel(PropertyCacheLevel cacheLevel)
{
switch (cacheLevel)
{
case PropertyCacheLevel.Content:
case PropertyCacheLevel.Snapshot:
case PropertyCacheLevel.Facade:
case PropertyCacheLevel.None:
break;
default:
throw new Exception("Invalid cache level.");
}
}
private void GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel)
{
// based upon the current reference cache level (ReferenceCacheLevel) and this property
// cache level (PropertyType.CacheLevel), determines both the actual cache level for the
// property, and the new reference cache level.
// sanity checks
ValidateCacheLevel(ReferenceCacheLevel);
ValidateCacheLevel(PropertyType.CacheLevel);
// if the property cache level is 'shorter-termed' that the reference
// then use it and it becomes the new reference, else use Content and
// don't change the reference.
@@ -71,7 +53,7 @@ namespace Umbraco.Web.PublishedCache
// snapshot, ok to use content. OTOH, currently caching at snapshot,
// property specifies facade, need to use facade.
//
if (PropertyType.CacheLevel > ReferenceCacheLevel)
if (PropertyType.CacheLevel > ReferenceCacheLevel || PropertyType.CacheLevel == PropertyCacheLevel.None)
{
cacheLevel = PropertyType.CacheLevel;
referenceCacheLevel = cacheLevel;
@@ -130,8 +112,7 @@ namespace Umbraco.Web.PublishedCache
{
lock (_locko)
{
PropertyCacheLevel cacheLevel, referenceCacheLevel;
GetCacheLevels(out cacheLevel, out referenceCacheLevel);
GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
var cacheValues = GetCacheValues(cacheLevel);
if (cacheValues.ObjectInitialized) return cacheValues.ObjectValue;
@@ -149,8 +130,7 @@ namespace Umbraco.Web.PublishedCache
{
lock (_locko)
{
PropertyCacheLevel cacheLevel, referenceCacheLevel;
GetCacheLevels(out cacheLevel, out referenceCacheLevel);
GetCacheLevels(out PropertyCacheLevel cacheLevel, out PropertyCacheLevel referenceCacheLevel);
var cacheValues = GetCacheValues(cacheLevel);
if (cacheValues.XPathInitialized) return cacheValues.XPathValue;

View File

@@ -20,7 +20,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
/// <summary>
/// Implements a facade service.
/// </summary>
class FacadeService : FacadeServiceBase
internal class FacadeService : FacadeServiceBase
{
private readonly XmlStore _xmlStore;
private readonly RoutesCache _routesCache;
@@ -242,6 +242,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
public override IPublishedProperty CreateSetProperty(PublishedPropertyType propertyType, IPropertySet set, bool previewing, PropertyCacheLevel referenceCacheLevel, object sourceValue = null)
{
// fixme - ouch?
throw new NotImplementedException();
}