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 @@
+