U4-8551 - import NuCache code
This commit is contained in:
151
src/Umbraco.Core/Cache/DictionaryCacheProvider.cs
Normal file
151
src/Umbraco.Core/Cache/DictionaryCacheProvider.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Core.Plugins;
|
||||
|
||||
namespace Umbraco.Core.Cache
|
||||
{
|
||||
class DictionaryCacheProvider : ICacheProvider
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Lazy<object>> _items
|
||||
= new ConcurrentDictionary<string, Lazy<object>>();
|
||||
|
||||
public void ClearAllCache()
|
||||
{
|
||||
_items.Clear();
|
||||
}
|
||||
|
||||
public void ClearCacheItem(string key)
|
||||
{
|
||||
Lazy<object> item;
|
||||
_items.TryRemove(key, out item);
|
||||
}
|
||||
|
||||
public void ClearCacheObjectTypes(string typeName)
|
||||
{
|
||||
var type = TypeFinder.GetTypeByName(typeName);
|
||||
if (type == null) return;
|
||||
var isInterface = type.IsInterface;
|
||||
|
||||
Lazy<object> item;
|
||||
foreach (var kvp in _items
|
||||
.Where(x =>
|
||||
{
|
||||
// entry.Value is Lazy<object> and not null, its value may be null
|
||||
// remove null values as well, does not hurt
|
||||
// get non-created as NonCreatedValue & exceptions as null
|
||||
var value = DictionaryCacheProviderBase.GetSafeLazyValue(x.Value, true);
|
||||
|
||||
// if T is an interface remove anything that implements that interface
|
||||
// otherwise remove exact types (not inherited types)
|
||||
return value == null || (isInterface ? (type.IsInstanceOfType(value)) : (value.GetType() == type));
|
||||
}))
|
||||
_items.TryRemove(kvp.Key, out item);
|
||||
}
|
||||
|
||||
public void ClearCacheObjectTypes<T>()
|
||||
{
|
||||
var typeOfT = typeof(T);
|
||||
var isInterface = typeOfT.IsInterface;
|
||||
|
||||
Lazy<object> item;
|
||||
foreach (var kvp in _items
|
||||
.Where(x =>
|
||||
{
|
||||
// entry.Value is Lazy<object> and not null, its value may be null
|
||||
// remove null values as well, does not hurt
|
||||
// compare on exact type, don't use "is"
|
||||
// get non-created as NonCreatedValue & exceptions as null
|
||||
var value = DictionaryCacheProviderBase.GetSafeLazyValue(x.Value, true);
|
||||
|
||||
// if T is an interface remove anything that implements that interface
|
||||
// otherwise remove exact types (not inherited types)
|
||||
return value == null || (isInterface ? (value is T) : (value.GetType() == typeOfT));
|
||||
}))
|
||||
_items.TryRemove(kvp.Key, out item);
|
||||
}
|
||||
|
||||
public void ClearCacheObjectTypes<T>(Func<string, T, bool> predicate)
|
||||
{
|
||||
var typeOfT = typeof(T);
|
||||
var isInterface = typeOfT.IsInterface;
|
||||
|
||||
Lazy<object> item;
|
||||
foreach (var kvp in _items
|
||||
.Where(x =>
|
||||
{
|
||||
// entry.Value is Lazy<object> and not null, its value may be null
|
||||
// remove null values as well, does not hurt
|
||||
// compare on exact type, don't use "is"
|
||||
// get non-created as NonCreatedValue & exceptions as null
|
||||
var value = DictionaryCacheProviderBase.GetSafeLazyValue(x.Value, true);
|
||||
if (value == null) return true;
|
||||
|
||||
// if T is an interface remove anything that implements that interface
|
||||
// otherwise remove exact types (not inherited types)
|
||||
return (isInterface ? (value is T) : (value.GetType() == typeOfT))
|
||||
// run predicate on the 'public key' part only, ie without prefix
|
||||
&& predicate(x.Key, (T)value);
|
||||
}))
|
||||
_items.TryRemove(kvp.Key, out item);
|
||||
}
|
||||
|
||||
public void ClearCacheByKeySearch(string keyStartsWith)
|
||||
{
|
||||
Lazy<object> item;
|
||||
foreach (var ikvp in _items
|
||||
.Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith)))
|
||||
_items.TryRemove(ikvp.Key, out item);
|
||||
}
|
||||
|
||||
public void ClearCacheByKeyExpression(string regexString)
|
||||
{
|
||||
Lazy<object> item;
|
||||
foreach (var ikvp in _items
|
||||
.Where(kvp => Regex.IsMatch(kvp.Key, regexString)))
|
||||
_items.TryRemove(ikvp.Key, out item);
|
||||
}
|
||||
|
||||
public IEnumerable<object> GetCacheItemsByKeySearch(string keyStartsWith)
|
||||
{
|
||||
return _items
|
||||
.Where(kvp => kvp.Key.InvariantStartsWith(keyStartsWith))
|
||||
.Select(kvp => DictionaryCacheProviderBase.GetSafeLazyValue(kvp.Value))
|
||||
.Where(x => x != null);
|
||||
}
|
||||
|
||||
public IEnumerable<object> GetCacheItemsByKeyExpression(string regexString)
|
||||
{
|
||||
return _items
|
||||
.Where(kvp => Regex.IsMatch(kvp.Key, regexString))
|
||||
.Select(kvp => DictionaryCacheProviderBase.GetSafeLazyValue(kvp.Value))
|
||||
.Where(x => x != null);
|
||||
}
|
||||
|
||||
public object GetCacheItem(string cacheKey)
|
||||
{
|
||||
Lazy<object> result;
|
||||
_items.TryGetValue(cacheKey, out result); // else null
|
||||
return result == null ? null : DictionaryCacheProviderBase.GetSafeLazyValue(result); // return exceptions as null
|
||||
}
|
||||
|
||||
public object GetCacheItem(string cacheKey, Func<object> getCacheItem)
|
||||
{
|
||||
var result = _items.GetOrAdd(cacheKey, k => DictionaryCacheProviderBase.GetSafeLazy(getCacheItem));
|
||||
|
||||
var value = result.Value; // will not throw (safe lazy)
|
||||
var eh = value as DictionaryCacheProviderBase.ExceptionHolder;
|
||||
if (eh == null)
|
||||
return value;
|
||||
|
||||
// and... it's in the cache anyway - so contrary to other cache providers,
|
||||
// which would trick with GetSafeLazyValue, we need to remove by ourselves,
|
||||
// in order NOT to cache exceptions
|
||||
|
||||
_items.TryRemove(cacheKey, out result);
|
||||
throw eh.Exception; // throw once!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace Umbraco.Core.Configuration
|
||||
/// Gets the version comment (like beta or RC).
|
||||
/// </summary>
|
||||
/// <value>The version comment.</value>
|
||||
public static string CurrentComment => "alpha0002";
|
||||
public static string CurrentComment => "alpha0004";
|
||||
|
||||
// Get the version of the Umbraco.Core.dll by looking at a class in that dll
|
||||
// Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Persistence.DatabaseAnnotations;
|
||||
|
||||
namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionEight
|
||||
{
|
||||
[Migration("8.0.0", 100, GlobalSettings.UmbracoMigrationName)]
|
||||
class AddContentNuTable : MigrationBase
|
||||
{
|
||||
public AddContentNuTable(IMigrationContext context)
|
||||
: base(context)
|
||||
{ }
|
||||
|
||||
public override void Up()
|
||||
{
|
||||
var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray();
|
||||
if (tables.InvariantContains("cmsContentNu")) return;
|
||||
|
||||
var textType = SqlSyntax.GetSpecialDbType(SpecialDbTypes.NTEXT);
|
||||
|
||||
Create.Table("cmsContentNu")
|
||||
.WithColumn("nodeId").AsInt32().NotNullable()
|
||||
.WithColumn("published").AsBoolean().NotNullable()
|
||||
.WithColumn("data").AsCustom(textType).NotNullable()
|
||||
.WithColumn("rv").AsInt64().NotNullable().WithDefaultValue(0);
|
||||
|
||||
Create.PrimaryKey("PK_cmsContentNu")
|
||||
.OnTable("cmsContentNu")
|
||||
.Columns(new[] { "nodeId", "published" });
|
||||
|
||||
Create.ForeignKey("FK_cmsContentNu_umbracoNode_id")
|
||||
.FromTable("cmsContentNu")
|
||||
.ForeignColumn("nodeId")
|
||||
.ToTable("umbracoNode")
|
||||
.PrimaryColumn("id")
|
||||
.OnDelete(Rule.Cascade)
|
||||
.OnUpdate(Rule.None);
|
||||
}
|
||||
|
||||
public override void Down()
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,9 @@ namespace Umbraco.Core.Persistence
|
||||
// failed: exists (due to race cond RC1)
|
||||
// RC2 race cond here: another thread may remove the record
|
||||
|
||||
// fixme - debugging, ok?
|
||||
throw;
|
||||
|
||||
// try to update
|
||||
rowCount = updateCommand.IsNullOrWhiteSpace()
|
||||
? db.Update(poco)
|
||||
|
||||
28
src/Umbraco.Core/Serialization/ForceInt32Converter.cs
Normal file
28
src/Umbraco.Core/Serialization/ForceInt32Converter.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Umbraco.Core.Serialization
|
||||
{
|
||||
internal class ForceInt32Converter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof (object) || objectType == typeof (int);
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var jsonValue = serializer.Deserialize<JValue>(reader);
|
||||
|
||||
return jsonValue.Type == JTokenType.Integer
|
||||
? jsonValue.Value<int>()
|
||||
: serializer.Deserialize(reader);
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,7 @@
|
||||
<Compile Include="Cache\CacheKeys.cs" />
|
||||
<Compile Include="Cache\CacheProviderExtensions.cs" />
|
||||
<Compile Include="Cache\DefaultRepositoryCachePolicy.cs" />
|
||||
<Compile Include="Cache\DictionaryCacheProvider.cs" />
|
||||
<Compile Include="Cache\FullDataSetRepositoryCachePolicy.cs" />
|
||||
<Compile Include="Cache\ICacheProvider.cs" />
|
||||
<Compile Include="Cache\CacheRefresherBase.cs" />
|
||||
@@ -256,6 +257,7 @@
|
||||
<DependentUpon>Files.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Models\DictionaryItemExtensions.cs" />
|
||||
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionEight\AddContentNuTable.cs" />
|
||||
<Compile Include="Persistence\Migrations\Upgrades\TargetVersionEight\RefactorXmlColumns.cs" />
|
||||
<Compile Include="Plugins\HideFromTypeFinderAttribute.cs" />
|
||||
<Compile Include="HttpContextExtensions.cs" />
|
||||
@@ -438,6 +440,7 @@
|
||||
<Compile Include="PropertyEditors\ValueConverters\DecimalValueConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\LabelValueConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\ImageCropperValueConverter.cs" />
|
||||
<Compile Include="Serialization\ForceInt32Converter.cs" />
|
||||
<Compile Include="Services\Changes\ContentTypeChange.cs" />
|
||||
<Compile Include="Services\Changes\ContentTypeChangeExtensions.cs" />
|
||||
<Compile Include="Services\Changes\ContentTypeChangeTypes.cs" />
|
||||
|
||||
710
src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
Normal file
710
src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
Normal file
@@ -0,0 +1,710 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Web.PublishedCache.NuCache;
|
||||
|
||||
namespace Umbraco.Tests.Cache
|
||||
{
|
||||
[TestFixture]
|
||||
public class SnapDictionaryTests
|
||||
{
|
||||
[Test]
|
||||
public void LiveGenUpdate()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length);
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Clear(1);
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length); // gone
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(0, d.Test.FloorGen);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OtherGenUpdate()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length);
|
||||
Assert.AreEqual(0, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s = d.CreateSnapshot();
|
||||
Assert.AreEqual(1, s.Gen);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 2
|
||||
d.Clear(1);
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length); // there
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
Assert.AreEqual(0, d.Test.FloorGen);
|
||||
|
||||
GC.KeepAlive(s);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MissingReturnsNull()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
var s = d.CreateSnapshot();
|
||||
|
||||
Assert.IsNull(s.Get(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DeletedReturnsNull()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
Assert.AreEqual("one", s1.Get(1));
|
||||
|
||||
// gen 2
|
||||
d.Clear(1);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
Assert.IsNull(s2.Get(1));
|
||||
|
||||
Assert.AreEqual("one", s1.Get(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void CollectValues()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 2
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var tv = d.Test.GetValues(1);
|
||||
Assert.AreEqual(3, tv[0].Gen);
|
||||
Assert.AreEqual(2, tv[1].Gen);
|
||||
Assert.AreEqual(1, tv[2].Gen);
|
||||
|
||||
Assert.AreEqual(0, d.Test.FloorGen);
|
||||
|
||||
// nothing to collect
|
||||
await d.CollectAsync();
|
||||
GC.KeepAlive(s1);
|
||||
GC.KeepAlive(s2);
|
||||
Assert.AreEqual(0, d.Test.FloorGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(2, d.SnapCount);
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
// one snapshot to collect
|
||||
s1 = null;
|
||||
GC.Collect();
|
||||
GC.KeepAlive(s2);
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(1, d.Test.FloorGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
// another snapshot to collect
|
||||
s2 = null;
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(2, d.Test.FloorGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void ProperlyCollects()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
for (var i = 0; i < 32; i++)
|
||||
{
|
||||
d.Set(i, i.ToString());
|
||||
d.CreateSnapshot().Dispose();
|
||||
}
|
||||
|
||||
Assert.AreEqual(32, d.GenCount);
|
||||
Assert.AreEqual(0, d.SnapCount); // because we've disposed them
|
||||
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(32, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual(0, d.GenCount);
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
Assert.AreEqual(32, d.Count);
|
||||
|
||||
for (var i = 0; i < 32; i++)
|
||||
d.Set(i, null);
|
||||
|
||||
d.CreateSnapshot().Dispose();
|
||||
|
||||
// because we haven't collected yet, but disposed nevertheless
|
||||
Assert.AreEqual(1, d.GenCount);
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
Assert.AreEqual(32, d.Count);
|
||||
|
||||
// once we collect, they are all gone
|
||||
// since noone is interested anymore
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(0, d.GenCount);
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
Assert.AreEqual(0, d.Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void CollectNulls()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 2
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
d.Clear(1);
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var tv = d.Test.GetValues(1);
|
||||
Assert.AreEqual(3, tv[0].Gen);
|
||||
Assert.AreEqual(2, tv[1].Gen);
|
||||
Assert.AreEqual(1, tv[2].Gen);
|
||||
|
||||
Assert.AreEqual(0, d.Test.FloorGen);
|
||||
|
||||
// nothing to collect
|
||||
await d.CollectAsync();
|
||||
GC.KeepAlive(s1);
|
||||
GC.KeepAlive(s2);
|
||||
Assert.AreEqual(0, d.Test.FloorGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(2, d.SnapCount);
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
// one snapshot to collect
|
||||
s1 = null;
|
||||
GC.Collect();
|
||||
GC.KeepAlive(s2);
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(1, d.Test.FloorGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
// another snapshot to collect
|
||||
s2 = null;
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(2, d.Test.FloorGen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
|
||||
// and everything is gone?
|
||||
// no, cannot collect the live gen because we'd need to lock
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
d.CreateSnapshot();
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
|
||||
// poof, gone
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void EventuallyCollectNulls()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length);
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
await d.CollectAsync();
|
||||
var tv = d.Test.GetValues(1);
|
||||
Assert.AreEqual(1, tv.Length);
|
||||
Assert.AreEqual(1, tv[0].Gen);
|
||||
|
||||
var s = d.CreateSnapshot();
|
||||
Assert.AreEqual("one", s.Get(1));
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
Assert.AreEqual(1, d.Count);
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
Assert.AreEqual(1, d.GenCount);
|
||||
|
||||
// gen 2
|
||||
d.Clear(1);
|
||||
tv = d.Test.GetValues(1);
|
||||
Assert.AreEqual(2, tv.Length);
|
||||
Assert.AreEqual(2, tv[0].Gen);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
Assert.AreEqual(1, d.Count);
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
Assert.AreEqual(1, d.GenCount);
|
||||
|
||||
// nothing to collect
|
||||
await d.CollectAsync();
|
||||
GC.KeepAlive(s);
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Count);
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
Assert.AreEqual(1, d.GenCount);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
// collect snapshot
|
||||
// don't collect liveGen+
|
||||
s = null; // without being disposed
|
||||
GC.Collect(); // should release the generation reference
|
||||
await d.CollectAsync();
|
||||
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length); // "one" value is gone
|
||||
Assert.AreEqual(1, d.Count); // still have 1 item
|
||||
Assert.AreEqual(0, d.SnapCount); // snapshot is gone
|
||||
Assert.AreEqual(0, d.GenCount); // and generation has been dequeued
|
||||
|
||||
// liveGen/nextGen
|
||||
s = d.CreateSnapshot();
|
||||
s = null;
|
||||
|
||||
// collect liveGen
|
||||
GC.Collect();
|
||||
|
||||
SnapDictionary<int, string>.GenerationObject genObj;
|
||||
Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
|
||||
genObj = null;
|
||||
|
||||
// in Release mode, it works, but in Debug mode, the weak reference is still alive
|
||||
// and for some reason we need to do this to ensure it is collected
|
||||
#if DEBUG
|
||||
await d.CollectAsync();
|
||||
GC.Collect();
|
||||
#endif
|
||||
|
||||
Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
|
||||
Assert.IsFalse(genObj.WeakReference.IsAlive); // snapshot is gone, along with its reference
|
||||
|
||||
await d.CollectAsync();
|
||||
|
||||
Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone
|
||||
Assert.AreEqual(0, d.Count); // item is gone
|
||||
Assert.AreEqual(0, d.Test.GenerationObjects.Count);
|
||||
Assert.AreEqual(0, d.SnapCount); // snapshot is gone
|
||||
Assert.AreEqual(0, d.GenCount); // and generation has been dequeued
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void CollectDisposedSnapshots()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 2
|
||||
d.Set(1, "two");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 3
|
||||
d.Set(1, "three");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s3 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
Assert.AreEqual(3, d.SnapCount);
|
||||
|
||||
s1.Dispose();
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(2, d.SnapCount);
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
s2.Dispose();
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
s3.Dispose();
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void CollectGcSnapshots()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 2
|
||||
d.Set(1, "two");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
// gen 3
|
||||
d.Set(1, "three");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s3 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
|
||||
Assert.AreEqual(3, d.SnapCount);
|
||||
|
||||
s1 = s2 = s3 = null;
|
||||
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(3, d.SnapCount);
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void RandomTest1()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
d.Set(1, "one");
|
||||
d.Set(2, "two");
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
var v1 = s1.Get(1);
|
||||
Assert.AreEqual("one", v1);
|
||||
|
||||
d.Set(1, "uno");
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
var v2 = s2.Get(1);
|
||||
Assert.AreEqual("uno", v2);
|
||||
|
||||
v1 = s1.Get(1);
|
||||
Assert.AreEqual("one", v1);
|
||||
|
||||
Assert.AreEqual(2, d.SnapCount);
|
||||
|
||||
s1 = null;
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
|
||||
// in Release mode, it works, but in Debug mode, the weak reference is still alive
|
||||
// and for some reason we need to do this to ensure it is collected
|
||||
#if DEBUG
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
#endif
|
||||
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
v2 = s2.Get(1);
|
||||
Assert.AreEqual("uno", v2);
|
||||
|
||||
s2 = null;
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async void RandomTest2()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
d.Set(1, "one");
|
||||
d.Set(2, "two");
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
var v1 = s1.Get(1);
|
||||
Assert.AreEqual("one", v1);
|
||||
|
||||
d.Clear(1);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
var v2 = s2.Get(1);
|
||||
Assert.AreEqual(null, v2);
|
||||
|
||||
v1 = s1.Get(1);
|
||||
Assert.AreEqual("one", v1);
|
||||
|
||||
Assert.AreEqual(2, d.SnapCount);
|
||||
|
||||
s1 = null;
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
|
||||
// in Release mode, it works, but in Debug mode, the weak reference is still alive
|
||||
// and for some reason we need to do this to ensure it is collected
|
||||
#if DEBUG
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
#endif
|
||||
|
||||
Assert.AreEqual(1, d.SnapCount);
|
||||
v2 = s2.Get(1);
|
||||
Assert.AreEqual(null, v2);
|
||||
|
||||
s2 = null;
|
||||
GC.Collect();
|
||||
await d.CollectAsync();
|
||||
|
||||
Assert.AreEqual(0, d.SnapCount);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteLockingFirstSnapshot()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
d.WriteLocked(() =>
|
||||
{
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(0, s1.Gen);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
Assert.IsNull(s1.Get(1));
|
||||
});
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, s2.Gen);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual("one", s2.Get(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteLocking()
|
||||
{
|
||||
var d = new SnapDictionary<int, string>();
|
||||
d.Test.CollectAuto = false;
|
||||
|
||||
// gen 1
|
||||
d.Set(1, "one");
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s1 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(1, s1.Gen);
|
||||
Assert.AreEqual(1, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual("one", s1.Get(1));
|
||||
|
||||
// gen 2
|
||||
Assert.AreEqual(1, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "uno");
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s2 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(2, s2.Gen);
|
||||
Assert.AreEqual(2, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual("uno", s2.Get(1));
|
||||
|
||||
d.WriteLocked(() =>
|
||||
{
|
||||
// gen 3
|
||||
Assert.AreEqual(2, d.Test.GetValues(1).Length);
|
||||
d.Set(1, "ein");
|
||||
Assert.AreEqual(3, d.Test.GetValues(1).Length);
|
||||
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen);
|
||||
|
||||
var s3 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(2, s3.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsTrue(d.Test.NextGen); // has NOT changed when (non) creating snapshot
|
||||
Assert.AreEqual("uno", s3.Get(1));
|
||||
});
|
||||
|
||||
var s4 = d.CreateSnapshot();
|
||||
|
||||
Assert.AreEqual(3, s4.Gen);
|
||||
Assert.AreEqual(3, d.Test.LiveGen);
|
||||
Assert.IsFalse(d.Test.NextGen);
|
||||
Assert.AreEqual("ein", s4.Get(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Cache\RefresherTests.cs" />
|
||||
<Compile Include="Cache\SnapDictionaryTests.cs" />
|
||||
<Compile Include="CoreXml\RenamedRootNavigatorTests.cs" />
|
||||
<Compile Include="Integration\ContentEventsTests.cs" />
|
||||
<Compile Include="Migrations\MigrationIssuesTests.cs" />
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
function facadeStatusController($scope, umbRequestHelper, $log, $http, $q, $timeout) {
|
||||
|
||||
// note: must defined 'facaStatusBaseUrl' in BackOfficeController
|
||||
|
||||
umbRequestHelper.resourcePromise(
|
||||
$http.get(umbRequestHelper.getApiUrl('facadeStatusBaseUrl', 'GetFacadeStatusUrl')),
|
||||
'Failed to get facade status url')
|
||||
.then(function (result) {
|
||||
$scope.includeUrl = angular.fromJson(result);
|
||||
});
|
||||
|
||||
//$scope.includeUrl = 'views/dashboard/developer/xmldataintegrityreport.html';
|
||||
|
||||
}
|
||||
angular.module("umbraco").controller("Umbraco.Dashboard.FacadeStatusController", facadeStatusController);
|
||||
@@ -0,0 +1,9 @@
|
||||
<div id="facade" ng-controller="Umbraco.Dashboard.FacadeStatusController">
|
||||
<h3>Facade Status</h3>
|
||||
|
||||
<div ng-show="loading">
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<div ng-include="includeUrl"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
function nuCacheController($scope, umbRequestHelper, $log, $http, $q, $timeout) {
|
||||
|
||||
$scope.reload = function () {
|
||||
if ($scope.working) return;
|
||||
if (confirm("Trigger a in-memory and local file cache reload on all servers.")) {
|
||||
$scope.working = true;
|
||||
umbRequestHelper.resourcePromise(
|
||||
$http.post(umbRequestHelper.getApiUrl("nuCacheStatusBaseUrl", "ReloadCache")),
|
||||
'Failed to trigger a cache reload')
|
||||
.then(function (result) {
|
||||
$scope.working = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.collect = function () {
|
||||
if ($scope.working) return;
|
||||
$scope.working = true;
|
||||
umbRequestHelper.resourcePromise(
|
||||
$http.get(umbRequestHelper.getApiUrl("nuCacheStatusBaseUrl", "Collect")),
|
||||
'Failed to verify the cache.')
|
||||
.then(function (result) {
|
||||
$scope.working = false;
|
||||
$scope.status = angular.fromJson(result);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.verify = function () {
|
||||
if ($scope.working) return;
|
||||
$scope.working = true;
|
||||
umbRequestHelper.resourcePromise(
|
||||
$http.get(umbRequestHelper.getApiUrl("nuCacheStatusBaseUrl", "GetStatus")),
|
||||
'Failed to verify the cache.')
|
||||
.then(function (result) {
|
||||
$scope.working = false;
|
||||
$scope.status = angular.fromJson(result);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.rebuild = function () {
|
||||
if ($scope.working) return;
|
||||
if (confirm("Rebuild cmsContentNu table content. Expensive.")) {
|
||||
$scope.working = true;
|
||||
umbRequestHelper.resourcePromise(
|
||||
$http.post(umbRequestHelper.getApiUrl("nuCacheStatusBaseUrl", "RebuildDbCache")),
|
||||
'Failed to rebuild the cache.')
|
||||
.then(function (result) {
|
||||
$scope.working = false;
|
||||
$scope.status = angular.fromJson(result);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.working = false;
|
||||
$scope.verify();
|
||||
}
|
||||
angular.module("umbraco").controller("Umbraco.Dashboard.NuCacheController", nuCacheController);
|
||||
@@ -0,0 +1,54 @@
|
||||
<div id="nuCache" ng-controller="Umbraco.Dashboard.NuCacheController">
|
||||
|
||||
<div ng-show="loading">
|
||||
Loading...
|
||||
</div>
|
||||
|
||||
<p>You are running the brand new NuCache!</p>
|
||||
|
||||
<p style="min-height: 120px; background: #f8f8f8; padding: 4px;">
|
||||
<!-- ng-if or ng-show? -->
|
||||
<span ng-if="working">NuCache is working.</span>
|
||||
<span ng-if="!working">NuCache says: {{status}}</span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This lets you refresh the status, or collect snapshots (after running a CLR GC).
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" ng-click="verify()" class="btn btn-warning">
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
<button type="button" ng-click="collect()" class="btn btn-warning">
|
||||
<span>Collect</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p></p>
|
||||
<p>
|
||||
This lets you rebuild the database cache (status above), ie the content of the cmsContentNu table.
|
||||
Rebuilding can be expensive.
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" ng-click="rebuild()" class="btn btn-warning">
|
||||
<span>Rebuild</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p></p>
|
||||
<p>
|
||||
This lets you reload the in-memory and local file cache by entirely reloading it from
|
||||
the data in the cmsContentNu table, but it does not rebuild that table. This is relatively
|
||||
fast. Triggers the reload on all servers in an LB environment.
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" ng-click="reload()" class="btn btn-warning">
|
||||
<span>Reload</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="umb-loader-wrapper" ng-show="working">
|
||||
<div class="umb-loader"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -37,6 +37,11 @@
|
||||
views/dashboard/developer/examinemanagement.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Facade Status">
|
||||
<control>
|
||||
views/dashboard/developer/facadestatus.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
|
||||
<section alias="StartupMediaDashboardSection">
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
views/dashboard/developer/examinemanagement.html
|
||||
</control>
|
||||
</tab>
|
||||
<tab caption="Facade Status">
|
||||
<control>
|
||||
views/dashboard/developer/facadestatus.html
|
||||
</control>
|
||||
</tab>
|
||||
</section>
|
||||
<section alias="StartupMediaDashboardSection">
|
||||
<areas>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<packages>
|
||||
<package id="AutoMapper" version="4.2.1" targetFramework="net461" />
|
||||
<package id="ClientDependency" version="1.9.0-beta4" targetFramework="net45" />
|
||||
<package id="CSharpTest.Net.Collections" version="14.906.1403.1082" targetFramework="net461" />
|
||||
<package id="ClientDependency-Mvc5" version="1.8.0.0" targetFramework="net45" />
|
||||
<package id="Examine" version="0.1.68.0" targetFramework="net45" />
|
||||
<package id="Examine" version="2.0.0-beta2" targetFramework="net461" />
|
||||
<package id="ImageProcessor" version="2.4.1.0" targetFramework="net45" />
|
||||
<package id="ImageProcessor.Web" version="4.6.1.0" targetFramework="net45" />
|
||||
<package id="log4net" version="2.0.5" targetFramework="net461" />
|
||||
|
||||
@@ -363,6 +363,14 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
"healthCheckBaseUrl", Url.GetUmbracoApiServiceBaseUrl<HealthCheckController>(
|
||||
controller => controller.GetAllHealthChecks())
|
||||
},
|
||||
{
|
||||
"facadeStatusBaseUrl", Url.GetUmbracoApiServiceBaseUrl<FacadeStatusController>(
|
||||
controller => controller.GetFacadeStatusUrl())
|
||||
},
|
||||
{
|
||||
"nuCacheStatusBaseUrl", Url.GetUmbracoApiServiceBaseUrl<NuCacheStatusController>(
|
||||
controller => controller.GetStatus())
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
78
src/Umbraco.Web/PublishedCache/NuCache/CacheKeys.cs
Normal file
78
src/Umbraco.Web/PublishedCache/NuCache/CacheKeys.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
static class CacheKeys
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static string DraftOrPub(bool previewing)
|
||||
{
|
||||
return previewing ? "D:" : "P:";
|
||||
}
|
||||
|
||||
public static string PublishedContentChildren(Guid contentUid, bool previewing)
|
||||
{
|
||||
return "NuCache.Content.Children[" + DraftOrPub(previewing) + ":" + contentUid + "]";
|
||||
}
|
||||
|
||||
public static string ContentCacheRoots(bool previewing)
|
||||
{
|
||||
return "NuCache.ContentCache.Roots[" + DraftOrPub(previewing) + "]";
|
||||
}
|
||||
|
||||
public static string MediaCacheRoots(bool previewing)
|
||||
{
|
||||
return "NuCache.MediaCache.Roots[" + DraftOrPub(previewing) + "]";
|
||||
}
|
||||
|
||||
public static string PublishedContentAsPreviewing(Guid contentUid)
|
||||
{
|
||||
return "NuCache.Content.AsPreviewing[" + contentUid + "]";
|
||||
}
|
||||
|
||||
public static string ProfileName(int userId)
|
||||
{
|
||||
return "NuCache.Profile.Name[" + userId + "]";
|
||||
}
|
||||
|
||||
public static string PropertyRecurse(Guid contentUid, string typeAlias, bool previewing)
|
||||
{
|
||||
return "NuCache.Property.Recurse[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]";
|
||||
}
|
||||
|
||||
public static string PropertyValueSet(Guid contentUid, string typeAlias, bool previewing)
|
||||
{
|
||||
return "NuCache.Property.ValueSet[" + DraftOrPub(previewing) + contentUid + ":" + typeAlias + "]";
|
||||
}
|
||||
|
||||
// routes still use int id and not Guid uid, because routable nodes must have
|
||||
// a valid ID in the database at that point, whereas content and properties
|
||||
// may be virtual (and not in umbracoNode).
|
||||
|
||||
public static string ContentCacheRouteByContent(int id, bool previewing)
|
||||
{
|
||||
return "NuCache.ContentCache.RouteByContent[" + DraftOrPub(previewing) + id + "]";
|
||||
}
|
||||
|
||||
public static string ContentCacheContentByRoute(string route, bool previewing)
|
||||
{
|
||||
return "NuCache.ContentCache.ContentByRoute[" + DraftOrPub(previewing) + route + "]";
|
||||
}
|
||||
|
||||
//public static string ContentCacheRouteByContentStartsWith()
|
||||
//{
|
||||
// return "NuCache.ContentCache.RouteByContent[";
|
||||
//}
|
||||
|
||||
//public static string ContentCacheContentByRouteStartsWith()
|
||||
//{
|
||||
// return "NuCache.ContentCache.ContentByRoute[";
|
||||
//}
|
||||
|
||||
public static string MemberCacheMember(string name, bool previewing, object p)
|
||||
{
|
||||
return "NuCache.MemberCache." + name + "[" + DraftOrPub(previewing) + p + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
386
src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs
Normal file
386
src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs
Normal file
@@ -0,0 +1,386 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.XPath;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Xml;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
using Umbraco.Web.PublishedCache.NuCache.Navigable;
|
||||
using Umbraco.Web.Routing;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable
|
||||
{
|
||||
private readonly ContentStore2.Snapshot _snapshot;
|
||||
private readonly ICacheProvider _facadeCache;
|
||||
private readonly ICacheProvider _snapshotCache;
|
||||
private readonly DomainHelper _domainHelper;
|
||||
|
||||
#region Constructor
|
||||
|
||||
public ContentCache(bool previewDefault, ContentStore2.Snapshot snapshot, ICacheProvider facadeCache, ICacheProvider snapshotCache, DomainHelper domainHelper)
|
||||
: base(previewDefault)
|
||||
{
|
||||
_snapshot = snapshot;
|
||||
_facadeCache = facadeCache;
|
||||
_snapshotCache = snapshotCache;
|
||||
_domainHelper = domainHelper;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Routes
|
||||
|
||||
// routes can be
|
||||
// "/"
|
||||
// "123/"
|
||||
// "/path/to/node"
|
||||
// "123/path/to/node"
|
||||
|
||||
// at the moment we try our best to be backward compatible, but really,
|
||||
// should get rid of hideTopLevelNode and other oddities entirely, eventually
|
||||
|
||||
public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null)
|
||||
{
|
||||
return GetByRoute(PreviewDefault, route, hideTopLevelNode);
|
||||
}
|
||||
|
||||
public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null)
|
||||
{
|
||||
if (route == null) throw new ArgumentNullException("route");
|
||||
|
||||
var cache = (preview == false || FacadeService.FullCacheWhenPreviewing) ? _snapshotCache : _facadeCache;
|
||||
var key = CacheKeys.ContentCacheContentByRoute(route, preview);
|
||||
return cache.GetCacheItem<IPublishedContent>(key, () => GetByRouteInternal(preview, route, hideTopLevelNode));
|
||||
}
|
||||
|
||||
private IPublishedContent GetByRouteInternal(bool preview, string route, bool? hideTopLevelNode)
|
||||
{
|
||||
hideTopLevelNode = hideTopLevelNode ?? GlobalSettings.HideTopLevelNodeFromPath; // default = settings
|
||||
|
||||
// the route always needs to be lower case because we only store the urlName attribute in lower case
|
||||
route = route.ToLowerInvariant();
|
||||
|
||||
var pos = route.IndexOf('/');
|
||||
var path = pos == 0 ? route : route.Substring(pos);
|
||||
var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos));
|
||||
var parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
IPublishedContent content;
|
||||
|
||||
if (startNodeId > 0)
|
||||
{
|
||||
// if in a domain then start with the root node of the domain
|
||||
// and follow the path
|
||||
// note: if domain has a path (eg example.com/en) which is not recommended anymore
|
||||
// then then /en part of the domain is basically ignored here...
|
||||
content = GetById(preview, startNodeId);
|
||||
content = FollowRoute(content, parts, 0);
|
||||
}
|
||||
else if (parts.Length == 0)
|
||||
{
|
||||
// if not in a domain, and path is empty - what is the default page?
|
||||
// let's say it is the first one in the tree, if any -- order by sortOrder
|
||||
content = GetAtRoot(preview).FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
// if not in a domain...
|
||||
// hideTopLevelNode = support legacy stuff, look for /*/path/to/node
|
||||
// else normal, look for /path/to/node
|
||||
content = hideTopLevelNode.Value
|
||||
? GetAtRoot(preview).SelectMany(x => x.Children).FirstOrDefault(x => x.UrlName == parts[0])
|
||||
: GetAtRoot(preview).FirstOrDefault(x => x.UrlName == parts[0]);
|
||||
content = FollowRoute(content, parts, 1);
|
||||
}
|
||||
|
||||
// if hideTopLevelNodePath is true then for url /foo we looked for /*/foo
|
||||
// but maybe that was the url of a non-default top-level node, so we also
|
||||
// have to look for /foo (see note in ApplyHideTopLevelNodeFromPath).
|
||||
if (content == null && hideTopLevelNode.Value && parts.Length == 1)
|
||||
{
|
||||
content = GetAtRoot(preview).FirstOrDefault(x => x.UrlName == parts[0]);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public string GetRouteById(int contentId)
|
||||
{
|
||||
return GetRouteById(PreviewDefault, contentId);
|
||||
}
|
||||
|
||||
public string GetRouteById(bool preview, int contentId)
|
||||
{
|
||||
var cache = (preview == false || FacadeService.FullCacheWhenPreviewing) ? _snapshotCache : _facadeCache;
|
||||
var key = CacheKeys.ContentCacheRouteByContent(contentId, preview);
|
||||
return cache.GetCacheItem<string>(key, () => GetRouteByIdInternal(preview, contentId, null));
|
||||
}
|
||||
|
||||
private string GetRouteByIdInternal(bool preview, int contentId, bool? hideTopLevelNode)
|
||||
{
|
||||
var node = GetById(preview, contentId);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
hideTopLevelNode = hideTopLevelNode ?? GlobalSettings.HideTopLevelNodeFromPath; // default = settings
|
||||
|
||||
// walk up from that node until we hit a node with a domain,
|
||||
// or we reach the content root, collecting urls in the way
|
||||
var pathParts = new List<string>();
|
||||
var n = node;
|
||||
var hasDomains = _domainHelper.NodeHasDomains(n.Id);
|
||||
while (hasDomains == false && n != null) // n is null at root
|
||||
{
|
||||
// get the url
|
||||
var urlName = n.UrlName;
|
||||
pathParts.Add(urlName);
|
||||
|
||||
// move to parent node
|
||||
n = n.Parent;
|
||||
hasDomains = n != null && _domainHelper.NodeHasDomains(n.Id);
|
||||
}
|
||||
|
||||
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
|
||||
if (hasDomains == false && hideTopLevelNode.Value)
|
||||
ApplyHideTopLevelNodeFromPath(node, pathParts, preview);
|
||||
|
||||
// assemble the route
|
||||
pathParts.Reverse();
|
||||
var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
|
||||
var route = (n == null ? "" : n.Id.ToString(CultureInfo.InvariantCulture)) + path;
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
private static IPublishedContent FollowRoute(IPublishedContent content, IReadOnlyList<string> parts, int start)
|
||||
{
|
||||
var i = start;
|
||||
while (content != null && i < parts.Count)
|
||||
{
|
||||
var part = parts[i++];
|
||||
content = content.Children.FirstOrDefault(x => x.UrlName == part);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private void ApplyHideTopLevelNodeFromPath(IPublishedContent content, IList<string> segments, bool preview)
|
||||
{
|
||||
// in theory if hideTopLevelNodeFromPath is true, then there should be only one
|
||||
// top-level node, or else domains should be assigned. but for backward compatibility
|
||||
// we add this check - we look for the document matching "/" and if it's not us, then
|
||||
// we do not hide the top level path
|
||||
// it has to be taken care of in GetByRoute too so if
|
||||
// "/foo" fails (looking for "/*/foo") we try also "/foo".
|
||||
// this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but
|
||||
// that's the way it works pre-4.10 and we try to be backward compat for the time being
|
||||
if (content.Parent == null)
|
||||
{
|
||||
var rootNode = GetByRoute(preview, "/", true);
|
||||
if (rootNode == null)
|
||||
throw new Exception("Failed to get node at /.");
|
||||
if (rootNode.Id == content.Id) // remove only if we're the default node
|
||||
segments.RemoveAt(segments.Count - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
segments.RemoveAt(segments.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get, Has
|
||||
|
||||
public override IPublishedContent GetById(bool preview, int contentId)
|
||||
{
|
||||
var n = _snapshot.Get(contentId);
|
||||
if (n == null) return null;
|
||||
|
||||
// both .Draft and .Published cannot be null at the same time
|
||||
return preview
|
||||
? n.Draft ?? GetPublishedContentAsPreviewing(n.Published)
|
||||
: n.Published;
|
||||
}
|
||||
|
||||
public override bool HasById(bool preview, int contentId)
|
||||
{
|
||||
var n = _snapshot.Get(contentId);
|
||||
if (n == null) return false;
|
||||
|
||||
return preview || n.Published != null;
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview)
|
||||
{
|
||||
if (FacadeService.CacheContentCacheRoots == false)
|
||||
return GetAtRootNoCache(preview);
|
||||
|
||||
var facade = Facade.Current;
|
||||
var cache = (facade == null)
|
||||
? null
|
||||
: (preview == false || FacadeService.FullCacheWhenPreviewing
|
||||
? facade.SnapshotCache
|
||||
: facade.FacadeCache);
|
||||
|
||||
if (cache == null)
|
||||
return GetAtRootNoCache(preview);
|
||||
|
||||
// note: ToArray is important here, we want to cache the result, not the function!
|
||||
return (IEnumerable<IPublishedContent>) cache.GetCacheItem(
|
||||
CacheKeys.ContentCacheRoots(preview),
|
||||
() => GetAtRootNoCache(preview).ToArray());
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent> GetAtRootNoCache(bool preview)
|
||||
{
|
||||
var c = _snapshot.GetAtRoot();
|
||||
|
||||
// both .Draft and .Published cannot be null at the same time
|
||||
return c.Select(n => preview
|
||||
? n.Draft ?? GetPublishedContentAsPreviewing(n.Published)
|
||||
: n.Published).WhereNotNull().OrderBy(x => x.SortOrder);
|
||||
}
|
||||
|
||||
// gets a published content as a previewing draft, if preview is true
|
||||
// this is for published content when previewing
|
||||
internal static IPublishedContent GetPublishedContentAsPreviewing(IPublishedContent content /*, bool preview*/)
|
||||
{
|
||||
if (content == null /*|| preview == false*/) return null; //content;
|
||||
|
||||
// an object in the cache is either an IPublishedContentOrMedia,
|
||||
// or a model inheriting from PublishedContentExtended - in which
|
||||
// case we need to unwrap to get to the original IPublishedContentOrMedia.
|
||||
|
||||
var inner = PublishedContent.UnwrapIPublishedContent(content);
|
||||
return inner.AsPreviewingModel();
|
||||
}
|
||||
|
||||
public override bool HasContent(bool preview)
|
||||
{
|
||||
return preview
|
||||
? _snapshot.IsEmpty == false
|
||||
: _snapshot.GetAtRoot().Any(x => x.Published != null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region XPath
|
||||
|
||||
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetSingleByXPath(iterator);
|
||||
}
|
||||
|
||||
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetSingleByXPath(iterator);
|
||||
}
|
||||
|
||||
private static IPublishedContent GetSingleByXPath(XPathNodeIterator iterator)
|
||||
{
|
||||
if (iterator.MoveNext() == false) return null;
|
||||
|
||||
var xnav = iterator.Current as NavigableNavigator;
|
||||
if (xnav == null) return null;
|
||||
|
||||
var xcontent = xnav.UnderlyingObject as NavigableContent;
|
||||
return xcontent == null ? null : xcontent.InnerContent;
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetByXPath(iterator);
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetByXPath(iterator);
|
||||
}
|
||||
|
||||
private static IEnumerable<IPublishedContent> GetByXPath(XPathNodeIterator iterator)
|
||||
{
|
||||
while (iterator.MoveNext())
|
||||
{
|
||||
var xnav = iterator.Current as NavigableNavigator;
|
||||
if (xnav == null) continue;
|
||||
|
||||
var xcontent = xnav.UnderlyingObject as NavigableContent;
|
||||
if (xcontent == null) continue;
|
||||
|
||||
yield return xcontent.InnerContent;
|
||||
}
|
||||
}
|
||||
|
||||
public override XPathNavigator CreateNavigator(bool preview)
|
||||
{
|
||||
var source = new Source(this, preview);
|
||||
var navigator = new NavigableNavigator(source);
|
||||
return navigator;
|
||||
}
|
||||
|
||||
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
|
||||
{
|
||||
var source = new Source(this, preview);
|
||||
var navigator = new NavigableNavigator(source);
|
||||
return navigator.CloneWithNewRoot(id, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Detached
|
||||
|
||||
// detached is something that needs to be refactored entirely eventually
|
||||
// detached property should accept the "container content" guid
|
||||
// etc
|
||||
|
||||
public IPublishedProperty CreateDetachedProperty(PublishedPropertyType propertyType, object value, bool isPreviewing)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Content types
|
||||
|
||||
public override PublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _snapshot.GetContentType(id);
|
||||
}
|
||||
|
||||
public override PublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _snapshot.GetContentType(alias);
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByContentType(PublishedContentType contentType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_snapshot.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
156
src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs
Normal file
156
src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.PublishedCache.NuCache.DataSource;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
// represents a content "node" ie a pair of draft + published versions
|
||||
// internal, never exposed, to be accessed from ContentStore (only!)
|
||||
internal class ContentNode
|
||||
{
|
||||
// special ctor with no content data - for members
|
||||
public ContentNode(int id, Guid uid, PublishedContentType contentType,
|
||||
int level, string path, int sortOrder,
|
||||
int parentContentId,
|
||||
DateTime createDate, int creatorId)
|
||||
{
|
||||
Id = id;
|
||||
Uid = uid;
|
||||
ContentType = contentType;
|
||||
Level = level;
|
||||
Path = path;
|
||||
SortOrder = sortOrder;
|
||||
ParentContentId = parentContentId;
|
||||
CreateDate = createDate;
|
||||
CreatorId = creatorId;
|
||||
|
||||
ChildContentIds = new List<int>();
|
||||
}
|
||||
|
||||
public ContentNode(int id, Guid uid, PublishedContentType contentType,
|
||||
int level, string path, int sortOrder,
|
||||
int parentContentId,
|
||||
DateTime createDate, int creatorId,
|
||||
ContentData draftData, ContentData publishedData)
|
||||
: this(id, uid, level, path, sortOrder, parentContentId, createDate, creatorId)
|
||||
{
|
||||
SetContentTypeAndData(contentType, draftData, publishedData);
|
||||
}
|
||||
|
||||
// 2-phases ctor, phase 1
|
||||
public ContentNode(int id, Guid uid,
|
||||
int level, string path, int sortOrder,
|
||||
int parentContentId,
|
||||
DateTime createDate, int creatorId)
|
||||
{
|
||||
Id = id;
|
||||
Uid = uid;
|
||||
Level = level;
|
||||
Path = path;
|
||||
SortOrder = sortOrder;
|
||||
ParentContentId = parentContentId;
|
||||
CreateDate = createDate;
|
||||
CreatorId = creatorId;
|
||||
|
||||
ChildContentIds = new List<int>();
|
||||
}
|
||||
|
||||
// two-phase ctor, phase 2
|
||||
public void SetContentTypeAndData(PublishedContentType contentType, ContentData draftData, ContentData publishedData)
|
||||
{
|
||||
ContentType = contentType;
|
||||
|
||||
if (draftData == null && publishedData == null)
|
||||
throw new ArgumentException("Both draftData and publishedData cannot be null at the same time.");
|
||||
|
||||
if (draftData != null)
|
||||
Draft = new PublishedContent(this, draftData).CreateModel();
|
||||
if (publishedData != null)
|
||||
Published = new PublishedContent(this, publishedData).CreateModel();
|
||||
}
|
||||
|
||||
// clone parent
|
||||
private ContentNode(ContentNode origin)
|
||||
{
|
||||
// everything is the same, except for the child items
|
||||
// list which is a clone of the original list
|
||||
|
||||
Id = origin.Id;
|
||||
Uid = origin.Uid;
|
||||
ContentType = origin.ContentType;
|
||||
Level = origin.Level;
|
||||
Path = origin.Path;
|
||||
SortOrder = origin.SortOrder;
|
||||
ParentContentId = origin.ParentContentId;
|
||||
CreateDate = origin.CreateDate;
|
||||
CreatorId = origin.CreatorId;
|
||||
|
||||
var originDraft = origin.Draft == null ? null : PublishedContent.UnwrapIPublishedContent(origin.Draft);
|
||||
var originPublished = origin.Published == null ? null : PublishedContent.UnwrapIPublishedContent(origin.Published);
|
||||
|
||||
Draft = originDraft == null ? null : new PublishedContent(this, originDraft).CreateModel();
|
||||
Published = originPublished == null ? null : new PublishedContent(this, originPublished).CreateModel();
|
||||
|
||||
ChildContentIds = new List<int>(origin.ChildContentIds); // needs to be *another* list
|
||||
}
|
||||
|
||||
// clone with new content type
|
||||
public ContentNode(ContentNode origin, PublishedContentType contentType)
|
||||
{
|
||||
Id = origin.Id;
|
||||
Uid = origin.Uid;
|
||||
ContentType = contentType; // change!
|
||||
Level = origin.Level;
|
||||
Path = origin.Path;
|
||||
SortOrder = origin.SortOrder;
|
||||
ParentContentId = origin.ParentContentId;
|
||||
CreateDate = origin.CreateDate;
|
||||
CreatorId = origin.CreatorId;
|
||||
|
||||
var originDraft = origin.Draft == null ? null : PublishedContent.UnwrapIPublishedContent(origin.Draft);
|
||||
var originPublished = origin.Published == null ? null : PublishedContent.UnwrapIPublishedContent(origin.Published);
|
||||
|
||||
Draft = originDraft == null ? null : new PublishedContent(this, originDraft._contentData).CreateModel();
|
||||
Published = originPublished == null ? null : new PublishedContent(this, originPublished._contentData).CreateModel();
|
||||
|
||||
ChildContentIds = origin.ChildContentIds; // can be the *same* list FIXME oh really?
|
||||
}
|
||||
|
||||
// everything that is common to both draft and published versions
|
||||
// keep this as small as possible
|
||||
public readonly int Id;
|
||||
public readonly Guid Uid;
|
||||
public PublishedContentType ContentType;
|
||||
public readonly int Level;
|
||||
public readonly string Path;
|
||||
public readonly int SortOrder;
|
||||
public readonly int ParentContentId;
|
||||
public List<int> ChildContentIds;
|
||||
public readonly DateTime CreateDate;
|
||||
public readonly int CreatorId;
|
||||
|
||||
// draft and published version (either can be null, but not both)
|
||||
public IPublishedContent Draft;
|
||||
public IPublishedContent Published;
|
||||
|
||||
public ContentNode CloneParent()
|
||||
{
|
||||
return new ContentNode(this);
|
||||
}
|
||||
|
||||
public ContentNodeKit ToKit()
|
||||
{
|
||||
return new ContentNodeKit
|
||||
{
|
||||
Node = this,
|
||||
ContentTypeId = ContentType.Id,
|
||||
// ReSharper disable MergeConditionalExpression
|
||||
DraftData = Draft == null ? null : ((PublishedContent) Draft)._contentData,
|
||||
PublishedData = Published == null ? null : ((PublishedContent) Published)._contentData
|
||||
// ReSharper restore MergeConditionalExpression
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs
Normal file
21
src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.PublishedCache.NuCache.DataSource;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
// what's needed to actually build a content node
|
||||
struct ContentNodeKit
|
||||
{
|
||||
public ContentNode Node;
|
||||
public int ContentTypeId;
|
||||
public ContentData DraftData;
|
||||
public ContentData PublishedData;
|
||||
|
||||
public bool IsEmpty { get { return Node == null; } }
|
||||
|
||||
public void Build(PublishedContentType contentType)
|
||||
{
|
||||
Node.SetContentTypeAndData(contentType, DraftData, PublishedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
1041
src/Umbraco.Web/PublishedCache/NuCache/ContentStore2.cs
Normal file
1041
src/Umbraco.Web/PublishedCache/NuCache/ContentStore2.cs
Normal file
File diff suppressed because it is too large
Load Diff
204
src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs
Normal file
204
src/Umbraco.Web/PublishedCache/NuCache/DataSource/BTree.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using CSharpTest.Net.Collections;
|
||||
using CSharpTest.Net.Serialization;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
class BTree
|
||||
{
|
||||
public static BPlusTree<int, ContentNodeKit> GetTree(string filepath, bool exists)
|
||||
{
|
||||
var keySerializer = new PrimitiveSerializer();
|
||||
var valueSerializer = new ContentNodeKitSerializer();
|
||||
var options = new BPlusTree<int, ContentNodeKit>.OptionsV2(keySerializer, valueSerializer)
|
||||
{
|
||||
CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always,
|
||||
FileName = filepath,
|
||||
|
||||
// other options?
|
||||
};
|
||||
|
||||
var tree = new BPlusTree<int, ContentNodeKit>(options);
|
||||
|
||||
// anything?
|
||||
//btree.
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
class ContentNodeKitSerializer : ISerializer<ContentNodeKit>
|
||||
{
|
||||
static readonly ContentDataSerializer DataSerializer = new ContentDataSerializer();
|
||||
//static readonly ListOfIntSerializer ChildContentIdsSerializer = new ListOfIntSerializer();
|
||||
|
||||
public ContentNodeKit ReadFrom(Stream stream)
|
||||
{
|
||||
var kit = new ContentNodeKit
|
||||
{
|
||||
Node = new ContentNode(
|
||||
PrimitiveSerializer.Int32.ReadFrom(stream), // id
|
||||
PrimitiveSerializer.Guid.ReadFrom(stream), // uid
|
||||
PrimitiveSerializer.Int32.ReadFrom(stream), // level
|
||||
PrimitiveSerializer.String.ReadFrom(stream), // path
|
||||
PrimitiveSerializer.Int32.ReadFrom(stream), // sort order
|
||||
PrimitiveSerializer.Int32.ReadFrom(stream), // parent id
|
||||
PrimitiveSerializer.DateTime.ReadFrom(stream), // date created
|
||||
PrimitiveSerializer.Int32.ReadFrom(stream) // creator id
|
||||
),
|
||||
ContentTypeId = PrimitiveSerializer.Int32.ReadFrom(stream)
|
||||
};
|
||||
var hasDraft = PrimitiveSerializer.Boolean.ReadFrom(stream);
|
||||
if (hasDraft)
|
||||
kit.DraftData = DataSerializer.ReadFrom(stream);
|
||||
var hasPublished = PrimitiveSerializer.Boolean.ReadFrom(stream);
|
||||
if (hasPublished)
|
||||
kit.PublishedData = DataSerializer.ReadFrom(stream);
|
||||
return kit;
|
||||
}
|
||||
|
||||
public void WriteTo(ContentNodeKit value, Stream stream)
|
||||
{
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Node.Id, stream);
|
||||
PrimitiveSerializer.Guid.WriteTo(value.Node.Uid, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Node.Level, stream);
|
||||
PrimitiveSerializer.String.WriteTo(value.Node.Path, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Node.SortOrder, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Node.ParentContentId, stream);
|
||||
PrimitiveSerializer.DateTime.WriteTo(value.Node.CreateDate, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Node.CreatorId, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.ContentTypeId, stream);
|
||||
|
||||
PrimitiveSerializer.Boolean.WriteTo(value.DraftData != null, stream);
|
||||
if (value.DraftData != null)
|
||||
DataSerializer.WriteTo(value.DraftData, stream);
|
||||
|
||||
PrimitiveSerializer.Boolean.WriteTo(value.PublishedData != null, stream);
|
||||
if (value.PublishedData != null)
|
||||
DataSerializer.WriteTo(value.PublishedData, stream);
|
||||
}
|
||||
}
|
||||
|
||||
class ContentDataSerializer : ISerializer<ContentData>
|
||||
{
|
||||
private readonly static DictionaryOfValuesSerializer PropertiesSerializer = new DictionaryOfValuesSerializer();
|
||||
|
||||
public ContentData ReadFrom(Stream stream)
|
||||
{
|
||||
return new ContentData
|
||||
{
|
||||
Published = PrimitiveSerializer.Boolean.ReadFrom(stream),
|
||||
Name = PrimitiveSerializer.String.ReadFrom(stream),
|
||||
Version = PrimitiveSerializer.Guid.ReadFrom(stream),
|
||||
VersionDate = PrimitiveSerializer.DateTime.ReadFrom(stream),
|
||||
WriterId = PrimitiveSerializer.Int32.ReadFrom(stream),
|
||||
TemplateId = PrimitiveSerializer.Int32.ReadFrom(stream),
|
||||
Properties = PropertiesSerializer.ReadFrom(stream)
|
||||
};
|
||||
}
|
||||
|
||||
public void WriteTo(ContentData value, Stream stream)
|
||||
{
|
||||
PrimitiveSerializer.Boolean.WriteTo(value.Published, stream);
|
||||
PrimitiveSerializer.String.WriteTo(value.Name, stream);
|
||||
PrimitiveSerializer.Guid.WriteTo(value.Version, stream);
|
||||
PrimitiveSerializer.DateTime.WriteTo(value.VersionDate, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.WriterId, stream);
|
||||
PrimitiveSerializer.Int32.WriteTo(value.TemplateId, stream);
|
||||
PropertiesSerializer.WriteTo(value.Properties, stream);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class ListOfIntSerializer : ISerializer<List<int>>
|
||||
{
|
||||
public List<int> ReadFrom(Stream stream)
|
||||
{
|
||||
var list = new List<int>();
|
||||
var count = PrimitiveSerializer.Int32.ReadFrom(stream);
|
||||
for (var i = 0; i < count; i++)
|
||||
list.Add(PrimitiveSerializer.Int32.ReadFrom(stream));
|
||||
return list;
|
||||
}
|
||||
|
||||
public void WriteTo(List<int> value, Stream stream)
|
||||
{
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Count, stream);
|
||||
foreach (var item in value)
|
||||
PrimitiveSerializer.Int32.WriteTo(item, stream);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
class DictionaryOfValuesSerializer : ISerializer<IDictionary<string, object>>
|
||||
{
|
||||
public IDictionary<string, object> ReadFrom(Stream stream)
|
||||
{
|
||||
var dict = new Dictionary<string, object>();
|
||||
var count = PrimitiveSerializer.Int32.ReadFrom(stream);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var key = PrimitiveSerializer.String.ReadFrom(stream);
|
||||
var type = PrimitiveSerializer.Char.ReadFrom(stream);
|
||||
switch (type)
|
||||
{
|
||||
case 'N':
|
||||
dict.Add(key, null);
|
||||
break;
|
||||
case 'S':
|
||||
dict.Add(key, PrimitiveSerializer.String.ReadFrom(stream));
|
||||
break;
|
||||
case 'I':
|
||||
dict.Add(key, PrimitiveSerializer.Int32.ReadFrom(stream));
|
||||
break;
|
||||
case 'L':
|
||||
dict.Add(key, PrimitiveSerializer.Int64.ReadFrom(stream));
|
||||
break;
|
||||
case 'D':
|
||||
dict.Add(key, PrimitiveSerializer.DateTime.ReadFrom(stream));
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException("Cannot deserialize '" + type + "' value.");
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
public void WriteTo(IDictionary<string, object> value, Stream stream)
|
||||
{
|
||||
PrimitiveSerializer.Int32.WriteTo(value.Count, stream);
|
||||
foreach (var kvp in value)
|
||||
{
|
||||
PrimitiveSerializer.String.WriteTo(kvp.Key, stream);
|
||||
if (kvp.Value == null)
|
||||
{
|
||||
PrimitiveSerializer.Char.WriteTo('N', stream);
|
||||
}
|
||||
else if (kvp.Value is string)
|
||||
{
|
||||
PrimitiveSerializer.Char.WriteTo('S', stream);
|
||||
PrimitiveSerializer.String.WriteTo((string) kvp.Value, stream);
|
||||
}
|
||||
else if (kvp.Value is int)
|
||||
{
|
||||
PrimitiveSerializer.Char.WriteTo('I', stream);
|
||||
PrimitiveSerializer.Int32.WriteTo((int) kvp.Value, stream);
|
||||
}
|
||||
else if (kvp.Value is long)
|
||||
{
|
||||
PrimitiveSerializer.Char.WriteTo('L', stream);
|
||||
PrimitiveSerializer.Int64.WriteTo((long) kvp.Value, stream);
|
||||
}
|
||||
else if (kvp.Value is DateTime)
|
||||
{
|
||||
PrimitiveSerializer.Char.WriteTo('D', stream);
|
||||
PrimitiveSerializer.DateTime.WriteTo((DateTime) kvp.Value, stream);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException("Value type " + kvp.Value.GetType().FullName + " cannot be serialized.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
// represents everything that is specific to draft or published version
|
||||
class ContentData
|
||||
{
|
||||
public bool Published { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public Guid Version { get; set; }
|
||||
public DateTime VersionDate { get; set; }
|
||||
public int WriterId { get; set; }
|
||||
public int TemplateId { get; set; }
|
||||
|
||||
public IDictionary<string, object> Properties { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using NPoco;
|
||||
using Umbraco.Core.Models.Rdbms;
|
||||
using Umbraco.Core.Persistence.DatabaseAnnotations;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
[TableName("cmsContentNu")]
|
||||
[PrimaryKey("nodeId", AutoIncrement = false)]
|
||||
[ExplicitColumns]
|
||||
internal class ContentNuDto
|
||||
{
|
||||
[Column("nodeId")]
|
||||
[PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")]
|
||||
[ForeignKey(typeof(ContentDto), Column = "nodeId")]
|
||||
public int NodeId { get; set; }
|
||||
|
||||
[Column("published")]
|
||||
public bool Published { get; set; }
|
||||
|
||||
[Column("data")]
|
||||
[SpecialDbType(SpecialDbTypes.NTEXT)]
|
||||
public string Data { get; set; }
|
||||
|
||||
[Column("rv")]
|
||||
public long Rv { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
// read-only dto
|
||||
internal class ContentSourceDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Guid Uid { get; set; }
|
||||
public int ContentTypeId { get; set; }
|
||||
|
||||
public int Level { get; set; }
|
||||
public string Path { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public int ParentId { get; set; }
|
||||
|
||||
public DateTime CreateDate { get; set; }
|
||||
public int CreatorId { get; set; }
|
||||
|
||||
public string DraftName { get; set; }
|
||||
public Guid DraftVersion { get; set; }
|
||||
public DateTime DraftVersionDate { get; set; }
|
||||
public int DraftWriterId { get; set; }
|
||||
public int DraftTemplateId { get; set; }
|
||||
public string DraftData { get; set; }
|
||||
|
||||
public string PubName { get; set; }
|
||||
public Guid PubVersion { get; set; }
|
||||
public DateTime PubVersionDate { get; set; }
|
||||
public int PubWriterId { get; set; }
|
||||
public int PubTemplateId { get; set; }
|
||||
public string PubData { get; set; }
|
||||
}
|
||||
}
|
||||
293
src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs
Normal file
293
src/Umbraco.Web/PublishedCache/NuCache/DataSource/Database.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NPoco;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Persistence.UnitOfWork;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
|
||||
{
|
||||
// provides efficient database access for NuCache
|
||||
class Database
|
||||
{
|
||||
public ContentNodeKit GetContentSource(IDatabaseUnitOfWork uow, int id)
|
||||
{
|
||||
var dto = uow.Database.Fetch<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.documentUser DraftWriterId, docDraft.templateId DraftTemplateId,
|
||||
nuDraft.data DraftData,
|
||||
docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.documentUser PubWriterId, docPub.templateId PubTemplateId,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0)
|
||||
LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1)
|
||||
LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType AND n.id=@id
|
||||
", new { objType = Constants.ObjectTypes.DocumentGuid, /*id =*/ id })).FirstOrDefault();
|
||||
return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto);
|
||||
}
|
||||
|
||||
public ContentNodeKit GetMediaSource(IDatabaseUnitOfWork uow, int id)
|
||||
{
|
||||
// should be only 1 version for medias
|
||||
|
||||
var dto = uow.Database.Fetch<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
JOIN cmsContentVersion ver ON (ver.contentId=n.id)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType AND n.id=@id
|
||||
", new { objType = Constants.ObjectTypes.MediaGuid, /*id =*/ id })).FirstOrDefault();
|
||||
return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto);
|
||||
}
|
||||
|
||||
// we want arrays, we want them all loaded, not an enumerable
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetAllContentSources(IDatabaseUnitOfWork uow)
|
||||
{
|
||||
return uow.Database.Query<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.documentUser DraftWriterId, docDraft.templateId DraftTemplateId,
|
||||
nuDraft.data DraftData,
|
||||
docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.documentUser PubWriterId, docPub.templateId PubTemplateId,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0)
|
||||
LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1)
|
||||
LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType
|
||||
ORDER BY n.level, n.sortOrder
|
||||
", new { objType = Constants.ObjectTypes.DocumentGuid })).Select(CreateContentNodeKit);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetAllMediaSources(IDatabaseUnitOfWork uow)
|
||||
{
|
||||
// should be only 1 version for medias
|
||||
|
||||
return uow.Database.Query<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
JOIN cmsContentVersion ver ON (ver.contentId=n.id)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType
|
||||
ORDER BY n.level, n.sortOrder
|
||||
", new { objType = Constants.ObjectTypes.MediaGuid })).Select(CreateMediaNodeKit);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetBranchContentSources(IDatabaseUnitOfWork uow, int id)
|
||||
{
|
||||
return uow.Database.Query<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.documentUser DraftWriterId, docDraft.templateId DraftTemplateId,
|
||||
nuDraft.data DraftData,
|
||||
docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.documentUser PubWriterId, docPub.templateId PubTemplateId,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN umbracoNode x ON (n.id=x.id OR n.path LIKE " + uow.Database.SqlSyntax.GetConcat("x.path", "',%'") + @")
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0)
|
||||
LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1)
|
||||
LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType AND x.id=@id
|
||||
ORDER BY n.level, n.sortOrder
|
||||
", new { objType = Constants.ObjectTypes.DocumentGuid, /*id =*/ id })).Select(CreateContentNodeKit);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetBranchMediaSources(IDatabaseUnitOfWork uow, int id)
|
||||
{
|
||||
// should be only 1 version for medias
|
||||
|
||||
return uow.Database.Query<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN umbracoNode x ON (n.id=x.id OR n.path LIKE " + uow.Database.SqlSyntax.GetConcat("x.path", "',%'") + @")
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
JOIN cmsContentVersion ver ON (ver.contentId=n.id)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType AND x.id=@id
|
||||
ORDER BY n.level, n.sortOrder
|
||||
", new { objType = Constants.ObjectTypes.MediaGuid, /*id =*/ id })).Select(CreateMediaNodeKit);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetTypeContentSources(IDatabaseUnitOfWork uow, IEnumerable<int> ids)
|
||||
{
|
||||
return uow.Database.Query<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
docDraft.text DraftName, docDraft.versionId DraftVersion, docDraft.updateDate DraftVersionDate, docDraft.documentUser DraftWriterId, docDraft.templateId DraftTemplateId,
|
||||
nuDraft.data DraftData,
|
||||
docPub.text PubName, docPub.versionId PubVersion, docPub.updateDate PubVersionDate, docPub.documentUser PubWriterId, docPub.templateId PubTemplateId,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
LEFT JOIN cmsDocument docDraft ON (docDraft.nodeId=n.id AND docDraft.newest=1 AND docDraft.published=0)
|
||||
LEFT JOIN cmsDocument docPub ON (docPub.nodeId=n.id AND docPub.published=1)
|
||||
LEFT JOIN cmsContentNu nuDraft ON (nuDraft.nodeId=n.id AND nuDraft.published=0)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType AND cmsContent.contentType IN (@ids)
|
||||
ORDER BY n.level, n.sortOrder
|
||||
", new { objType = Constants.ObjectTypes.DocumentGuid, /*id =*/ ids })).Select(CreateContentNodeKit);
|
||||
}
|
||||
|
||||
public IEnumerable<ContentNodeKit> GetTypeMediaSources(IDatabaseUnitOfWork uow, IEnumerable<int> ids)
|
||||
{
|
||||
// should be only 1 version for medias
|
||||
|
||||
return uow.Database.Query<ContentSourceDto>(new Sql(@"SELECT
|
||||
n.id Id, n.uniqueId Uid,
|
||||
cmsContent.contentType ContentTypeId,
|
||||
n.level Level, n.path Path, n.sortOrder SortOrder, n.parentId ParentId,
|
||||
n.createDate CreateDate, n.nodeUser CreatorId,
|
||||
n.text PubName, ver.versionId PubVersion, ver.versionDate PubVersionDate,
|
||||
nuPub.data PubData
|
||||
FROM umbracoNode n
|
||||
JOIN cmsContent ON (cmsContent.nodeId=n.id)
|
||||
JOIN cmsContentVersion ver ON (ver.contentId=n.id)
|
||||
LEFT JOIN cmsContentNu nuPub ON (nuPub.nodeId=n.id AND nuPub.published=1)
|
||||
WHERE n.nodeObjectType=@objType AND cmsContent.contentType IN (@ids)
|
||||
ORDER BY n.level, n.sortOrder
|
||||
", new { objType = Constants.ObjectTypes.MediaGuid, /*id =*/ ids })).Select(CreateMediaNodeKit);
|
||||
}
|
||||
|
||||
private static ContentNodeKit CreateContentNodeKit(ContentSourceDto dto)
|
||||
{
|
||||
ContentData d = null;
|
||||
ContentData p = null;
|
||||
|
||||
if (dto.DraftVersion != Guid.Empty)
|
||||
{
|
||||
if (dto.DraftData == null)
|
||||
{
|
||||
//throw new Exception("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding.");
|
||||
LogHelper.Warn<Database>("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding.");
|
||||
}
|
||||
else
|
||||
{
|
||||
d = new ContentData
|
||||
{
|
||||
Name = dto.DraftName,
|
||||
Published = false,
|
||||
TemplateId = dto.DraftTemplateId,
|
||||
Version = dto.DraftVersion,
|
||||
VersionDate = dto.DraftVersionDate,
|
||||
WriterId = dto.DraftWriterId,
|
||||
Properties = DeserializeData(dto.DraftData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.PubVersion != Guid.Empty)
|
||||
{
|
||||
if (dto.PubData == null)
|
||||
{
|
||||
//throw new Exception("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding.");
|
||||
LogHelper.Warn<Database>("Missing cmsContentNu content for node " + dto.Id + ", consider rebuilding.");
|
||||
}
|
||||
else
|
||||
{
|
||||
p = new ContentData
|
||||
{
|
||||
Name = dto.PubName,
|
||||
Published = true,
|
||||
TemplateId = dto.PubTemplateId,
|
||||
Version = dto.PubVersion,
|
||||
VersionDate = dto.PubVersionDate,
|
||||
WriterId = dto.PubWriterId,
|
||||
Properties = DeserializeData(dto.PubData)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var n = new ContentNode(dto.Id, dto.Uid,
|
||||
dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId);
|
||||
|
||||
var s = new ContentNodeKit
|
||||
{
|
||||
Node = n,
|
||||
ContentTypeId = dto.ContentTypeId,
|
||||
DraftData = d,
|
||||
PublishedData = p
|
||||
};
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto)
|
||||
{
|
||||
if (dto.PubData == null)
|
||||
throw new Exception("No data for media " + dto.Id);
|
||||
|
||||
var p = new ContentData
|
||||
{
|
||||
Name = dto.PubName,
|
||||
Published = true,
|
||||
TemplateId = -1,
|
||||
Version = dto.PubVersion,
|
||||
VersionDate = dto.PubVersionDate,
|
||||
WriterId = dto.CreatorId, // what-else?
|
||||
Properties = DeserializeData(dto.PubData)
|
||||
};
|
||||
|
||||
var n = new ContentNode(dto.Id, dto.Uid,
|
||||
dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId);
|
||||
|
||||
var s = new ContentNodeKit
|
||||
{
|
||||
Node = n,
|
||||
ContentTypeId = dto.ContentTypeId,
|
||||
PublishedData = p
|
||||
};
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> DeserializeData(string data)
|
||||
{
|
||||
// by default JsonConvert will deserialize our numeric values as Int64
|
||||
// which is bad, because they were Int32 in the database - take care
|
||||
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter> { new ForceInt32Converter() }
|
||||
};
|
||||
|
||||
return JsonConvert.DeserializeObject<Dictionary<string, object>>(data, settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs
Normal file
34
src/Umbraco.Web/PublishedCache/NuCache/DomainCache.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Web.Routing;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
class DomainCache : IDomainCache
|
||||
{
|
||||
private readonly SnapDictionary<int, Domain>.Snapshot _snapshot;
|
||||
|
||||
public DomainCache(SnapDictionary<int, Domain>.Snapshot snapshot)
|
||||
{
|
||||
_snapshot = snapshot;
|
||||
}
|
||||
|
||||
public IEnumerable<Domain> GetAll(bool includeWildcards)
|
||||
{
|
||||
var list = _snapshot.GetAll();
|
||||
if (includeWildcards == false) list = list.Where(x => x.IsWildcard == false);
|
||||
return list;
|
||||
}
|
||||
|
||||
public IEnumerable<Domain> GetAssigned(int contentId, bool includeWildcards)
|
||||
{
|
||||
// probably this could be optimized with an index
|
||||
// but then we'd need a custom DomainStore of some sort
|
||||
|
||||
var list = _snapshot.GetAll();
|
||||
list = list.Where(x => x.ContentId == contentId);
|
||||
if (includeWildcards == false) list = list.Where(x => x.IsWildcard == false);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/Umbraco.Web/PublishedCache/NuCache/Facade.cs
Normal file
126
src/Umbraco.Web/PublishedCache/NuCache/Facade.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.ObjectResolution;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
// implements the facade
|
||||
class Facade : IFacade, IDisposable
|
||||
{
|
||||
private readonly FacadeService _service;
|
||||
private readonly bool _defaultPreview;
|
||||
private FacadeElements _elements;
|
||||
|
||||
#region Constructors
|
||||
|
||||
public Facade(FacadeService service, bool defaultPreview)
|
||||
{
|
||||
_service = service;
|
||||
_defaultPreview = defaultPreview;
|
||||
FacadeCache = new ObjectCacheRuntimeCacheProvider();
|
||||
}
|
||||
|
||||
public class FacadeElements : IDisposable
|
||||
{
|
||||
public ContentCache ContentCache;
|
||||
public MediaCache MediaCache;
|
||||
public MemberCache MemberCache;
|
||||
public DomainCache DomainCache;
|
||||
public ICacheProvider FacadeCache;
|
||||
public ICacheProvider SnapshotCache;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ContentCache.Dispose();
|
||||
MediaCache.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private FacadeElements Elements
|
||||
{
|
||||
get
|
||||
{
|
||||
// no lock - facades are single-thread
|
||||
return _elements ?? (_elements = _service.GetElements(_defaultPreview));
|
||||
}
|
||||
}
|
||||
|
||||
public void Resync()
|
||||
{
|
||||
// no lock - facades are single-thread
|
||||
if (_elements != null)
|
||||
_elements.Dispose();
|
||||
_elements = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Current - for tests
|
||||
|
||||
private static Func<Facade> _getCurrentFacadeFunc = () =>
|
||||
{
|
||||
#if DEBUG
|
||||
if (FacadeServiceResolver.HasCurrent == false) return null;
|
||||
var service = FacadeServiceResolver.Current.Service as FacadeService;
|
||||
if (service == null) return null;
|
||||
return (Facade) service.GetFacadeFunc();
|
||||
#else
|
||||
return (Facade) ((FacadeService) FacadeServiceResolver.Current.Service).GetFacadeFunc();
|
||||
#endif
|
||||
};
|
||||
|
||||
public static Func<Facade> GetCurrentFacadeFunc
|
||||
{
|
||||
get { return _getCurrentFacadeFunc; }
|
||||
set
|
||||
{
|
||||
using (Resolution.Configuration)
|
||||
{
|
||||
_getCurrentFacadeFunc = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Facade Current
|
||||
{
|
||||
get { return _getCurrentFacadeFunc(); }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Caches
|
||||
|
||||
public ICacheProvider FacadeCache { get; private set; }
|
||||
|
||||
public ICacheProvider SnapshotCache { get { return Elements.SnapshotCache; } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region IFacade
|
||||
|
||||
public IPublishedContentCache ContentCache { get { return Elements.ContentCache; } }
|
||||
|
||||
public IPublishedMediaCache MediaCache { get { return Elements.MediaCache; } }
|
||||
|
||||
public IPublishedMemberCache MemberCache { get { return Elements.MemberCache; } }
|
||||
|
||||
public IDomainCache DomainCache { get { return Elements.DomainCache; } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
if (_elements != null)
|
||||
_elements.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
1488
src/Umbraco.Web/PublishedCache/NuCache/FacadeService.cs
Normal file
1488
src/Umbraco.Web/PublishedCache/NuCache/FacadeService.cs
Normal file
File diff suppressed because it is too large
Load Diff
180
src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs
Normal file
180
src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.XPath;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Xml;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
using Umbraco.Web.PublishedCache.NuCache.Navigable;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable
|
||||
{
|
||||
private readonly ContentStore2.Snapshot _snapshot;
|
||||
|
||||
#region Constructors
|
||||
|
||||
public MediaCache(bool previewDefault, ContentStore2.Snapshot snapshot)
|
||||
: base(previewDefault)
|
||||
{
|
||||
_snapshot = snapshot;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get, Has
|
||||
|
||||
public override IPublishedContent GetById(bool preview, int contentId)
|
||||
{
|
||||
var n = _snapshot.Get(contentId);
|
||||
return n == null ? null : n.Published;
|
||||
}
|
||||
|
||||
public override bool HasById(bool preview, int contentId)
|
||||
{
|
||||
var n = _snapshot.Get(contentId);
|
||||
return n != null;
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetAtRoot(bool preview)
|
||||
{
|
||||
if (FacadeService.CacheContentCacheRoots == false)
|
||||
return GetAtRootNoCache(preview);
|
||||
|
||||
var facade = Facade.Current;
|
||||
var cache = (facade == null)
|
||||
? null
|
||||
: (preview == false || FacadeService.FullCacheWhenPreviewing
|
||||
? facade.SnapshotCache
|
||||
: facade.FacadeCache);
|
||||
|
||||
if (cache == null)
|
||||
return GetAtRootNoCache(preview);
|
||||
|
||||
// note: ToArray is important here, we want to cache the result, not the function!
|
||||
return (IEnumerable<IPublishedContent>)cache.GetCacheItem(
|
||||
CacheKeys.MediaCacheRoots(preview),
|
||||
() => GetAtRootNoCache(preview).ToArray());
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent> GetAtRootNoCache(bool preview)
|
||||
{
|
||||
var c = _snapshot.GetAtRoot();
|
||||
|
||||
// there's no .Draft for medias, only non-null .Published
|
||||
// but we may want published as previewing, still
|
||||
return c.Select(n => preview
|
||||
? ContentCache.GetPublishedContentAsPreviewing(n.Published)
|
||||
: n.Published);
|
||||
}
|
||||
|
||||
public override bool HasContent(bool preview)
|
||||
{
|
||||
return _snapshot.IsEmpty == false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region XPath
|
||||
|
||||
public override IPublishedContent GetSingleByXPath(bool preview, string xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetSingleByXPath(iterator);
|
||||
}
|
||||
|
||||
public override IPublishedContent GetSingleByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetSingleByXPath(iterator);
|
||||
}
|
||||
|
||||
private static IPublishedContent GetSingleByXPath(XPathNodeIterator iterator)
|
||||
{
|
||||
if (iterator.MoveNext() == false) return null;
|
||||
|
||||
var xnav = iterator.Current as NavigableNavigator;
|
||||
if (xnav == null) return null;
|
||||
|
||||
var xcontent = xnav.UnderlyingObject as NavigableContent;
|
||||
return xcontent == null ? null : xcontent.InnerContent;
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, string xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetByXPath(iterator);
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByXPath(bool preview, XPathExpression xpath, XPathVariable[] vars)
|
||||
{
|
||||
var navigator = CreateNavigator(preview);
|
||||
var iterator = navigator.Select(xpath, vars);
|
||||
return GetByXPath(iterator);
|
||||
}
|
||||
|
||||
private static IEnumerable<IPublishedContent> GetByXPath(XPathNodeIterator iterator)
|
||||
{
|
||||
while (iterator.MoveNext())
|
||||
{
|
||||
var xnav = iterator.Current as NavigableNavigator;
|
||||
if (xnav == null) continue;
|
||||
|
||||
var xcontent = xnav.UnderlyingObject as NavigableContent;
|
||||
if (xcontent == null) continue;
|
||||
|
||||
yield return xcontent.InnerContent;
|
||||
}
|
||||
}
|
||||
|
||||
public override XPathNavigator CreateNavigator(bool preview)
|
||||
{
|
||||
var source = new Source(this, preview);
|
||||
var navigator = new NavigableNavigator(source);
|
||||
return navigator;
|
||||
}
|
||||
|
||||
public override XPathNavigator CreateNodeNavigator(int id, bool preview)
|
||||
{
|
||||
var source = new Source(this, preview);
|
||||
var navigator = new NavigableNavigator(source);
|
||||
return navigator.CloneWithNewRoot(id, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Content types
|
||||
|
||||
public override PublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _snapshot.GetContentType(id);
|
||||
}
|
||||
|
||||
public override PublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _snapshot.GetContentType(alias);
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> GetByContentType(PublishedContentType contentType)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_snapshot.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
167
src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs
Normal file
167
src/Umbraco.Web/PublishedCache/NuCache/MemberCache.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.XPath;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
using Umbraco.Web.PublishedCache.NuCache.Navigable;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
class MemberCache : IPublishedMemberCache, INavigableData
|
||||
{
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly PublishedContentTypeCache _contentTypeCache;
|
||||
private readonly bool _previewDefault;
|
||||
|
||||
public MemberCache(bool previewDefault, IMemberService memberService, IDataTypeService dataTypeService, PublishedContentTypeCache contentTypeCache)
|
||||
{
|
||||
_memberService = memberService;
|
||||
_dataTypeService = dataTypeService;
|
||||
_previewDefault = previewDefault;
|
||||
_contentTypeCache = contentTypeCache;
|
||||
}
|
||||
|
||||
//private static T GetCacheItem<T>(string cacheKey)
|
||||
// where T : class
|
||||
//{
|
||||
// var facade = Facade.Current;
|
||||
// var cache = facade == null ? null : facade.FacadeCache;
|
||||
// return cache == null
|
||||
// ? null
|
||||
// : (T) cache.GetCacheItem(cacheKey);
|
||||
//}
|
||||
|
||||
private static T GetCacheItem<T>(string cacheKey, Func<T> getCacheItem)
|
||||
where T : class
|
||||
{
|
||||
var facade = Facade.Current;
|
||||
var cache = facade == null ? null : facade.FacadeCache;
|
||||
return cache == null
|
||||
? getCacheItem()
|
||||
: cache.GetCacheItem<T>(cacheKey, getCacheItem);
|
||||
}
|
||||
|
||||
private static void EnsureProvider()
|
||||
{
|
||||
var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
|
||||
if (provider.IsUmbracoMembershipProvider() == false)
|
||||
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
|
||||
}
|
||||
|
||||
public IPublishedContent GetById(bool preview, int memberId)
|
||||
{
|
||||
return GetById(memberId);
|
||||
}
|
||||
|
||||
public IPublishedContent /*IPublishedMember*/ GetById(int memberId)
|
||||
{
|
||||
return GetCacheItem(CacheKeys.MemberCacheMember("ById", _previewDefault, memberId), () =>
|
||||
{
|
||||
EnsureProvider();
|
||||
var member = _memberService.GetById(memberId);
|
||||
return member == null
|
||||
? null
|
||||
: PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault);
|
||||
});
|
||||
}
|
||||
|
||||
private IPublishedContent /*IPublishedMember*/ GetById(IMember member, bool previewing)
|
||||
{
|
||||
return GetCacheItem(CacheKeys.MemberCacheMember("ById", _previewDefault, member.Id), () =>
|
||||
PublishedMember.Create(member, GetContentType(member.ContentTypeId), previewing));
|
||||
}
|
||||
|
||||
public IPublishedContent /*IPublishedMember*/ GetByProviderKey(object key)
|
||||
{
|
||||
return GetCacheItem(CacheKeys.MemberCacheMember("ByProviderKey", _previewDefault, key), () =>
|
||||
{
|
||||
EnsureProvider();
|
||||
var member = _memberService.GetByProviderKey(key);
|
||||
return member == null ? null : GetById(member, _previewDefault);
|
||||
});
|
||||
}
|
||||
|
||||
public IPublishedContent /*IPublishedMember*/ GetByUsername(string username)
|
||||
{
|
||||
return GetCacheItem(CacheKeys.MemberCacheMember("ByUsername", _previewDefault, username), () =>
|
||||
{
|
||||
EnsureProvider();
|
||||
var member = _memberService.GetByUsername(username);
|
||||
return member == null ? null : GetById(member, _previewDefault);
|
||||
});
|
||||
}
|
||||
|
||||
public IPublishedContent /*IPublishedMember*/ GetByEmail(string email)
|
||||
{
|
||||
return GetCacheItem(CacheKeys.MemberCacheMember("ByEmail", _previewDefault, email), () =>
|
||||
{
|
||||
EnsureProvider();
|
||||
var member = _memberService.GetByEmail(email);
|
||||
return member == null ? null : GetById(member, _previewDefault);
|
||||
});
|
||||
}
|
||||
|
||||
public IPublishedContent /*IPublishedMember*/ GetByMember(IMember member)
|
||||
{
|
||||
return PublishedMember.Create(member, GetContentType(member.ContentTypeId), _previewDefault);
|
||||
}
|
||||
|
||||
public IEnumerable<IPublishedContent> GetAtRoot(bool preview)
|
||||
{
|
||||
// because members are flat (not a tree) everything is at root
|
||||
// because we're loading everything... let's just not cache?
|
||||
var members = _memberService.GetAllMembers();
|
||||
return members.Select(m => PublishedMember.Create(m, GetContentType(m.ContentTypeId), preview));
|
||||
}
|
||||
|
||||
public XPathNavigator CreateNavigator()
|
||||
{
|
||||
var source = new Source(this, false);
|
||||
var navigator = new NavigableNavigator(source);
|
||||
return navigator;
|
||||
}
|
||||
|
||||
public XPathNavigator CreateNavigator(bool preview)
|
||||
{
|
||||
return CreateNavigator();
|
||||
}
|
||||
|
||||
public XPathNavigator CreateNodeNavigator(int id, bool preview)
|
||||
{
|
||||
var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider();
|
||||
if (provider.IsUmbracoMembershipProvider() == false)
|
||||
{
|
||||
throw new NotSupportedException("Cannot access this method unless the Umbraco membership provider is active");
|
||||
}
|
||||
|
||||
var result = _memberService.GetById(id);
|
||||
if (result == null) return null;
|
||||
|
||||
var exs = new EntityXmlSerializer();
|
||||
var s = exs.Serialize(_dataTypeService, result);
|
||||
var n = s.GetXmlNode();
|
||||
return n.CreateNavigator();
|
||||
}
|
||||
|
||||
#region Content types
|
||||
|
||||
public PublishedContentType GetContentType(int id)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Member, id);
|
||||
}
|
||||
|
||||
public PublishedContentType GetContentType(string alias)
|
||||
{
|
||||
return _contentTypeCache.Get(PublishedItemType.Member, alias);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Models;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Navigable
|
||||
{
|
||||
interface INavigableData
|
||||
{
|
||||
IPublishedContent GetById(bool preview, int contentId);
|
||||
IEnumerable<IPublishedContent> GetAtRoot(bool preview);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Navigable
|
||||
{
|
||||
class NavigableContent : INavigableContent
|
||||
{
|
||||
private readonly IPublishedContent _icontent;
|
||||
private readonly PublishedContent _content;
|
||||
//private readonly object[] _builtInValues1;
|
||||
private readonly string[] _builtInValues;
|
||||
|
||||
public NavigableContent(IPublishedContent content)
|
||||
{
|
||||
_icontent = content;
|
||||
_content = PublishedContent.UnwrapIPublishedContent(_icontent);
|
||||
|
||||
// built-in properties (attributes)
|
||||
//_builtInValues1 = new object[]
|
||||
// {
|
||||
// _content.Name,
|
||||
// _content.ParentId,
|
||||
// _content.CreateDate,
|
||||
// _content.UpdateDate,
|
||||
// true, // isDoc
|
||||
// _content.SortOrder,
|
||||
// _content.Level,
|
||||
// _content.TemplateId,
|
||||
// _content.WriterId,
|
||||
// _content.CreatorId,
|
||||
// _content.UrlName,
|
||||
// _content.IsDraft
|
||||
// };
|
||||
|
||||
var i = 0;
|
||||
_builtInValues = new []
|
||||
{
|
||||
XmlString(i++, _content.Name),
|
||||
XmlString(i++, _content.ParentId),
|
||||
XmlString(i++, _content.CreateDate),
|
||||
XmlString(i++, _content.UpdateDate),
|
||||
XmlString(i++, true), // isDoc
|
||||
XmlString(i++, _content.SortOrder),
|
||||
XmlString(i++, _content.Level),
|
||||
XmlString(i++, _content.TemplateId),
|
||||
XmlString(i++, _content.WriterId),
|
||||
XmlString(i++, _content.CreatorId),
|
||||
XmlString(i++, _content.UrlName),
|
||||
XmlString(i, _content.IsDraft)
|
||||
};
|
||||
}
|
||||
|
||||
private string XmlString(int index, object value)
|
||||
{
|
||||
var field = Type.FieldTypes[index];
|
||||
return field.XmlStringConverter == null ? value.ToString() : field.XmlStringConverter(value);
|
||||
}
|
||||
|
||||
#region INavigableContent
|
||||
|
||||
public IPublishedContent InnerContent
|
||||
{
|
||||
get { return _icontent; }
|
||||
}
|
||||
|
||||
public int Id
|
||||
{
|
||||
get { return _content.Id; }
|
||||
}
|
||||
|
||||
public int ParentId
|
||||
{
|
||||
get { return _content.ParentId; }
|
||||
}
|
||||
|
||||
public INavigableContentType Type
|
||||
{
|
||||
get { return NavigableContentType.GetContentType(_content.ContentType); }
|
||||
}
|
||||
|
||||
// returns all child ids, will be filtered by the source
|
||||
public IList<int> ChildIds
|
||||
{
|
||||
get { return _content.ChildIds; }
|
||||
}
|
||||
|
||||
public object Value(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
|
||||
if (index < NavigableContentType.BuiltinProperties.Length)
|
||||
{
|
||||
// built-in field, ie attribute
|
||||
//return XmlString(index, _builtInValues1[index]);
|
||||
return _builtInValues[index];
|
||||
}
|
||||
|
||||
index -= NavigableContentType.BuiltinProperties.Length;
|
||||
var properties = _content.PropertiesArray;
|
||||
if (index >= properties.Length)
|
||||
throw new ArgumentOutOfRangeException("index");
|
||||
|
||||
// custom property, ie element
|
||||
return properties[index].XPathValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Navigable
|
||||
{
|
||||
class NavigableContentType : INavigableContentType
|
||||
{
|
||||
public static readonly INavigableFieldType[] BuiltinProperties;
|
||||
private readonly object _locko = new object();
|
||||
|
||||
// called by the conditional weak table -- must be public
|
||||
// ReSharper disable EmptyConstructor
|
||||
public NavigableContentType()
|
||||
// ReSharper restore EmptyConstructor
|
||||
{ }
|
||||
|
||||
// note - PublishedContentType are immutable ie they do not _change_ when the actual IContentTypeComposition
|
||||
// changes, but they are replaced by a new instance, so our map here will clean itself automatically and
|
||||
// we don't have to manage cache - ConditionalWeakTable does not prevent keys from beeing GCed
|
||||
|
||||
static private readonly ConditionalWeakTable<PublishedContentType, NavigableContentType> TypesMap
|
||||
= new ConditionalWeakTable<PublishedContentType,NavigableContentType>();
|
||||
|
||||
public static NavigableContentType GetContentType(PublishedContentType contentType)
|
||||
{
|
||||
return TypesMap.GetOrCreateValue(contentType).EnsureInitialized(contentType);
|
||||
}
|
||||
|
||||
static NavigableContentType()
|
||||
{
|
||||
BuiltinProperties = new INavigableFieldType[]
|
||||
{
|
||||
new NavigablePropertyType("nodeName"),
|
||||
new NavigablePropertyType("parentId"),
|
||||
new NavigablePropertyType("createDate", v => XmlConvert.ToString((DateTime)v, "yyyy-MM-ddTHH:mm:ss")),
|
||||
new NavigablePropertyType("updateDate", v => XmlConvert.ToString((DateTime)v, "yyyy-MM-ddTHH:mm:ss")),
|
||||
new NavigablePropertyType("isDoc", v => XmlConvert.ToString((bool)v)),
|
||||
new NavigablePropertyType("sortOrder"),
|
||||
new NavigablePropertyType("level"),
|
||||
new NavigablePropertyType("templateId"),
|
||||
new NavigablePropertyType("writerId"),
|
||||
new NavigablePropertyType("creatorId"),
|
||||
new NavigablePropertyType("urlName"),
|
||||
new NavigablePropertyType("isDraft", v => XmlConvert.ToString((bool)v))
|
||||
};
|
||||
}
|
||||
|
||||
private NavigableContentType EnsureInitialized(PublishedContentType contentType)
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
if (Name == null)
|
||||
{
|
||||
Name = contentType.Alias;
|
||||
FieldTypes = BuiltinProperties
|
||||
.Union(contentType.PropertyTypes.Select(propertyType => new NavigablePropertyType(propertyType.PropertyTypeAlias)))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
public INavigableFieldType[] FieldTypes { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Navigable
|
||||
{
|
||||
internal class NavigablePropertyType : INavigableFieldType
|
||||
{
|
||||
public NavigablePropertyType(string name, Func<object, string> xmlStringConverter = null)
|
||||
{
|
||||
Name = name;
|
||||
XmlStringConverter = xmlStringConverter;
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
public Func<object, string> XmlStringConverter { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Navigable
|
||||
{
|
||||
class RootContent : INavigableContent
|
||||
{
|
||||
private static readonly RootContentType ContentType = new RootContentType();
|
||||
private readonly int[] _childIds;
|
||||
|
||||
public RootContent(IEnumerable<int> childIds)
|
||||
{
|
||||
_childIds = childIds.ToArray();
|
||||
}
|
||||
|
||||
public int Id
|
||||
{
|
||||
get { return -1; }
|
||||
}
|
||||
|
||||
public int ParentId
|
||||
{
|
||||
get { return -1; }
|
||||
}
|
||||
|
||||
public INavigableContentType Type
|
||||
{
|
||||
get { return ContentType; }
|
||||
}
|
||||
|
||||
public IList<int> ChildIds
|
||||
{
|
||||
get { return _childIds; }
|
||||
}
|
||||
|
||||
public object Value(int index)
|
||||
{
|
||||
// only id has a value
|
||||
return index == 0 ? "-1" : null;
|
||||
}
|
||||
|
||||
class RootContentType : INavigableContentType
|
||||
{
|
||||
public string Name
|
||||
{
|
||||
get { return "root"; }
|
||||
}
|
||||
|
||||
public INavigableFieldType[] FieldTypes
|
||||
{
|
||||
get { return NavigableContentType.BuiltinProperties; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Umbraco.Web/PublishedCache/NuCache/Navigable/Source.cs
Normal file
39
src/Umbraco.Web/PublishedCache/NuCache/Navigable/Source.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Xml.XPath;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache.Navigable
|
||||
{
|
||||
class Source : INavigableSource
|
||||
{
|
||||
private readonly INavigableData _data;
|
||||
private readonly bool _preview;
|
||||
private readonly RootContent _root;
|
||||
|
||||
public Source(INavigableData data, bool preview)
|
||||
{
|
||||
_data = data;
|
||||
_preview = preview;
|
||||
|
||||
var contentAtRoot = data.GetAtRoot(preview);
|
||||
_root = new RootContent(contentAtRoot.Select(x => x.Id));
|
||||
}
|
||||
|
||||
public INavigableContent Get(int id)
|
||||
{
|
||||
// wrap in a navigable content
|
||||
|
||||
var content = _data.GetById(_preview, id);
|
||||
return content == null ? null : new NavigableContent(content);
|
||||
}
|
||||
|
||||
public int LastAttributeIndex
|
||||
{
|
||||
get { return NavigableContentType.BuiltinProperties.Length - 1; }
|
||||
}
|
||||
|
||||
public INavigableContent Root
|
||||
{
|
||||
get { return _root; }
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/Umbraco.Web/PublishedCache/NuCache/Property.cs
Normal file
183
src/Umbraco.Web/PublishedCache/NuCache/Property.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Xml.Serialization;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
[Serializable]
|
||||
[XmlType(Namespace = "http://umbraco.org/webservices/")]
|
||||
class Property : PublishedPropertyBase
|
||||
{
|
||||
private readonly object _dataValue;
|
||||
private readonly Guid _contentUid;
|
||||
private readonly bool _isPreviewing;
|
||||
private readonly bool _isMember;
|
||||
|
||||
readonly object _locko = new object();
|
||||
|
||||
private ValueSet _valueSet;
|
||||
private string _valueSetCacheKey;
|
||||
private string _recurseCacheKey;
|
||||
|
||||
// initializes a published content property with no value
|
||||
public Property(PublishedPropertyType propertyType, PublishedContent content)
|
||||
: this(propertyType, content, null)
|
||||
{ }
|
||||
|
||||
// initializes a published content property with a value
|
||||
public Property(PublishedPropertyType propertyType, PublishedContent content, object valueSource)
|
||||
: base(propertyType)
|
||||
{
|
||||
_dataValue = valueSource;
|
||||
_contentUid = content.Key;
|
||||
var inner = PublishedContent.UnwrapIPublishedContent(content);
|
||||
_isPreviewing = inner.IsPreviewing;
|
||||
_isMember = content.ContentType.ItemType == PublishedItemType.Member;
|
||||
}
|
||||
|
||||
// clone for previewing as draft a published content that is published and has no draft
|
||||
public Property(Property origin)
|
||||
: base(origin.PropertyType)
|
||||
{
|
||||
_dataValue = origin._dataValue;
|
||||
_contentUid = origin._contentUid;
|
||||
_isPreviewing = true;
|
||||
_isMember = origin._isMember;
|
||||
}
|
||||
|
||||
// detached
|
||||
//internal Property(PublishedPropertyType propertyType, Guid contentUid, object valueSource, bool isPreviewing, bool isMember)
|
||||
// : base(propertyType)
|
||||
//{
|
||||
// _dataValue = valueSource;
|
||||
// _contentUid = contentUid;
|
||||
// _isPreviewing = isPreviewing;
|
||||
// _isMember = isMember;
|
||||
//}
|
||||
|
||||
public override bool HasValue
|
||||
{
|
||||
get { return _dataValue != null && ((_dataValue is string) == false || string.IsNullOrWhiteSpace((string)_dataValue) == false); }
|
||||
}
|
||||
|
||||
private class ValueSet
|
||||
{
|
||||
public bool SourceInitialized;
|
||||
public object Source;
|
||||
public bool ValueInitialized;
|
||||
public object Value;
|
||||
public bool XPathInitialized;
|
||||
public object XPath;
|
||||
}
|
||||
|
||||
internal string RecurseCacheKey
|
||||
{
|
||||
get { return _recurseCacheKey ?? (_recurseCacheKey = CacheKeys.PropertyRecurse(_contentUid, PropertyTypeAlias, _isPreviewing)); }
|
||||
}
|
||||
|
||||
internal string ValueSetCacheKey
|
||||
{
|
||||
get { return _valueSetCacheKey ?? (_valueSetCacheKey = CacheKeys.PropertyValueSet(_contentUid, PropertyTypeAlias, _isPreviewing)); }
|
||||
}
|
||||
|
||||
private ValueSet GetValueSet(PropertyCacheLevel cacheLevel)
|
||||
{
|
||||
ValueSet valueSet;
|
||||
Facade facade;
|
||||
ICacheProvider cache;
|
||||
switch (cacheLevel)
|
||||
{
|
||||
case PropertyCacheLevel.None:
|
||||
// never cache anything
|
||||
valueSet = new ValueSet();
|
||||
break;
|
||||
case PropertyCacheLevel.Content:
|
||||
// cache within the property object itself, ie within the content object
|
||||
valueSet = _valueSet ?? (_valueSet = new ValueSet());
|
||||
break;
|
||||
case PropertyCacheLevel.ContentCache:
|
||||
// cache within the snapshot cache, unless previewing, then use the facade or
|
||||
// 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
|
||||
facade = Facade.Current;
|
||||
cache = facade == null
|
||||
? null
|
||||
: ((_isPreviewing == false || FacadeService.FullCacheWhenPreviewing) && (_isMember == false)
|
||||
? facade.SnapshotCache
|
||||
: facade.FacadeCache);
|
||||
valueSet = GetValueSet(cache);
|
||||
break;
|
||||
case PropertyCacheLevel.Request:
|
||||
// cache within the facade cache
|
||||
facade = Facade.Current;
|
||||
cache = facade == null ? null : facade.FacadeCache;
|
||||
valueSet = GetValueSet(cache);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid cache level.");
|
||||
}
|
||||
return valueSet;
|
||||
}
|
||||
|
||||
private ValueSet GetValueSet(ICacheProvider cache)
|
||||
{
|
||||
if (cache == null) // no cache, don't cache
|
||||
return new ValueSet();
|
||||
return (ValueSet) cache.GetCacheItem(ValueSetCacheKey, () => new ValueSet());
|
||||
}
|
||||
|
||||
private object GetSourceValue()
|
||||
{
|
||||
var valueSet = GetValueSet(PropertyType.SourceCacheLevel);
|
||||
if (valueSet.SourceInitialized == false)
|
||||
{
|
||||
valueSet.Source = PropertyType.ConvertDataToSource(_dataValue, _isPreviewing);
|
||||
valueSet.SourceInitialized = true;
|
||||
}
|
||||
return valueSet.Source;
|
||||
}
|
||||
|
||||
public override object DataValue
|
||||
{
|
||||
get { return _dataValue; }
|
||||
}
|
||||
|
||||
public override object Value
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
var valueSet = GetValueSet(PropertyType.ObjectCacheLevel);
|
||||
if (valueSet.ValueInitialized == false)
|
||||
{
|
||||
valueSet.Value = PropertyType.ConvertSourceToObject(GetSourceValue(), _isPreviewing);
|
||||
valueSet.ValueInitialized = true;
|
||||
}
|
||||
return valueSet.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override object XPathValue
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_locko)
|
||||
{
|
||||
var valueSet = GetValueSet(PropertyType.XPathCacheLevel);
|
||||
if (valueSet.XPathInitialized == false)
|
||||
{
|
||||
valueSet.XPath = PropertyType.ConvertSourceToXPath(GetSourceValue(), _isPreviewing);
|
||||
valueSet.XPathInitialized = true;
|
||||
}
|
||||
return valueSet.XPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
355
src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs
Normal file
355
src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.PublishedCache.NuCache.DataSource;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
internal class PublishedContent : PublishedContentWithKeyBase
|
||||
{
|
||||
private readonly ContentNode _contentNode;
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal readonly ContentData _contentData; // internal for ContentNode cloning
|
||||
|
||||
private readonly IPublishedProperty[] _properties;
|
||||
private readonly string _urlName;
|
||||
private readonly bool _isPreviewing;
|
||||
|
||||
#region Constructors
|
||||
|
||||
public PublishedContent(ContentNode contentNode, ContentData contentData)
|
||||
{
|
||||
_contentNode = contentNode;
|
||||
_contentData = contentData;
|
||||
|
||||
_urlName = _contentData.Name.ToUrlSegment();
|
||||
_isPreviewing = _contentData.Published == false;
|
||||
_properties = CreateProperties(this, contentData.Properties);
|
||||
}
|
||||
|
||||
private static string GetProfileNameById(int id)
|
||||
{
|
||||
var facade = Facade.Current;
|
||||
var cache = facade == null ? null : facade.FacadeCache;
|
||||
return cache == null
|
||||
? GetProfileNameByIdNoCache(id)
|
||||
: (string) cache.GetCacheItem(CacheKeys.ProfileName(id), () => GetProfileNameByIdNoCache(id));
|
||||
}
|
||||
|
||||
private static string GetProfileNameByIdNoCache(int id)
|
||||
{
|
||||
#if DEBUG
|
||||
var context = ApplicationContext.Current;
|
||||
var servicesContext = context == null ? null : context.Services;
|
||||
var userService = servicesContext == null ? null : servicesContext.UserService;
|
||||
if (userService == null) return "[null]"; // for tests
|
||||
#else
|
||||
// we don't want each published content to hold a reference to the service
|
||||
// so where should they get the service from really? from the source...
|
||||
var userService = ApplicationContext.Current.Services.UserService;
|
||||
#endif
|
||||
var user = userService.GetProfileById(id);
|
||||
return user == null ? null : user.Name;
|
||||
}
|
||||
|
||||
private static IPublishedProperty[] CreateProperties(PublishedContent content, IDictionary<string, object> values)
|
||||
{
|
||||
return content._contentNode.ContentType
|
||||
.PropertyTypes
|
||||
.Select(propertyType =>
|
||||
{
|
||||
object value;
|
||||
return values.TryGetValue(propertyType.PropertyTypeAlias, out value)
|
||||
? (IPublishedProperty) new Property(propertyType, content, value)
|
||||
: (IPublishedProperty) new Property(propertyType, content);
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
// (see ContentNode.CloneParent)
|
||||
public PublishedContent(ContentNode contentNode, PublishedContent origin)
|
||||
{
|
||||
_contentNode = contentNode;
|
||||
_contentData = origin._contentData;
|
||||
|
||||
_urlName = origin._urlName;
|
||||
_isPreviewing = origin._isPreviewing;
|
||||
|
||||
// here is the main benefit: we do not re-create properties so if anything
|
||||
// is cached locally, we share the cache - which is fine - if anything depends
|
||||
// on the tree structure, it should not be cached locally to begin with
|
||||
_properties = origin._properties;
|
||||
}
|
||||
|
||||
// clone for previewing as draft a published content that is published and has no draft
|
||||
private PublishedContent(PublishedContent origin)
|
||||
{
|
||||
_contentNode = origin._contentNode;
|
||||
_contentData = origin._contentData;
|
||||
|
||||
_urlName = origin._urlName;
|
||||
_isPreviewing = true;
|
||||
|
||||
// clone properties so _isPreviewing is true
|
||||
_properties = origin._properties.Select(x => (IPublishedProperty) new Property((Property) x)).ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Content/Media for Parent/Children
|
||||
|
||||
// this is for tests purposes
|
||||
// args are: current facade (may be null), previewing, content id - returns: content
|
||||
private static Func<IFacade, bool, int, IPublishedContent> _getContentByIdFunc =
|
||||
(facade, previewing, id) => facade.ContentCache.GetById(previewing, id);
|
||||
private static Func<IFacade, bool, int, IPublishedContent> _getMediaByIdFunc =
|
||||
(facade, previewing, id) => facade.MediaCache.GetById(previewing, id);
|
||||
|
||||
internal static Func<IFacade, bool, int, IPublishedContent> GetContentByIdFunc
|
||||
{
|
||||
get { return _getContentByIdFunc; }
|
||||
set
|
||||
{
|
||||
_getContentByIdFunc = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal static Func<IFacade, bool, int, IPublishedContent> GetMediaByIdFunc
|
||||
{
|
||||
get { return _getMediaByIdFunc; }
|
||||
set
|
||||
{
|
||||
_getMediaByIdFunc = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static IPublishedContent GetContentById(bool previewing, int id)
|
||||
{
|
||||
return _getContentByIdFunc(Facade.Current, previewing, id);
|
||||
}
|
||||
|
||||
private static IEnumerable<IPublishedContent> GetContentByIds(bool previewing, IEnumerable<int> ids)
|
||||
{
|
||||
var facade = Facade.Current;
|
||||
|
||||
// beware! the loop below CANNOT be converted to query such as:
|
||||
//return ids.Select(x => _getContentByIdFunc(facade, previewing, x)).Where(x => x != null);
|
||||
// because it would capture the facade and cause all sorts of issues
|
||||
//
|
||||
// we WANT to get the actual current facade each time we run
|
||||
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var content = _getContentByIdFunc(facade, previewing, id);
|
||||
if (content != null) yield return content;
|
||||
}
|
||||
}
|
||||
|
||||
private static IPublishedContent GetMediaById(bool previewing, int id)
|
||||
{
|
||||
return _getMediaByIdFunc(Facade.Current, previewing, id);
|
||||
}
|
||||
|
||||
private static IEnumerable<IPublishedContent> GetMediaByIds(bool previewing, IEnumerable<int> ids)
|
||||
{
|
||||
var facade = Facade.Current;
|
||||
|
||||
// see note above for content
|
||||
|
||||
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var content = _getMediaByIdFunc(facade, previewing, id);
|
||||
if (content != null) yield return content;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPublishedContent
|
||||
|
||||
public override int Id { get { return _contentNode.Id; } }
|
||||
public override Guid Key { get { return _contentNode.Uid; } }
|
||||
public override int DocumentTypeId { get { return _contentNode.ContentType.Id; } }
|
||||
public override string DocumentTypeAlias { get { return _contentNode.ContentType.Alias; } }
|
||||
public override PublishedItemType ItemType { get { return _contentNode.ContentType.ItemType; } }
|
||||
|
||||
public override string Name { get { return _contentData.Name; } }
|
||||
public override int Level { get { return _contentNode.Level; } }
|
||||
public override string Path { get { return _contentNode.Path; } }
|
||||
public override int SortOrder { get { return _contentNode.SortOrder; } }
|
||||
public override Guid Version { get { return _contentData.Version; } }
|
||||
public override int TemplateId { get { return _contentData.TemplateId; } }
|
||||
|
||||
public override string UrlName { get { return _urlName; } }
|
||||
|
||||
public override DateTime CreateDate { get { return _contentNode.CreateDate; } }
|
||||
public override DateTime UpdateDate { get { return _contentData.VersionDate; } }
|
||||
|
||||
public override int CreatorId { get { return _contentNode.CreatorId; } }
|
||||
public override string CreatorName { get { return GetProfileNameById(_contentNode.CreatorId); } }
|
||||
public override int WriterId { get { return _contentData.WriterId; } }
|
||||
public override string WriterName { get { return GetProfileNameById(_contentData.WriterId); } }
|
||||
|
||||
public override bool IsDraft { get { return _contentData.Published == false; } }
|
||||
|
||||
// beware what you use that one for - you don't want to cache its result
|
||||
private ICacheProvider GetAppropriateFacadeCache()
|
||||
{
|
||||
var facade = Facade.Current;
|
||||
var cache = facade == null
|
||||
? null
|
||||
: ((_isPreviewing == false || FacadeService.FullCacheWhenPreviewing) && (ItemType != PublishedItemType.Member)
|
||||
? facade.SnapshotCache
|
||||
: facade.FacadeCache);
|
||||
return cache;
|
||||
}
|
||||
|
||||
public override IPublishedContent Parent
|
||||
{
|
||||
get
|
||||
{
|
||||
// have to use the "current" cache because a PublishedContent can be shared
|
||||
// amongst many snapshots and other content depend on the snapshots
|
||||
switch (_contentNode.ContentType.ItemType)
|
||||
{
|
||||
case PublishedItemType.Content:
|
||||
return GetContentById(_isPreviewing, _contentNode.ParentContentId);
|
||||
case PublishedItemType.Media:
|
||||
return GetMediaById(_isPreviewing, _contentNode.ParentContentId);
|
||||
default:
|
||||
throw new Exception("oops");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string _childrenCacheKey;
|
||||
|
||||
private string ChildrenCacheKey
|
||||
{
|
||||
get { return _childrenCacheKey ?? (_childrenCacheKey = CacheKeys.PublishedContentChildren(Key, _isPreviewing)); }
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
var cache = GetAppropriateFacadeCache();
|
||||
if (cache == null || FacadeService.CachePublishedContentChildren == false)
|
||||
return GetChildren();
|
||||
|
||||
// note: ToArray is important here, we want to cache the result, not the function!
|
||||
return (IEnumerable<IPublishedContent>) cache.GetCacheItem(ChildrenCacheKey, () => GetChildren().ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent> GetChildren()
|
||||
{
|
||||
IEnumerable<IPublishedContent> c;
|
||||
switch (_contentNode.ContentType.ItemType)
|
||||
{
|
||||
case PublishedItemType.Content:
|
||||
c = GetContentByIds(_isPreviewing, _contentNode.ChildContentIds);
|
||||
break;
|
||||
case PublishedItemType.Media:
|
||||
c = GetMediaByIds(_isPreviewing, _contentNode.ChildContentIds);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("oops");
|
||||
}
|
||||
|
||||
return c.OrderBy(x => x.SortOrder);
|
||||
|
||||
// notes:
|
||||
// _contentNode.ChildContentIds is an unordered int[]
|
||||
// need needs to fetch & sort - do it only once, lazyily, though
|
||||
// Q: perfs-wise, is it better than having the store managed an ordered list
|
||||
}
|
||||
|
||||
public override ICollection<IPublishedProperty> Properties { get { return _properties; } }
|
||||
|
||||
public override IPublishedProperty GetProperty(string alias)
|
||||
{
|
||||
var index = _contentNode.ContentType.GetPropertyIndex(alias);
|
||||
var property = index < 0 ? null : _properties[index];
|
||||
return property;
|
||||
}
|
||||
|
||||
public override IPublishedProperty GetProperty(string alias, bool recurse)
|
||||
{
|
||||
var property = GetProperty(alias);
|
||||
if (recurse == false) return property;
|
||||
|
||||
var cache = GetAppropriateFacadeCache();
|
||||
if (cache == null)
|
||||
return base.GetProperty(alias, true);
|
||||
|
||||
var key = ((Property) property).RecurseCacheKey;
|
||||
return (Property) cache.GetCacheItem(key, () => base.GetProperty(alias, true));
|
||||
}
|
||||
|
||||
public override PublishedContentType ContentType
|
||||
{
|
||||
get { return _contentNode.ContentType; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal
|
||||
|
||||
// used by navigable content
|
||||
internal IPublishedProperty[] PropertiesArray { get { return _properties; } }
|
||||
|
||||
// used by navigable content
|
||||
internal int ParentId { get { return _contentNode.ParentContentId; } }
|
||||
|
||||
// used by navigable content
|
||||
// includes all children, published or unpublished
|
||||
// NavigableNavigator takes care of selecting those it wants
|
||||
internal IList<int> ChildIds { get { return _contentNode.ChildContentIds; } }
|
||||
|
||||
// used by Property
|
||||
// gets a value indicating whether the content or media exists in
|
||||
// a previewing context or not, ie whether its Parent, Children, and
|
||||
// properties should refer to published, or draft content
|
||||
internal bool IsPreviewing { get { return _isPreviewing; } }
|
||||
|
||||
private string _asPreviewingCacheKey;
|
||||
|
||||
private string AsPreviewingCacheKey
|
||||
{
|
||||
get { return _asPreviewingCacheKey ?? (_asPreviewingCacheKey = CacheKeys.PublishedContentAsPreviewing(Key)); }
|
||||
}
|
||||
|
||||
// used by ContentCache
|
||||
internal IPublishedContent AsPreviewingModel()
|
||||
{
|
||||
if (_isPreviewing)
|
||||
return this;
|
||||
|
||||
var cache = GetAppropriateFacadeCache();
|
||||
if (cache == null) return new PublishedContent(this).CreateModel();
|
||||
return (IPublishedContent) cache.GetCacheItem(AsPreviewingCacheKey, () => new PublishedContent(this).CreateModel());
|
||||
}
|
||||
|
||||
// used by Navigable.Source,...
|
||||
internal static PublishedContent UnwrapIPublishedContent(IPublishedContent content)
|
||||
{
|
||||
PublishedContentWrapped wrapped;
|
||||
while ((wrapped = content as PublishedContentWrapped) != null)
|
||||
content = wrapped.Unwrap();
|
||||
var inner = content as PublishedContent;
|
||||
if (inner == null)
|
||||
throw new InvalidOperationException("Innermost content is not PublishedContent.");
|
||||
return inner;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
151
src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs
Normal file
151
src/Umbraco.Web/PublishedCache/NuCache/PublishedMember.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.ServiceModel.Security;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.PublishedCache.NuCache.DataSource;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
// note
|
||||
// the whole PublishedMember thing should be refactored because as soon as a member
|
||||
// is wrapped on in a model, the inner IMember and all associated properties are lost
|
||||
|
||||
class PublishedMember : PublishedContent //, IPublishedMember
|
||||
{
|
||||
private readonly IMember _member;
|
||||
|
||||
private PublishedMember(IMember member, ContentNode contentNode, ContentData contentData)
|
||||
: base(contentNode, contentData)
|
||||
{
|
||||
_member = member;
|
||||
}
|
||||
|
||||
public static IPublishedContent Create(IMember member, PublishedContentType contentType, bool previewing)
|
||||
{
|
||||
var d = new ContentData
|
||||
{
|
||||
Name = member.Name,
|
||||
Published = previewing,
|
||||
TemplateId = -1,
|
||||
Version = member.Version,
|
||||
VersionDate = member.UpdateDate,
|
||||
WriterId = member.CreatorId, // what else?
|
||||
Properties = GetPropertyValues(contentType, member)
|
||||
};
|
||||
var n = new ContentNode(member.Id, member.Key,
|
||||
contentType,
|
||||
member.Level, member.Path, member.SortOrder,
|
||||
member.ParentId,
|
||||
member.CreateDate, member.CreatorId);
|
||||
return new PublishedMember(member, n, d).CreateModel();
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> GetPropertyValues(PublishedContentType contentType, IMember member)
|
||||
{
|
||||
// see node in FacadeService
|
||||
// we do not (want to) support ConvertDbToXml/String
|
||||
|
||||
//var propertyEditorResolver = PropertyEditorResolver.Current;
|
||||
|
||||
var properties = member
|
||||
.Properties
|
||||
//.Select(property =>
|
||||
//{
|
||||
// var e = propertyEditorResolver.GetByAlias(property.PropertyType.PropertyEditorAlias);
|
||||
// var v = e == null
|
||||
// ? property.Value
|
||||
// : e.ValueEditor.ConvertDbToString(property, property.PropertyType, ApplicationContext.Current.Services.DataTypeService);
|
||||
// return new KeyValuePair<string, object>(property.Alias, v);
|
||||
//})
|
||||
//.ToDictionary(x => x.Key, x => x.Value);
|
||||
.ToDictionary(x => x.Alias, x => x.Value, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
AddIf(contentType, properties, "Email", member.Email);
|
||||
AddIf(contentType, properties, "Username", member.Username);
|
||||
//AddIf(contentType, properties, "PasswordQuestion", member.PasswordQuestion);
|
||||
//AddIf(contentType, properties, "Comments", member.Comments);
|
||||
//AddIf(contentType, properties, "IsApproved", member.IsApproved);
|
||||
//AddIf(contentType, properties, "IsLockedOut", member.IsLockedOut);
|
||||
//AddIf(contentType, properties, "LastLockoutDate", member.LastLockoutDate);
|
||||
//AddIf(contentType, properties, "CreateDate", member.CreateDate);
|
||||
//AddIf(contentType, properties, "LastLoginDate", member.LastLoginDate);
|
||||
//AddIf(contentType, properties, "LastPasswordChangeDate", member.LastPasswordChangeDate);
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private static void AddIf(PublishedContentType contentType, IDictionary<string, object> properties, string alias, object value)
|
||||
{
|
||||
var propertyType = contentType.GetPropertyType(alias);
|
||||
if (propertyType == null || propertyType.IsUmbraco == false) return;
|
||||
properties[alias] = value;
|
||||
}
|
||||
|
||||
#region IPublishedMember
|
||||
|
||||
public IMember Member
|
||||
{
|
||||
get { return _member; }
|
||||
}
|
||||
|
||||
public string Email
|
||||
{
|
||||
get { return _member.Email; }
|
||||
}
|
||||
|
||||
public string UserName
|
||||
{
|
||||
get { return _member.Username; }
|
||||
}
|
||||
|
||||
public string PasswordQuestion
|
||||
{
|
||||
get { return _member.PasswordQuestion; }
|
||||
}
|
||||
|
||||
public string Comments
|
||||
{
|
||||
get { return _member.Comments; }
|
||||
}
|
||||
|
||||
public bool IsApproved
|
||||
{
|
||||
get { return _member.IsApproved; }
|
||||
}
|
||||
|
||||
public bool IsLockedOut
|
||||
{
|
||||
get { return _member.IsLockedOut; }
|
||||
}
|
||||
|
||||
public DateTime LastLockoutDate
|
||||
{
|
||||
get { return _member.LastLockoutDate; }
|
||||
}
|
||||
|
||||
public DateTime CreationDate
|
||||
{
|
||||
get { return _member.CreateDate; }
|
||||
}
|
||||
|
||||
public DateTime LastLoginDate
|
||||
{
|
||||
get { return _member.LastLoginDate; }
|
||||
}
|
||||
|
||||
public DateTime LastActivityDate
|
||||
{
|
||||
get { return _member.LastLoginDate; }
|
||||
}
|
||||
|
||||
public DateTime LastPasswordChangedDate
|
||||
{
|
||||
get { return _member.LastPasswordChangeDate; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
631
src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs
Normal file
631
src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs
Normal file
@@ -0,0 +1,631 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Web.PublishedCache.NuCache
|
||||
{
|
||||
internal class SnapDictionary<TKey, TValue>
|
||||
where TValue : class
|
||||
{
|
||||
// read
|
||||
// http://www.codeproject.com/Articles/548406/Dictionary-plus-Locking-versus-ConcurrentDictionar
|
||||
// http://arbel.net/2013/02/03/best-practices-for-using-concurrentdictionary/
|
||||
// http://blogs.msdn.com/b/pfxteam/archive/2011/04/02/10149222.aspx
|
||||
|
||||
// Set, Clear and GetSnapshot have to be protected by a lock
|
||||
// This class is optimized for many readers, few writers
|
||||
// Readers are lock-free
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, LinkedNode> _items;
|
||||
private readonly ConcurrentQueue<GenerationObject> _generationObjects;
|
||||
private GenerationObject _generationObject;
|
||||
private readonly object _wlocko = new object();
|
||||
private readonly object _rlocko = new object();
|
||||
private long _liveGen, _floorGen;
|
||||
private bool _nextGen, _collectAuto;
|
||||
private Task _collectTask;
|
||||
private volatile int _wlocked;
|
||||
|
||||
// fixme - collection trigger (ok for now)
|
||||
// minGenDelta to be adjusted
|
||||
// we may want to throttle collects even if delta is reached
|
||||
// we may want to force collect if delta is not reached but very old
|
||||
// we may want to adjust delta depending on the number of changes
|
||||
private const long CollectMinGenDelta = 4;
|
||||
|
||||
#region Ctor
|
||||
|
||||
public SnapDictionary()
|
||||
{
|
||||
_items = new ConcurrentDictionary<TKey, LinkedNode>();
|
||||
_generationObjects = new ConcurrentQueue<GenerationObject>();
|
||||
_generationObject = null; // no initial gen exists
|
||||
_liveGen = _floorGen = 0;
|
||||
_nextGen = false; // first time, must create a snapshot
|
||||
_collectAuto = true; // collect automatically by default
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Locking
|
||||
|
||||
public void WriteLocked(Action action)
|
||||
{
|
||||
var wtaken = false;
|
||||
var wcount = false;
|
||||
try
|
||||
{
|
||||
Monitor.Enter(_wlocko, ref wtaken);
|
||||
|
||||
var rtaken = false;
|
||||
try
|
||||
{
|
||||
Monitor.Enter(_rlocko, ref rtaken);
|
||||
|
||||
// assume everything in finally runs atomically
|
||||
// http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex
|
||||
// http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/
|
||||
// http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/
|
||||
// http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html
|
||||
//RuntimeHelpers.PrepareConstrainedRegions();
|
||||
try
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
_wlocked++;
|
||||
wcount = true;
|
||||
if (_nextGen == false)
|
||||
{
|
||||
// because we are changing things, a new generation
|
||||
// is created, which will trigger a new snapshot
|
||||
_nextGen = true;
|
||||
_liveGen += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rtaken) Monitor.Exit(_rlocko);
|
||||
}
|
||||
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (wcount) _wlocked--;
|
||||
if (wtaken) Monitor.Exit(_wlocko);
|
||||
}
|
||||
}
|
||||
|
||||
public T WriteLocked<T>(Func<T> func)
|
||||
{
|
||||
var wtaken = false;
|
||||
var wcount = false;
|
||||
try
|
||||
{
|
||||
Monitor.Enter(_wlocko, ref wtaken);
|
||||
|
||||
var rtaken = false;
|
||||
try
|
||||
{
|
||||
Monitor.Enter(_rlocko, ref rtaken);
|
||||
|
||||
try
|
||||
{ }
|
||||
finally
|
||||
{
|
||||
_wlocked++;
|
||||
wcount = true;
|
||||
if (_nextGen == false)
|
||||
{
|
||||
// because we are changing things, a new generation
|
||||
// is created, which will trigger a new snapshot
|
||||
_nextGen = true;
|
||||
_liveGen += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rtaken) Monitor.Exit(_rlocko);
|
||||
}
|
||||
|
||||
return func();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (wcount) _wlocked--;
|
||||
if (wtaken) Monitor.Exit(_wlocko);
|
||||
}
|
||||
}
|
||||
|
||||
private T ReadLocked<T>(Func<bool, T> func)
|
||||
{
|
||||
var rtaken = false;
|
||||
try
|
||||
{
|
||||
Monitor.Enter(_rlocko, ref rtaken);
|
||||
|
||||
// we have rlock, so it cannot ++
|
||||
// it could -- though, so... volatile
|
||||
var wlocked = _wlocked > 0;
|
||||
return func(wlocked);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rtaken) Monitor.Exit(_rlocko);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set, Clear, Get, Has
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { return _items.Count; }
|
||||
}
|
||||
|
||||
private LinkedNode GetHead(TKey key)
|
||||
{
|
||||
LinkedNode link;
|
||||
_items.TryGetValue(key, out link); // else null
|
||||
return link;
|
||||
}
|
||||
|
||||
public void Set(TKey key, TValue value)
|
||||
{
|
||||
WriteLocked(() =>
|
||||
{
|
||||
// this is safe only because we're write-locked
|
||||
var link = GetHead(key);
|
||||
if (link != null)
|
||||
{
|
||||
// already in the dict
|
||||
if (link.Gen != _liveGen)
|
||||
{
|
||||
// for an older gen - if value is different then insert a new
|
||||
// link for the new gen, with the new value
|
||||
if (link.Value != value)
|
||||
_items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link);
|
||||
}
|
||||
else
|
||||
{
|
||||
// for the live gen - we can fix the live gen - and remove it
|
||||
// if value is null and there's no next gen
|
||||
if (value == null && link.Next == null)
|
||||
_items.TryRemove(key, out link);
|
||||
else
|
||||
link.Value = value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_items.TryAdd(key, new LinkedNode(value, _liveGen));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Clear(TKey key)
|
||||
{
|
||||
Set(key, null);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
WriteLocked(() =>
|
||||
{
|
||||
// this is safe only because we're write-locked
|
||||
foreach (var kvp in _items.Where(x => x.Value != null))
|
||||
{
|
||||
if (kvp.Value.Gen < _liveGen)
|
||||
{
|
||||
var link = new LinkedNode(null, _liveGen, kvp.Value);
|
||||
_items.TryUpdate(kvp.Key, link, kvp.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
kvp.Value.Value = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public TValue Get(TKey key, long gen)
|
||||
{
|
||||
// look ma, no lock!
|
||||
var link = GetHead(key);
|
||||
while (link != null)
|
||||
{
|
||||
if (link.Gen <= gen)
|
||||
return link.Value; // may be null
|
||||
link = link.Next;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<TValue> GetAll(long gen)
|
||||
{
|
||||
// enumerating on .Values locks the concurrent dictionary,
|
||||
// so better get a shallow clone in an array and release
|
||||
var links = _items.Values.ToArray();
|
||||
return links.Select(link =>
|
||||
{
|
||||
while (link != null)
|
||||
{
|
||||
if (link.Gen <= gen)
|
||||
return link.Value; // may be null
|
||||
link = link.Next;
|
||||
}
|
||||
return null;
|
||||
}).Where(x => x != null);
|
||||
}
|
||||
|
||||
public bool IsEmpty(long gen)
|
||||
{
|
||||
var has = _items.Any(x =>
|
||||
{
|
||||
var link = x.Value;
|
||||
while (link != null)
|
||||
{
|
||||
if (link.Gen <= gen && link.Value != null)
|
||||
return true;
|
||||
link = link.Next;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return has == false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Snapshots
|
||||
|
||||
public Snapshot CreateSnapshot()
|
||||
{
|
||||
return ReadLocked(wlocked =>
|
||||
{
|
||||
// if no next generation is required, and we already have one,
|
||||
// use it and create a new snapshot
|
||||
if (_nextGen == false && _generationObject != null)
|
||||
return new Snapshot(this, _generationObject.GetReference());
|
||||
|
||||
// else we need to try to create a new gen ref
|
||||
// whether we are wlocked or not, noone can rlock while we do,
|
||||
// so _liveGen and _nextGen are safe
|
||||
if (wlocked)
|
||||
{
|
||||
// write-locked, cannot use latest gen (at least 1) so use previous
|
||||
var snapGen = _nextGen ? _liveGen - 1 : _liveGen;
|
||||
|
||||
// create a new gen ref unless we already have it
|
||||
if (_generationObject == null)
|
||||
_generationObjects.Enqueue(_generationObject = new GenerationObject(snapGen));
|
||||
else if (_generationObject.Gen != snapGen)
|
||||
throw new Exception("panic");
|
||||
}
|
||||
else
|
||||
{
|
||||
// not write-locked, can use latest gen, create a new gen ref
|
||||
_generationObjects.Enqueue(_generationObject = new GenerationObject(_liveGen));
|
||||
_nextGen = false; // this is the ONLY thing that triggers a _liveGen++
|
||||
}
|
||||
|
||||
// so...
|
||||
// the genRefRef has a weak ref to the genRef, and is queued
|
||||
// the snapshot has a ref to the genRef, which has a ref to the genRefRef
|
||||
// when the snapshot is disposed, it decreases genRefRef counter
|
||||
// so after a while, one of these conditions is going to be true:
|
||||
// - the genRefRef counter is zero because all snapshots have properly been disposed
|
||||
// - the genRefRef weak ref is dead because all snapshots have been collected
|
||||
// in both cases, we will dequeue and collect
|
||||
|
||||
var snapshot = new Snapshot(this, _generationObject.GetReference());
|
||||
|
||||
// reading _floorGen is safe if _collectTask is null
|
||||
if (_collectTask == null && _collectAuto && _liveGen - _floorGen > CollectMinGenDelta)
|
||||
CollectAsyncLocked();
|
||||
|
||||
return snapshot;
|
||||
});
|
||||
}
|
||||
|
||||
public Task CollectAsync()
|
||||
{
|
||||
lock (_rlocko)
|
||||
{
|
||||
return CollectAsyncLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private Task CollectAsyncLocked()
|
||||
{
|
||||
if (_collectTask != null)
|
||||
return _collectTask;
|
||||
|
||||
// ReSharper disable InconsistentlySynchronizedField
|
||||
var task = _collectTask = Task.Run(() => Collect());
|
||||
_collectTask.ContinueWith(_ =>
|
||||
{
|
||||
lock (_rlocko)
|
||||
{
|
||||
_collectTask = null;
|
||||
}
|
||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||
// ReSharper restore InconsistentlySynchronizedField
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private void Collect()
|
||||
{
|
||||
// see notes in CreateSnapshot
|
||||
GenerationObject generationObject;
|
||||
while (_generationObjects.TryPeek(out generationObject) && (generationObject.Count == 0 || generationObject.WeakReference.IsAlive == false))
|
||||
{
|
||||
_generationObjects.TryDequeue(out generationObject); // cannot fail since TryPeek has succeeded
|
||||
_floorGen = generationObject.Gen;
|
||||
}
|
||||
|
||||
Collect(_items);
|
||||
}
|
||||
|
||||
private void Collect(ConcurrentDictionary<TKey, LinkedNode> dict)
|
||||
{
|
||||
// it is OK to enumerate a concurrent dictionary and it does not lock
|
||||
// it - and here it's not an issue if we skip some items, they will be
|
||||
// processed next time we collect
|
||||
|
||||
long liveGen;
|
||||
lock (_rlocko) // r is good
|
||||
{
|
||||
liveGen = _liveGen;
|
||||
if (_nextGen == false)
|
||||
liveGen += 1;
|
||||
}
|
||||
|
||||
//Console.WriteLine("Collect live=" + liveGen + " floor=" + _floorGen);
|
||||
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
var link = kvp.Value;
|
||||
|
||||
//Console.WriteLine("Collect id=" + kvp.Key + " gen=" + link.Gen
|
||||
// + " nxt=" + (link.Next == null ? null : "next")
|
||||
// + " val=" + link.Value);
|
||||
|
||||
// reasons to collect the head:
|
||||
// gen must be < liveGen (we never collect live gen)
|
||||
// next == null && value == null (we have no data at all)
|
||||
// next != null && value == null BUT gen > floor (noone wants us)
|
||||
// not live means .Next and .Value are safe
|
||||
if (link.Gen < liveGen && link.Value == null
|
||||
&& (link.Next == null || link.Gen <= _floorGen))
|
||||
{
|
||||
// not live, null value, no next link = remove that one -- but only if
|
||||
// the dict has not been updated, have to do it via ICollection<> (thanks
|
||||
// Mr Toub) -- and if the dict has been updated there is nothing to collect
|
||||
var idict = dict as ICollection<KeyValuePair<TKey, LinkedNode>>;
|
||||
/*var removed =*/ idict.Remove(kvp);
|
||||
//Console.WriteLine("remove (" + (removed ? "true" : "false") + ")");
|
||||
continue;
|
||||
}
|
||||
|
||||
// in any other case we're not collecting the head, we need to go to Next
|
||||
// and if there is no Next, skip
|
||||
if (link.Next == null)
|
||||
continue;
|
||||
|
||||
// else go to Next and loop while above floor, and kill everything below
|
||||
while (link.Next != null && link.Next.Gen > _floorGen)
|
||||
link = link.Next;
|
||||
link.Next = null;
|
||||
}
|
||||
}
|
||||
|
||||
public /*async*/ Task PendingCollect()
|
||||
{
|
||||
Task task;
|
||||
lock (_rlocko)
|
||||
{
|
||||
task = _collectTask;
|
||||
}
|
||||
return task ?? Task.FromResult(0);
|
||||
//if (task != null)
|
||||
// await task;
|
||||
}
|
||||
|
||||
public long GenCount
|
||||
{
|
||||
get { return _generationObjects.Count; }
|
||||
}
|
||||
|
||||
public long SnapCount
|
||||
{
|
||||
get
|
||||
{
|
||||
return _generationObjects.Sum(x => x.Count);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unit testing
|
||||
|
||||
private TestHelper _unitTesting;
|
||||
|
||||
// note: nothing here is thread-safe
|
||||
internal class TestHelper
|
||||
{
|
||||
private readonly SnapDictionary<TKey, TValue> _dict;
|
||||
|
||||
public TestHelper(SnapDictionary<TKey, TValue> dict)
|
||||
{
|
||||
_dict = dict;
|
||||
}
|
||||
|
||||
public long LiveGen { get { return _dict._liveGen; } }
|
||||
public long FloorGen { get { return _dict._floorGen; } }
|
||||
public bool NextGen { get { return _dict._nextGen; } }
|
||||
public bool CollectAuto { get { return _dict._collectAuto; } set { _dict._collectAuto = value; } }
|
||||
|
||||
public ConcurrentQueue<GenerationObject> GenerationObjects { get { return _dict._generationObjects; } }
|
||||
|
||||
public GenVal[] GetValues(TKey key)
|
||||
{
|
||||
LinkedNode link;
|
||||
_dict._items.TryGetValue(key, out link); // else null
|
||||
|
||||
if (link == null)
|
||||
return new GenVal[0];
|
||||
|
||||
var genVals = new List<GenVal>();
|
||||
do
|
||||
{
|
||||
genVals.Add(new GenVal(link.Gen, link.Value));
|
||||
link = link.Next;
|
||||
} while (link != null);
|
||||
return genVals.ToArray();
|
||||
}
|
||||
|
||||
public class GenVal
|
||||
{
|
||||
public GenVal(long gen, TValue value)
|
||||
{
|
||||
Gen = gen;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public long Gen { get; private set; }
|
||||
public TValue Value { get; private set; }
|
||||
}
|
||||
}
|
||||
|
||||
internal TestHelper Test { get { return _unitTesting ?? (_unitTesting = new TestHelper(this)); } }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Classes
|
||||
|
||||
private class LinkedNode
|
||||
{
|
||||
public LinkedNode(TValue value, long gen, LinkedNode next = null)
|
||||
{
|
||||
Value = value;
|
||||
Gen = gen;
|
||||
Next = next;
|
||||
}
|
||||
|
||||
internal readonly long Gen;
|
||||
|
||||
// reading & writing references is thread-safe on all .NET platforms
|
||||
// mark as volatile to ensure we always read the correct value
|
||||
internal volatile TValue Value;
|
||||
internal volatile LinkedNode Next;
|
||||
}
|
||||
|
||||
public class Snapshot : IDisposable
|
||||
{
|
||||
private readonly SnapDictionary<TKey, TValue> _store;
|
||||
private readonly GenerationReference _generationReference;
|
||||
private long _gen; // copied for perfs
|
||||
|
||||
internal Snapshot(SnapDictionary<TKey, TValue> store, GenerationReference generationReference)
|
||||
{
|
||||
_store = store;
|
||||
_generationReference = generationReference;
|
||||
_gen = generationReference.GenerationObject.Gen;
|
||||
_generationReference.GenerationObject.Reference();
|
||||
}
|
||||
|
||||
public TValue Get(TKey key)
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
return _store.Get(key, _gen);
|
||||
}
|
||||
|
||||
public IEnumerable<TValue> GetAll()
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
return _store.GetAll(_gen);
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
return _store.IsEmpty(_gen);
|
||||
}
|
||||
}
|
||||
|
||||
public long Gen
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_gen < 0)
|
||||
throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/);
|
||||
return _gen;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_gen < 0) return;
|
||||
_gen = -1;
|
||||
_generationReference.GenerationObject.Release();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal class GenerationObject
|
||||
{
|
||||
public GenerationObject(long gen)
|
||||
{
|
||||
Gen = gen;
|
||||
WeakReference = new WeakReference(null);
|
||||
}
|
||||
|
||||
public GenerationReference GetReference()
|
||||
{
|
||||
// not thread-safe but always invoked from within a lock
|
||||
var generationReference = (GenerationReference) WeakReference.Target;
|
||||
if (generationReference == null)
|
||||
WeakReference.Target = generationReference = new GenerationReference(this);
|
||||
return generationReference;
|
||||
}
|
||||
|
||||
public readonly long Gen;
|
||||
public readonly WeakReference WeakReference;
|
||||
public int Count;
|
||||
|
||||
public void Reference()
|
||||
{
|
||||
Interlocked.Increment(ref Count);
|
||||
}
|
||||
|
||||
public void Release()
|
||||
{
|
||||
Interlocked.Decrement(ref Count);
|
||||
}
|
||||
}
|
||||
|
||||
internal class GenerationReference
|
||||
{
|
||||
public GenerationReference(GenerationObject generationObject)
|
||||
{
|
||||
GenerationObject = generationObject;
|
||||
}
|
||||
|
||||
public readonly GenerationObject GenerationObject;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
120
src/Umbraco.Web/PublishedCache/NuCache/notes.txt
Normal file
120
src/Umbraco.Web/PublishedCache/NuCache/notes.txt
Normal file
@@ -0,0 +1,120 @@
|
||||
NuCache Documentation
|
||||
======================
|
||||
|
||||
HOW IT WORKS
|
||||
-------------
|
||||
|
||||
NuCache uses a ContentStore to keep content - basically, a dictionary of int => content,
|
||||
and some logic to maintain it up-to-date. In order to provide immutable content to
|
||||
pages rendering, a ContentStore can create ContentViews. A ContentView basically is
|
||||
another int => content dictionary, containing entries only for things that have changed
|
||||
in the ContentStore - so the ContentStore changes, but it updates the views so that
|
||||
they
|
||||
|
||||
Views are chained, ie each new view is the parent of previously existing views. A view
|
||||
knows its parent but not the other way round, so views just disappear when they are GC.
|
||||
|
||||
When reading the cache, we read views up the chain until we find a value (which may be
|
||||
null) for the given id, and finally we read the store itself.
|
||||
|
||||
|
||||
The FacadeService manages a ContentStore for content, and another for media.
|
||||
When a Facade is created, the FacadeService gets ContentView objects from the stores.
|
||||
Views provide an immutable snapshot of the content and media.
|
||||
|
||||
When the FacadeService is notified of changes, it notifies the stores.
|
||||
Then it resync the current Facade, so that it requires new views, etc.
|
||||
|
||||
Whenever a content, media or member is modified or removed, the cmsContentNu table
|
||||
is updated with a json dictionary of alias => property value, so that a content,
|
||||
media or member can be loaded with one database row - this is what is used to populate
|
||||
the in-memory cache.
|
||||
|
||||
|
||||
A ContentStore actually stores ContentNode instances, which contain what's common to
|
||||
both the published and draft version of content + the actual published and/or draft
|
||||
content.
|
||||
|
||||
|
||||
LOCKS
|
||||
------
|
||||
|
||||
Each ContentStore is protected by a reader/writer lock 'Locker' that is used both by
|
||||
the store and its views to ensure that the store remains consistent.
|
||||
|
||||
Each ContentStore has a _freezeLock object used to protect the 'Frozen'
|
||||
state of the store. It's a disposable object that releases the lock when disposed,
|
||||
so usage would be: using (store.Frozen) { ... }.
|
||||
|
||||
The FacadeService has a _storesLock object used to guarantee atomic access to the
|
||||
set of content, media stores.
|
||||
|
||||
|
||||
CACHE
|
||||
------
|
||||
|
||||
For each set of views, the FacadeService creates a SnapshotCache. So a SnapshotCache
|
||||
is valid until anything changes in the content or media trees. In other words, things
|
||||
that go in the SnapshotCache stay until a content or media is modified.
|
||||
|
||||
For each Facade, the FacadeService creates a FacadeCache. So a FacadeCache is valid
|
||||
for the duration of the Facade (usually, the request). In other words, things that go
|
||||
in the FacadeCache stay (and are visible to) for the duration of the request only.
|
||||
|
||||
The FacadeService defines a static constant FullCacheWhenPreviewing, that defines
|
||||
how caches operate when previewing:
|
||||
- when true, the caches in preview mode work normally.
|
||||
- when false, everything that would go to the SnapshotCache goes to the FacadeCache.
|
||||
At the moment it is true in the code, which means that eg converted values for
|
||||
previewed content will go in the SnapshotCache. Makes for faster preview, but uses
|
||||
more memory on the long term... would need some benchmarking to figure out what is
|
||||
best.
|
||||
|
||||
Members only live for the duration of the Facade. So, for members SnapshotCache is
|
||||
never used, and everything goes to the FacadeCache.
|
||||
|
||||
All cache keys are computed in the CacheKeys static class.
|
||||
|
||||
|
||||
TESTS
|
||||
-----
|
||||
|
||||
For testing purposes the following mechanisms exist:
|
||||
|
||||
The Facade type has a static Current property that is used to obtain the 'current'
|
||||
facade in many places, going through the PublishedCachesServiceResolver to get the
|
||||
current service, and asking the current service for the current facade, which by
|
||||
default relies on UmbracoContext. For test purposes, it is possible to override the
|
||||
entire mechanism by defining Facade.GetCurrentFacadeFunc which should return a facade.
|
||||
|
||||
A PublishedContent keeps only id-references to its parent and children, and needs a
|
||||
way to retrieve the actual objects from the cache - which depends on the current
|
||||
facade. It is possible to override the entire mechanism by defining PublishedContent.
|
||||
GetContentByIdFunc or .GetMediaByIdFunc.
|
||||
|
||||
Setting these functions must be done before Resolution is frozen.
|
||||
|
||||
|
||||
STATUS
|
||||
------
|
||||
|
||||
"Detached" contents & properties, which need to be refactored anyway, are not supported
|
||||
by NuCache - throwing NotImplemented in ContentCache.
|
||||
|
||||
All the cached elements rely on guids for the cache key, and not ints, so it could be
|
||||
possible to support detached contents & properties, even those that do not have an actual
|
||||
int id, but again this should be refactored entirely anyway.
|
||||
|
||||
Not doing any row-version checks (see XmlStore) when reloading from database, though it
|
||||
is maintained in the database. Two FIXME in FacadeService. Should we do it?
|
||||
|
||||
There is no on-disk cache at all so everything is reloaded from the cmsContentNu table
|
||||
when the site restarts. This is pretty fast, but we should experiment with solutions to
|
||||
store things locally (and deal with the sync issues, see XmlStore...).
|
||||
|
||||
Doing our best with PublishedMember but the whole thing should be refactored, because
|
||||
PublishedMember exposes properties that IPublishedContent does not, and that are going
|
||||
to be lost soon as the member is wrapped (content set, model...) - so we probably need
|
||||
some sort of IPublishedMember.
|
||||
|
||||
/
|
||||
@@ -17,7 +17,6 @@ using Umbraco.Core.Models.Rdbms;
|
||||
using Umbraco.Core.ObjectResolution;
|
||||
using Umbraco.Core.Persistence;
|
||||
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
|
||||
using Umbraco.Core.Persistence.Querying;
|
||||
using Umbraco.Core.Persistence.Repositories;
|
||||
using Umbraco.Core.Persistence.UnitOfWork;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
@@ -213,6 +213,31 @@
|
||||
<Compile Include="PublishedCache\IFacade.cs" />
|
||||
<Compile Include="PublishedCache\IFacadeService.cs" />
|
||||
<Compile Include="PublishedCache\IPublishedMemberCache.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\CacheKeys.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\ContentCache.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\ContentNode.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\ContentNodeKit.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\ContentStore2.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\DataSource\BTree.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\DataSource\ContentData.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\DataSource\ContentNuDto.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\DataSource\ContentSourceDto.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\DataSource\Database.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\DomainCache.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Facade.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\FacadeService.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\MediaCache.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\MemberCache.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Navigable\INavigableData.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Navigable\NavigableContent.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Navigable\NavigableContentType.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Navigable\NavigablePropertyType.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Navigable\RootContent.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Navigable\Source.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\Property.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\PublishedContent.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\PublishedMember.cs" />
|
||||
<Compile Include="PublishedCache\NuCache\SnapDictionary.cs" />
|
||||
<Compile Include="PublishedCache\PublishedCacheBase.cs" />
|
||||
<Compile Include="PublishedCache\PublishedContentTypeCache.cs" />
|
||||
<Compile Include="PublishedCache\XmlPublishedCache\DomainCache.cs" />
|
||||
@@ -236,6 +261,8 @@
|
||||
<Compile Include="Trees\UserTypesTreeController.cs" />
|
||||
<Compile Include="Trees\XsltTreeController.cs" />
|
||||
<Compile Include="Trees\UsersTreeController.cs" />
|
||||
<Compile Include="WebServices\FacadeStatusController.cs" />
|
||||
<Compile Include="WebServices\NuCacheStatusController.cs" />
|
||||
<Compile Include="_Legacy\Actions\Action.cs" />
|
||||
<Compile Include="_Legacy\Actions\ActionAssignDomain.cs" />
|
||||
<Compile Include="_Legacy\Actions\ActionAudit.cs" />
|
||||
@@ -1527,6 +1554,7 @@
|
||||
<EmbeddedResource Include="UI\JavaScript\Main.js" />
|
||||
<EmbeddedResource Include="UI\JavaScript\JsInitialize.js" />
|
||||
<EmbeddedResource Include="UI\JavaScript\ServerVariables.js" />
|
||||
<Content Include="PublishedCache\NuCache\notes.txt" />
|
||||
<Content Include="umbraco.presentation\umbraco\controls\Tree\CustomTreeService.asmx" />
|
||||
<Content Include="umbraco.presentation\umbraco\developer\RelationTypes\EditRelationType.aspx">
|
||||
<SubType>ASPXCodeBehind</SubType>
|
||||
|
||||
@@ -335,11 +335,17 @@ namespace Umbraco.Web
|
||||
container.RegisterSingleton<IUmbracoContextAccessor, DefaultUmbracoContextAccessor>();
|
||||
|
||||
// register the facade service
|
||||
container.RegisterSingleton<IFacadeService>(factory => new FacadeService(
|
||||
//container.RegisterSingleton<IFacadeService>(factory => new FacadeService(
|
||||
// factory.GetInstance<ServiceContext>(),
|
||||
// factory.GetInstance<IDatabaseUnitOfWorkProvider>(),
|
||||
// factory.GetInstance<CacheHelper>().RequestCache,
|
||||
// factory.GetAllInstances<IUrlSegmentProvider>()));
|
||||
container.RegisterSingleton<IFacadeService>(factory => new PublishedCache.NuCache.FacadeService(
|
||||
new PublishedCache.NuCache.FacadeService.Options { FacadeCacheIsApplicationRequestCache = true },
|
||||
factory.GetInstance<ApplicationContext>().MainDom,
|
||||
factory.GetInstance<ServiceContext>(),
|
||||
factory.GetInstance<IDatabaseUnitOfWorkProvider>(),
|
||||
factory.GetInstance<CacheHelper>().RequestCache,
|
||||
factory.GetAllInstances<IUrlSegmentProvider>()));
|
||||
factory.GetInstance<ILogger>()));
|
||||
|
||||
//no need to declare as per request, currently we manage it's lifetime as the singleton
|
||||
container.Register(factory => UmbracoContext.Current);
|
||||
|
||||
23
src/Umbraco.Web/WebServices/FacadeStatusController.cs
Normal file
23
src/Umbraco.Web/WebServices/FacadeStatusController.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Web.Http;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
using Umbraco.Web.WebApi;
|
||||
|
||||
namespace Umbraco.Web.WebServices
|
||||
{
|
||||
public class FacadeStatusController : UmbracoAuthorizedApiController
|
||||
{
|
||||
[HttpGet]
|
||||
public string GetFacadeStatusUrl()
|
||||
{
|
||||
var service = FacadeServiceResolver.Current.Service;
|
||||
if (service is Umbraco.Web.PublishedCache.XmlPublishedCache.FacadeService)
|
||||
return "views/dashboard/developer/xmldataintegrityreport.html";
|
||||
//if (service is PublishedCache.PublishedNoCache.FacadeService)
|
||||
// return "views/dashboard/developer/nocache.html";
|
||||
if (service is PublishedCache.NuCache.FacadeService)
|
||||
return "views/dashboard/developer/nucache.html";
|
||||
throw new NotSupportedException("Not supported: " + service.GetType().FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Umbraco.Web/WebServices/NuCacheStatusController.cs
Normal file
56
src/Umbraco.Web/WebServices/NuCacheStatusController.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Web.Http;
|
||||
using Umbraco.Web.Cache;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
using Umbraco.Web.PublishedCache.NuCache;
|
||||
using Umbraco.Web.WebApi;
|
||||
|
||||
namespace Umbraco.Web.WebServices
|
||||
{
|
||||
public class NuCacheStatusController : UmbracoAuthorizedApiController
|
||||
{
|
||||
private static FacadeService FacadeService
|
||||
{
|
||||
get
|
||||
{
|
||||
var svc = FacadeServiceResolver.Current.Service as FacadeService;
|
||||
if (svc == null)
|
||||
throw new NotSupportedException("Not running NuCache.");
|
||||
return svc;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public string RebuildDbCache()
|
||||
{
|
||||
// fixme - should wrap in a service scope once we have them
|
||||
var service = FacadeService;
|
||||
service.RebuildContentDbCache();
|
||||
service.RebuildMediaDbCache();
|
||||
service.RebuildMemberDbCache();
|
||||
return service.GetStatus();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string GetStatus()
|
||||
{
|
||||
var service = FacadeService;
|
||||
return service.GetStatus();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public string Collect()
|
||||
{
|
||||
var service = FacadeService;
|
||||
GC.Collect();
|
||||
service.Collect();
|
||||
return service.GetStatus();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public void ReloadCache()
|
||||
{
|
||||
DistributedCache.Instance.RefreshAllFacade();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,8 @@
|
||||
"semver": "1.*",
|
||||
"SharpZipLib": "0.86.0",
|
||||
"UrlRewritingNet.UrlRewriter": "2.0.7",
|
||||
"xmlrpcnet": "2.5.0"
|
||||
"xmlrpcnet": "2.5.0",
|
||||
"CSharpTest.Net.Collections": "14.906.1403.1082"
|
||||
},
|
||||
"frameworks": {
|
||||
"net461": {}
|
||||
|
||||
Reference in New Issue
Block a user