From 434703f4ec66aa9605299ba488e17ece576a3fd7 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 19 Jan 2017 19:47:09 +0100 Subject: [PATCH] U4-9322 - begin implementing scoped repository caches --- .../Cache/DefaultRepositoryCachePolicy.cs | 4 +- .../ScopedDefaultRepositoryCachePolicy.cs | 83 ++++++++++++++ .../Repositories/RepositoryBase.cs | 54 +++++++-- .../UnitOfWork/PetaPocoUnitOfWork.cs | 4 +- src/Umbraco.Core/Scoping/IScope.cs | 9 ++ src/Umbraco.Core/Scoping/IScopeProvider.cs | 4 +- src/Umbraco.Core/Scoping/NoScope.cs | 10 ++ .../Scoping/RepositoryCacheMode.cs | 16 +++ src/Umbraco.Core/Scoping/Scope.cs | 93 ++++++++++++--- src/Umbraco.Core/Scoping/ScopeProvider.cs | 12 +- src/Umbraco.Core/Umbraco.Core.csproj | 2 + src/Umbraco.Tests/Scoping/ScopeTests.cs | 20 ++++ .../Scoping/ScopedRepositoryTests.cs | 107 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 14 files changed, 384 insertions(+), 35 deletions(-) create mode 100644 src/Umbraco.Core/Cache/ScopedDefaultRepositoryCachePolicy.cs create mode 100644 src/Umbraco.Core/Scoping/RepositoryCacheMode.cs create mode 100644 src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs index 656d532d8a..5d08e36f6a 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -238,7 +238,9 @@ namespace Umbraco.Core.Cache /// public override void ClearAll() { - Cache.ClearCacheByKeySearch(GetEntityTypeCacheKey()); + // fixme the cache should NOT contain anything else so we can clean all, can't we? + Cache.ClearAllCache(); + //Cache.ClearCacheByKeySearch(GetEntityTypeCacheKey()); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/ScopedDefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/ScopedDefaultRepositoryCachePolicy.cs new file mode 100644 index 0000000000..3c237f307c --- /dev/null +++ b/src/Umbraco.Core/Cache/ScopedDefaultRepositoryCachePolicy.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Scoping; + +namespace Umbraco.Core.Cache +{ + internal class ScopedDefaultRepositoryCachePolicy : IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private readonly DefaultRepositoryCachePolicy _cachePolicy; + private readonly IRuntimeCacheProvider _globalIsolatedCache; + private readonly IScope _scope; + + public ScopedDefaultRepositoryCachePolicy(DefaultRepositoryCachePolicy cachePolicy, IRuntimeCacheProvider globalIsolatedCache, IScope scope) + { + _cachePolicy = cachePolicy; + _globalIsolatedCache = globalIsolatedCache; + _scope = scope; + } + + // when the scope completes we need to clear the global isolated cache + // for now, we are not doing it selectively at all - just kill everything + private void RegisterDirty(TEntity entity = null) + { + // "name" would be used to de-duplicate? + // fixme - casting! + ((Scope) _scope).Register("name", completed => _globalIsolatedCache.ClearAllCache()); + } + + public TEntity Get(TId id, Func performGet, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.Get(id, performGet, performGetAll); + } + + public TEntity GetCached(TId id) + { + // loads into the local cache only, ok for now + return _cachePolicy.GetCached(id); + } + + public bool Exists(TId id, Func performExists, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.Exists(id, performExists, performGetAll); + } + + public void Create(TEntity entity, Action persistNew) + { + // writes into the local cache + _cachePolicy.Create(entity, persistNew); + RegisterDirty(entity); + } + + public void Update(TEntity entity, Action persistUpdated) + { + // writes into the local cache + _cachePolicy.Update(entity, persistUpdated); + RegisterDirty(entity); + } + + public void Delete(TEntity entity, Action persistDeleted) + { + // deletes the local cache + _cachePolicy.Delete(entity, persistDeleted); + RegisterDirty(entity); + } + + public TEntity[] GetAll(TId[] ids, Func> performGetAll) + { + // loads into the local cache only, ok for now + return _cachePolicy.GetAll(ids, performGetAll); + } + + public void ClearAll() + { + // fixme - what's this doing? + _cachePolicy.ClearAll(); + RegisterDirty(); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 201520cbe3..09b4d7609d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Scoping; namespace Umbraco.Core.Persistence.Repositories { @@ -84,7 +85,7 @@ namespace Umbraco.Core.Persistence.Repositories #region Static Queries - private IQuery _hasIdQuery; + private static IQuery _hasIdQuery; #endregion @@ -101,23 +102,56 @@ namespace Umbraco.Core.Persistence.Repositories get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache(); } } - private IRepositoryCachePolicy _cachePolicy; + private static RepositoryCachePolicyOptions _defaultOptions; + protected virtual RepositoryCachePolicyOptions DefaultOptions + { + get + { + return _defaultOptions ?? (_defaultOptions + = new RepositoryCachePolicyOptions(() => + { + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + var query = _hasIdQuery ?? (_hasIdQuery = Query.Builder.Where(x => x.Id != 0)); + return PerformCount(query); + })); + } + } + // this would be better for perfs BUT it breaks the tests - l8tr + // + //private static IRepositoryCachePolicy _defaultCachePolicy; + //protected virtual IRepositoryCachePolicy DefaultCachePolicy + //{ + // get + // { + // return _defaultCachePolicy ?? (_defaultCachePolicy + // = new DefaultRepositoryCachePolicy(RuntimeCache, DefaultOptions)); + // } + //} + + private IRepositoryCachePolicy _cachePolicy; protected virtual IRepositoryCachePolicy CachePolicy { get { if (_cachePolicy != null) return _cachePolicy; - var options = new RepositoryCachePolicyOptions(() => + var scope = ((PetaPocoUnitOfWork) UnitOfWork).Scope; + switch (scope.RepositoryCacheMode) { - //Get count of all entities of current type (TEntity) to ensure cached result is correct - //create query once if it is needed (no need for locking here) - var query = _hasIdQuery ?? (_hasIdQuery = Query.Builder.Where(x => x.Id != 0)); - return PerformCount(query); - }); - - _cachePolicy = new DefaultRepositoryCachePolicy(RuntimeCache, options); + case RepositoryCacheMode.Default: + //_cachePolicy = DefaultCachePolicy; + _cachePolicy = new DefaultRepositoryCachePolicy(RuntimeCache, DefaultOptions); + break; + case RepositoryCacheMode.Scoped: + var scopedCache = scope.IsolatedRuntimeCache.GetOrCreateCache(); + var scopedPolicy = new DefaultRepositoryCachePolicy(scopedCache, DefaultOptions); + _cachePolicy = new ScopedDefaultRepositoryCachePolicy(scopedPolicy, RuntimeCache, scope); + break; + default: + throw new Exception(); + } return _cachePolicy; } diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs index 9caf787ddc..b9564e92cb 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs @@ -144,14 +144,14 @@ namespace Umbraco.Core.Persistence.UnitOfWork get { return _key; } } - private IScope ThisScope + public IScope Scope { get { return _scope ?? (_scope = _scopeProvider.CreateScope(_isolationLevel)); } } public UmbracoDatabase Database { - get { return ThisScope.Database; } + get { return Scope.Database; } } #region Operation diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs index 4960350575..e9af72b55e 100644 --- a/src/Umbraco.Core/Scoping/IScope.cs +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Persistence; @@ -20,6 +21,14 @@ namespace Umbraco.Core.Scoping /// IList Messages { get; } + /// + /// Gets the repository cache mode. + /// + RepositoryCacheMode RepositoryCacheMode { get; } + + // fixme + IsolatedRuntimeCache IsolatedRuntimeCache { get; } + /// /// Completes the scope. /// diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 9a3e935a78..9f3f18bf46 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Scoping /// If an ambient scope already exists, it becomes the parent of the created scope. /// When the created scope is disposed, the parent scope becomes the ambient scope again. /// - IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified); + IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified); /// /// Creates a detached scope. @@ -27,7 +27,7 @@ namespace Umbraco.Core.Scoping /// A detached scope is not ambient and has no parent. /// It is meant to be attached by . /// - IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified); + IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified); /// /// Attaches a scope. diff --git a/src/Umbraco.Core/Scoping/NoScope.cs b/src/Umbraco.Core/Scoping/NoScope.cs index 5c53e89047..3bcdc8ed8b 100644 --- a/src/Umbraco.Core/Scoping/NoScope.cs +++ b/src/Umbraco.Core/Scoping/NoScope.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Persistence; @@ -29,6 +30,15 @@ namespace Umbraco.Core.Scoping public Guid InstanceId { get { return _instanceId; } } #endif + /// + public RepositoryCacheMode RepositoryCacheMode + { + get { return RepositoryCacheMode.Default; } + } + + /// + public IsolatedRuntimeCache IsolatedRuntimeCache { get { throw new NotImplementedException(); } } + /// public UmbracoDatabase Database { diff --git a/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs new file mode 100644 index 0000000000..2b25f2eb59 --- /dev/null +++ b/src/Umbraco.Core/Scoping/RepositoryCacheMode.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Core.Scoping +{ + public enum RepositoryCacheMode + { + // ? + Unspecified = 0, + + // the default, full L2 cache + Default = 1, + + // a scoped cache + // reads from and writes to a local cache + // clears the global cache on completion + Scoped = 2 + } +} diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 4979aa6ae1..773d62b7d9 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Persistence; @@ -14,9 +15,11 @@ namespace Umbraco.Core.Scoping { private readonly ScopeProvider _scopeProvider; private readonly IsolationLevel _isolationLevel; + private readonly RepositoryCacheMode _repositoryCacheMode; private bool _disposed; private bool? _completed; + private IsolatedRuntimeCache _isolatedRuntimeCache; private UmbracoDatabase _database; private IList _messages; @@ -24,10 +27,11 @@ namespace Umbraco.Core.Scoping private const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; // initializes a new scope - public Scope(ScopeProvider scopeProvider, IsolationLevel isolationLevel = IsolationLevel.Unspecified, bool detachable = false) + public Scope(ScopeProvider scopeProvider, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, bool detachable = false) { _scopeProvider = scopeProvider; _isolationLevel = isolationLevel; + _repositoryCacheMode = repositoryCacheMode; Detachable = detachable; #if DEBUG_SCOPES _scopeProvider.Register(this); @@ -35,15 +39,19 @@ namespace Umbraco.Core.Scoping } // initializes a new scope in a nested scopes chain, with its parent - public Scope(ScopeProvider scopeProvider, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified) - : this(scopeProvider, isolationLevel) + public Scope(ScopeProvider scopeProvider, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified) + : this(scopeProvider, isolationLevel, repositoryCacheMode) { ParentScope = parent; + + // cannot specify a different mode! + if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode) + throw new ArgumentException("Cannot be different from parent.", "repositoryCacheMode"); } // initializes a new scope, replacing a NoScope instance - public Scope(ScopeProvider scopeProvider, NoScope noScope, IsolationLevel isolationLevel = IsolationLevel.Unspecified) - : this(scopeProvider, isolationLevel) + public Scope(ScopeProvider scopeProvider, NoScope noScope, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified) + : this(scopeProvider, isolationLevel, repositoryCacheMode) { // steal everything from NoScope _database = noScope.DatabaseOrNull; @@ -59,6 +67,29 @@ namespace Umbraco.Core.Scoping public Guid InstanceId { get { return _instanceId; } } #endif + /// + public RepositoryCacheMode RepositoryCacheMode + { + get + { + if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode; + if (ParentScope != null) return ParentScope.RepositoryCacheMode; + return RepositoryCacheMode.Default; + } + } + + /// + public IsolatedRuntimeCache IsolatedRuntimeCache + { + get + { + if (ParentScope != null) return ParentScope.IsolatedRuntimeCache; + + return _isolatedRuntimeCache ?? (_isolatedRuntimeCache + = new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + } + } + // a value indicating whether the scope is detachable // ie whether it was created by CreateDetachedScope public bool Detachable { get; private set; } @@ -208,20 +239,54 @@ namespace Umbraco.Core.Scoping // at the moment we are totally not filtering the messages based on completion // status, so whether the scope is committed or rolled back makes no difference - if (_database == null) return; + var completed = _completed.HasValue && _completed.Value; - try + if (_database != null) { - if (_completed.HasValue && _completed.Value) - _database.CompleteTransaction(); - else - _database.AbortTransaction(); + try + { + if (completed) + _database.CompleteTransaction(); + else + _database.AbortTransaction(); + } + finally + { + _database.Dispose(); + _database = null; + } } - finally + + // run everything we need to run when completing + foreach (var action in Actions.Values) + action(completed); // fixme try catch and everything + } + + // fixme - wip + private IDictionary> _actions; + + private IDictionary> Actions + { + get { - _database.Dispose(); - _database = null; + if (ParentScope != null) return ParentScope.Actions; + + return _actions ?? (_actions + = new Dictionary>()); } } + + public void Register(string name, Action action) + { + Actions[name] = completed => + { + if (completed) action(); + }; + } + + public void Register(string name, Action action) + { + Actions[name] = action; + } } } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index 7843b4f92d..03c7b6647b 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -114,9 +114,9 @@ namespace Umbraco.Core.Scoping } /// - public IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + public IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified) { - return new Scope(this, isolationLevel, true); + return new Scope(this, isolationLevel, repositoryCacheMode, true); } /// @@ -157,11 +157,11 @@ namespace Umbraco.Core.Scoping } /// - public IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified) + public IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified) { var ambient = AmbientScope; if (ambient == null) - return AmbientScope = new Scope(this, isolationLevel); + return AmbientScope = new Scope(this, isolationLevel, repositoryCacheMode); // replace noScope with a real one var noScope = ambient as NoScope; @@ -174,13 +174,13 @@ namespace Umbraco.Core.Scoping var database = noScope.DatabaseOrNull; if (database != null && database.InTransaction) throw new Exception("NoScope is in a transaction."); - return AmbientScope = new Scope(this, noScope, isolationLevel); + return AmbientScope = new Scope(this, noScope, isolationLevel, repositoryCacheMode); } var scope = ambient as Scope; if (scope == null) throw new Exception("Ambient scope is not a Scope instance."); - return AmbientScope = new Scope(this, scope, isolationLevel); + return AmbientScope = new Scope(this, scope, isolationLevel, repositoryCacheMode); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index fc778bc53b..2c229c6d73 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -156,6 +156,7 @@ + @@ -515,6 +516,7 @@ + diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index f6a9ab71df..7124d73074 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -418,5 +418,25 @@ namespace Umbraco.Tests.Scoping var db = nested.Database; }); } + + [TestCase(true)] + [TestCase(false)] + public void ScopeAction(bool complete) + { + var scopeProvider = DatabaseContext.ScopeProvider; + + bool? completed = null; + + Assert.IsNull(scopeProvider.AmbientScope); + using (var scope = scopeProvider.CreateScope()) + { + ((Scope) scope).Register("name", x => completed = x); + if (complete) + scope.Complete(); + } + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNotNull(completed); + Assert.AreEqual(complete, completed.Value); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs new file mode 100644 index 0000000000..329af6833f --- /dev/null +++ b/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs @@ -0,0 +1,107 @@ +using System; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Scoping +{ + [TestFixture] + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + public class ScopedRepositoryTests : BaseDatabaseFactoryTest + { + // setup + public override void Initialize() + { + base.Initialize(); + + Assert.IsNull(DatabaseContext.ScopeProvider.AmbientScope); // gone + } + + protected override CacheHelper CreateCacheHelper() + { + //return CacheHelper.CreateDisabledCacheHelper(); + return new CacheHelper( + new ObjectCacheRuntimeCacheProvider(), + new StaticCacheProvider(), + new NullCacheProvider(), + new IsolatedRuntimeCache(type => new ObjectCacheRuntimeCacheProvider())); + } + + [TestCase(true)] + [TestCase(false)] + public void Test(bool complete) + { + var scopeProvider = DatabaseContext.ScopeProvider; + var userService = ApplicationContext.Services.UserService; + var globalCache = ApplicationContext.ApplicationCache.IsolatedRuntimeCache.GetOrCreateCache(typeof(IUser)); + + var userType = userService.GetUserTypeByAlias("admin"); + var user = (IUser) new User("name", "email", "username", "rawPassword", userType); + userService.Save(user); + + // global cache contains the user entity + var globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey(user.Id), () => null); + Assert.IsNotNull(globalCached); + Assert.AreEqual(user.Id, globalCached.Id); + Assert.AreEqual("name", globalCached.Name); + + Assert.IsNull(scopeProvider.AmbientScope); + using (var scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) + { + Assert.IsInstanceOf(scope); + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.AreSame(scope, scopeProvider.AmbientScope); + + // scope has its own isolated cache + var scopedCache = scope.IsolatedRuntimeCache.GetOrCreateCache(typeof (IUser)); + Assert.AreNotSame(globalCache, scopedCache); + + user.Name = "changed"; + ApplicationContext.Services.UserService.Save(user); + + // scoped cache contains the "new" user entity + var scopeCached = (IUser) scopedCache.GetCacheItem(GetCacheIdKey(user.Id), () => null); + Assert.IsNotNull(scopeCached); + Assert.AreEqual(user.Id, scopeCached.Id); + Assert.AreEqual("changed", scopeCached.Name); + + // global cache is unchanged + globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey(user.Id), () => null); + Assert.IsNotNull(globalCached); + Assert.AreEqual(user.Id, globalCached.Id); + Assert.AreEqual("name", globalCached.Name); + + if (complete) + scope.Complete(); + } + Assert.IsNull(scopeProvider.AmbientScope); + + // global cache has been cleared + globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey(user.Id), () => null); + Assert.IsNull(globalCached); + + // get again, updated if completed + user = userService.GetUserById(user.Id); + Assert.AreEqual(complete ? "changed" : "name", user.Name); + + // global cache contains the entity again + globalCached = (IUser) globalCache.GetCacheItem(GetCacheIdKey(user.Id), () => null); + Assert.IsNotNull(globalCached); + Assert.AreEqual(user.Id, globalCached.Id); + Assert.AreEqual(complete ? "changed" : "name", globalCached.Name); + } + + public static string GetCacheIdKey(object id) + { + return string.Format("{0}{1}", GetCacheTypeKey(), id); + } + + public static string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(T).Name); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index bdfcac3420..696964d0a9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -170,6 +170,7 @@ +