U4-9322 - begin implementing scoped repository caches
This commit is contained in:
@@ -238,7 +238,9 @@ namespace Umbraco.Core.Cache
|
||||
/// <inheritdoc />
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/Umbraco.Core/Cache/ScopedDefaultRepositoryCachePolicy.cs
Normal file
83
src/Umbraco.Core/Cache/ScopedDefaultRepositoryCachePolicy.cs
Normal file
@@ -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<TEntity, TId> : IRepositoryCachePolicy<TEntity, TId>
|
||||
where TEntity : class, IAggregateRoot
|
||||
{
|
||||
private readonly DefaultRepositoryCachePolicy<TEntity, TId> _cachePolicy;
|
||||
private readonly IRuntimeCacheProvider _globalIsolatedCache;
|
||||
private readonly IScope _scope;
|
||||
|
||||
public ScopedDefaultRepositoryCachePolicy(DefaultRepositoryCachePolicy<TEntity, TId> 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<TId, TEntity> performGet, Func<TId[], IEnumerable<TEntity>> 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<TId, bool> performExists, Func<TId[], IEnumerable<TEntity>> performGetAll)
|
||||
{
|
||||
// loads into the local cache only, ok for now
|
||||
return _cachePolicy.Exists(id, performExists, performGetAll);
|
||||
}
|
||||
|
||||
public void Create(TEntity entity, Action<TEntity> persistNew)
|
||||
{
|
||||
// writes into the local cache
|
||||
_cachePolicy.Create(entity, persistNew);
|
||||
RegisterDirty(entity);
|
||||
}
|
||||
|
||||
public void Update(TEntity entity, Action<TEntity> persistUpdated)
|
||||
{
|
||||
// writes into the local cache
|
||||
_cachePolicy.Update(entity, persistUpdated);
|
||||
RegisterDirty(entity);
|
||||
}
|
||||
|
||||
public void Delete(TEntity entity, Action<TEntity> persistDeleted)
|
||||
{
|
||||
// deletes the local cache
|
||||
_cachePolicy.Delete(entity, persistDeleted);
|
||||
RegisterDirty(entity);
|
||||
}
|
||||
|
||||
public TEntity[] GetAll(TId[] ids, Func<TId[], IEnumerable<TEntity>> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<TEntity> _hasIdQuery;
|
||||
private static IQuery<TEntity> _hasIdQuery;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -101,23 +102,56 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache<TEntity>(); }
|
||||
}
|
||||
|
||||
private IRepositoryCachePolicy<TEntity, TId> _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<TEntity>.Builder.Where(x => x.Id != 0));
|
||||
return PerformCount(query);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// this would be better for perfs BUT it breaks the tests - l8tr
|
||||
//
|
||||
//private static IRepositoryCachePolicy<TEntity, TId> _defaultCachePolicy;
|
||||
//protected virtual IRepositoryCachePolicy<TEntity, TId> DefaultCachePolicy
|
||||
//{
|
||||
// get
|
||||
// {
|
||||
// return _defaultCachePolicy ?? (_defaultCachePolicy
|
||||
// = new DefaultRepositoryCachePolicy<TEntity, TId>(RuntimeCache, DefaultOptions));
|
||||
// }
|
||||
//}
|
||||
|
||||
private IRepositoryCachePolicy<TEntity, TId> _cachePolicy;
|
||||
protected virtual IRepositoryCachePolicy<TEntity, TId> 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<TEntity>.Builder.Where(x => x.Id != 0));
|
||||
return PerformCount(query);
|
||||
});
|
||||
|
||||
_cachePolicy = new DefaultRepositoryCachePolicy<TEntity, TId>(RuntimeCache, options);
|
||||
case RepositoryCacheMode.Default:
|
||||
//_cachePolicy = DefaultCachePolicy;
|
||||
_cachePolicy = new DefaultRepositoryCachePolicy<TEntity, TId>(RuntimeCache, DefaultOptions);
|
||||
break;
|
||||
case RepositoryCacheMode.Scoped:
|
||||
var scopedCache = scope.IsolatedRuntimeCache.GetOrCreateCache<TEntity>();
|
||||
var scopedPolicy = new DefaultRepositoryCachePolicy<TEntity, TId>(scopedCache, DefaultOptions);
|
||||
_cachePolicy = new ScopedDefaultRepositoryCachePolicy<TEntity, TId>(scopedPolicy, RuntimeCache, scope);
|
||||
break;
|
||||
default:
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
return _cachePolicy;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
IList<EventMessage> Messages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository cache mode.
|
||||
/// </summary>
|
||||
RepositoryCacheMode RepositoryCacheMode { get; }
|
||||
|
||||
// fixme
|
||||
IsolatedRuntimeCache IsolatedRuntimeCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Completes the scope.
|
||||
/// </summary>
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Umbraco.Core.Scoping
|
||||
/// <para>If an ambient scope already exists, it becomes the parent of the created scope.</para>
|
||||
/// <para>When the created scope is disposed, the parent scope becomes the ambient scope again.</para>
|
||||
/// </remarks>
|
||||
IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
|
||||
IScope CreateScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a detached scope.
|
||||
@@ -27,7 +27,7 @@ namespace Umbraco.Core.Scoping
|
||||
/// <para>A detached scope is not ambient and has no parent.</para>
|
||||
/// <para>It is meant to be attached by <see cref="AttachScope"/>.</para>
|
||||
/// </remarks>
|
||||
IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified);
|
||||
IScope CreateDetachedScope(IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified);
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a scope.
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc />
|
||||
public RepositoryCacheMode RepositoryCacheMode
|
||||
{
|
||||
get { return RepositoryCacheMode.Default; }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IsolatedRuntimeCache IsolatedRuntimeCache { get { throw new NotImplementedException(); } }
|
||||
|
||||
/// <inheritdoc />
|
||||
public UmbracoDatabase Database
|
||||
{
|
||||
|
||||
16
src/Umbraco.Core/Scoping/RepositoryCacheMode.cs
Normal file
16
src/Umbraco.Core/Scoping/RepositoryCacheMode.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<EventMessage> _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
|
||||
|
||||
/// <inheritdoc />
|
||||
public RepositoryCacheMode RepositoryCacheMode
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode;
|
||||
if (ParentScope != null) return ParentScope.RepositoryCacheMode;
|
||||
return RepositoryCacheMode.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, Action<bool>> _actions;
|
||||
|
||||
private IDictionary<string, Action<bool>> Actions
|
||||
{
|
||||
get
|
||||
{
|
||||
_database.Dispose();
|
||||
_database = null;
|
||||
if (ParentScope != null) return ParentScope.Actions;
|
||||
|
||||
return _actions ?? (_actions
|
||||
= new Dictionary<string, Action<bool>>());
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(string name, Action action)
|
||||
{
|
||||
Actions[name] = completed =>
|
||||
{
|
||||
if (completed) action();
|
||||
};
|
||||
}
|
||||
|
||||
public void Register(string name, Action<bool> action)
|
||||
{
|
||||
Actions[name] = action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +114,9 @@ namespace Umbraco.Core.Scoping
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -157,11 +157,11 @@ namespace Umbraco.Core.Scoping
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -156,6 +156,7 @@
|
||||
<Compile Include="Cache\PayloadCacheRefresherBase.cs" />
|
||||
<Compile Include="Cache\RepositoryCachePolicyOptions.cs" />
|
||||
<Compile Include="Cache\StaticCacheProvider.cs" />
|
||||
<Compile Include="Cache\ScopedDefaultRepositoryCachePolicy.cs" />
|
||||
<Compile Include="Cache\TypedCacheRefresherBase.cs" />
|
||||
<Compile Include="Cache\DeepCloneRuntimeCacheProvider.cs" />
|
||||
<Compile Include="CodeAnnotations\FriendlyNameAttribute.cs" />
|
||||
@@ -515,6 +516,7 @@
|
||||
<Compile Include="Scoping\Scope.cs" />
|
||||
<Compile Include="Scoping\ScopeProvider.cs" />
|
||||
<Compile Include="Scoping\ScopeReference.cs" />
|
||||
<Compile Include="Scoping\RepositoryCacheMode.cs" />
|
||||
<Compile Include="Security\ActiveDirectoryBackOfficeUserPasswordChecker.cs" />
|
||||
<Compile Include="Security\BackOfficeClaimsIdentityFactory.cs" />
|
||||
<Compile Include="Security\BackOfficeCookieAuthenticationProvider.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs
Normal file
107
src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs
Normal file
@@ -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<IUser>(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>(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<IUser>(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<IUser>(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<IUser>(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<IUser>(user.Id), () => null);
|
||||
Assert.IsNotNull(globalCached);
|
||||
Assert.AreEqual(user.Id, globalCached.Id);
|
||||
Assert.AreEqual(complete ? "changed" : "name", globalCached.Name);
|
||||
}
|
||||
|
||||
public static string GetCacheIdKey<T>(object id)
|
||||
{
|
||||
return string.Format("{0}{1}", GetCacheTypeKey<T>(), id);
|
||||
}
|
||||
|
||||
public static string GetCacheTypeKey<T>()
|
||||
{
|
||||
return string.Format("uRepo_{0}_", typeof(T).Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@
|
||||
<Compile Include="Scheduling\DeployTest.cs" />
|
||||
<Compile Include="Routing\NiceUrlRoutesTests.cs" />
|
||||
<Compile Include="Scoping\LeakTests.cs" />
|
||||
<Compile Include="Scoping\ScopedRepositoryTests.cs" />
|
||||
<Compile Include="Scoping\ScopeTests.cs" />
|
||||
<Compile Include="TestHelpers\Entities\MockedPropertyTypes.cs" />
|
||||
<Compile Include="TryConvertToTests.cs" />
|
||||
|
||||
Reference in New Issue
Block a user