New implementation of IDatabaseFactory, allows setting the DatabaseContext.Current at runtime

with a custom IDatabaseFactory (mostly for testing).
Changes AuditTrail to internal so the public way is just with the 'Audit' class.
Fixed ThreadSafetyServiceTests which was failing with the new AuditTrail stuff because of the
Database instances, this is not solved with the new PerThreadDatabaseFactory for the unit test.
Created new 'UmbracoDatabase' object which inherits from the PetaPoco one so that we can future proof
the implementation as we might want some custom logic on there. Now the IDatabaseFactory returns
an UmbracoDatabase instead of just Database.
This commit is contained in:
Shannon Deminick
2012-12-12 03:47:04 +05:00
parent 6ac2db9497
commit a0e4492a35
9 changed files with 190 additions and 65 deletions

View File

@@ -1,5 +1,8 @@
namespace Umbraco.Core.Auditing
{
/// <summary>
/// Public method to create new audit trail
/// </summary>
public static class Audit
{
public static void Add(AuditTypes type, string comment, int userId, int objectId)

View File

@@ -5,7 +5,7 @@ namespace Umbraco.Core.Auditing
/// <summary>
/// Represents the Audit implementation
/// </summary>
public class AuditTrail
internal class AuditTrail
{
#region Singleton

View File

@@ -22,25 +22,34 @@ namespace Umbraco.Core
/// </remarks>
public class DatabaseContext
{
private bool _configured;
private readonly IDatabaseFactory _factory;
private bool _configured;
private string _connectionString;
private string _providerName;
private static volatile Database _globalInstance = null;
private static readonly object Locker = new object();
#region Singleton
private static readonly Lazy<DatabaseContext> lazy = new Lazy<DatabaseContext>(() => new DatabaseContext());
/// <summary>
/// Gets the current Database Context.
/// </summary>
public static DatabaseContext Current { get { return lazy.Value; } }
private DatabaseContext()
private static DatabaseContext _customContext = null;
private static readonly Lazy<DatabaseContext> lazy = new Lazy<DatabaseContext>(() => new DatabaseContext(new DefaultDatabaseFactory()));
/// <summary>
/// Gets the current Database Context.
/// </summary>
public static DatabaseContext Current
{
//return the _custom context if it is set, otherwise the automatic lazy instance
get { return _customContext ?? lazy.Value; }
//Allows setting a custom database context for the 'Current', normally used for unit tests or if the
//default IDatabaseFactory is not sufficient.
internal set { _customContext = value; }
}
internal DatabaseContext(IDatabaseFactory factory)
{
_factory = factory;
}
#endregion
#endregion
/// <summary>
/// Gets the <see cref="Database"/> object for doing CRUD operations
@@ -49,38 +58,10 @@ namespace Umbraco.Core
/// <remarks>
/// This should not be used for CRUD operations or queries against the
/// standard Umbraco tables! Use the Public services for that.
///
/// If we are running in an http context
/// it will create on per context, otherwise it will a global singleton object
/// </remarks>
public Database Database
{
get
{
//no http context, create the singleton global object
if (HttpContext.Current == null)
{
if (_globalInstance == null)
{
lock (Locker)
{
//double check
if (_globalInstance == null)
{
_globalInstance = new Database(GlobalSettings.UmbracoConnectionName);
}
}
}
return _globalInstance;
}
//we have an http context, so only create one per request
if (!HttpContext.Current.Items.Contains(typeof(DatabaseContext)))
{
HttpContext.Current.Items.Add(typeof(DatabaseContext), new Database(GlobalSettings.UmbracoConnectionName));
}
return (Database)HttpContext.Current.Items[typeof(DatabaseContext)];
}
get { return _factory.CreateDatabase(); }
}
/// <summary>

View File

@@ -1,6 +1,6 @@
namespace Umbraco.Core.Persistence
{
public enum DatabaseProviders
public enum DatabaseProviders
{
SqlServer,
SqlAzure,

View File

@@ -0,0 +1,61 @@
using System.Web;
using Umbraco.Core.Configuration;
namespace Umbraco.Core.Persistence
{
/// <summary>
/// The default implementation for the IDatabaseFactory
/// </summary>
/// <remarks>
/// If we are running in an http context
/// it will create on per context, otherwise it will a global singleton object which is NOT thread safe
/// since we need (at least) a new instance of the database object per thread.
/// </remarks>
internal class DefaultDatabaseFactory : DisposableObject, IDatabaseFactory
{
private static volatile UmbracoDatabase _globalInstance = null;
private static readonly object Locker = new object();
public UmbracoDatabase CreateDatabase()
{
//no http context, create the singleton global object
if (HttpContext.Current == null)
{
if (_globalInstance == null)
{
lock (Locker)
{
//double check
if (_globalInstance == null)
{
_globalInstance = new UmbracoDatabase(GlobalSettings.UmbracoConnectionName);
}
}
}
return _globalInstance;
}
//we have an http context, so only create one per request
if (!HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory)))
{
HttpContext.Current.Items.Add(typeof(DefaultDatabaseFactory), new Database(GlobalSettings.UmbracoConnectionName));
}
return (UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)];
}
protected override void DisposeResources()
{
if (HttpContext.Current == null)
{
_globalInstance.Dispose();
}
else
{
if (HttpContext.Current.Items.Contains(typeof(DefaultDatabaseFactory)))
{
((UmbracoDatabase)HttpContext.Current.Items[typeof(DefaultDatabaseFactory)]).Dispose();
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace Umbraco.Core.Persistence
{
/// <summary>
/// Used to create the UmbracoDatabase for use in the DatabaseContext
/// </summary>
internal interface IDatabaseFactory : IDisposable
{
UmbracoDatabase CreateDatabase();
}
}

View File

@@ -0,0 +1,32 @@
using System.Data;
using System.Data.Common;
namespace Umbraco.Core.Persistence
{
/// <summary>
/// Represents the Umbraco implementation of the PetaPoco Database object
/// </summary>
/// <remarks>
/// Currently this object exists for 'future proofing' our implementation. By having our own inheritied implementation we
/// can then override any additional execution (such as additional loggging, functionality, etc...) that we need to without breaking compatibility since we'll always be exposing
/// this object instead of the base PetaPoco database object.
/// </remarks>
public class UmbracoDatabase : Database
{
public UmbracoDatabase(IDbConnection connection) : base(connection)
{
}
public UmbracoDatabase(string connectionString, string providerName) : base(connectionString, providerName)
{
}
public UmbracoDatabase(string connectionString, DbProviderFactory provider) : base(connectionString, provider)
{
}
public UmbracoDatabase(string connectionStringName) : base(connectionStringName)
{
}
}
}

View File

@@ -197,6 +197,7 @@
<Compile Include="Persistence\DatabaseModelDefinitions\SystemMethods.cs" />
<Compile Include="Persistence\DatabaseModelDefinitions\TableDefinition.cs" />
<Compile Include="Persistence\DatabaseProviders.cs" />
<Compile Include="Persistence\DefaultDatabaseFactory.cs" />
<Compile Include="Persistence\Factories\ContentFactory.cs" />
<Compile Include="Persistence\Factories\ContentTypeFactory.cs" />
<Compile Include="Persistence\Factories\DataTypeDefinitionFactory.cs" />
@@ -213,6 +214,7 @@
<Compile Include="Persistence\Factories\TemplateFactory.cs" />
<Compile Include="Persistence\Factories\UserFactory.cs" />
<Compile Include="Persistence\Factories\UserTypeFactory.cs" />
<Compile Include="Persistence\IDatabaseFactory.cs" />
<Compile Include="Persistence\Mappers\BaseMapper.cs" />
<Compile Include="Persistence\Mappers\ContentMapper.cs" />
<Compile Include="Persistence\Mappers\ContentTypeMapper.cs" />
@@ -396,6 +398,7 @@
<Compile Include="Persistence\SqlSyntax\SqlSyntaxProviderBase.cs" />
<Compile Include="Persistence\SqlSyntax\SyntaxConfig.cs" />
<Compile Include="Persistence\TransactionType.cs" />
<Compile Include="Persistence\UmbracoDatabase.cs" />
<Compile Include="Persistence\UnitOfWork\FileUnitOfWork.cs" />
<Compile Include="Persistence\UnitOfWork\FileUnitOfWorkProvider.cs" />
<Compile Include="Persistence\UnitOfWork\IDatabaseUnitOfWork.cs" />

View File

@@ -20,16 +20,27 @@ namespace Umbraco.Tests.Services
public class ThreadSafetyServiceTest : BaseDatabaseFactoryTest
{
private PerThreadPetaPocoUnitOfWorkProvider _uowProvider;
private PerThreadDatabaseFactory _dbFactory;
[SetUp]
public override void Initialize()
{
base.Initialize();
//we need to use our own custom IDatabaseFactory for the DatabaseContext because we MUST ensure that
//a Database instance is created per thread, whereas the default implementation which will work in an HttpContext
//threading environment, or a single apartment threading environment will not work for this test because
//it is multi-threaded.
_dbFactory = new PerThreadDatabaseFactory();
//assign the custom factory to the new context and assign that to 'Current'
DatabaseContext.Current = new DatabaseContext(_dbFactory);
//overwrite the local object
DatabaseContext = DatabaseContext.Current;
//here we are going to override the ServiceContext because normally with our test cases we use a
//global Database object but this is NOT how it should work in the web world or in any multi threaded scenario.
//we need a new Database object for each thread.
_uowProvider = new PerThreadPetaPocoUnitOfWorkProvider();
_uowProvider = new PerThreadPetaPocoUnitOfWorkProvider(_dbFactory);
ServiceContext = new ServiceContext(_uowProvider, new FileUnitOfWorkProvider(), new PublishingStrategy());
CreateTestData();
@@ -39,9 +50,9 @@ namespace Umbraco.Tests.Services
public override void TearDown()
{
_error = null;
_lastUowIdWithThread = null;
//dispose!
_dbFactory.Dispose();
_uowProvider.Dispose();
base.TearDown();
@@ -54,9 +65,7 @@ namespace Umbraco.Tests.Services
/// </summary>
private volatile Exception _error = null;
private int _maxThreadCount = 1;
private object _locker = new object();
private Tuple<int, Guid> _lastUowIdWithThread = null;
private const int MaxThreadCount = 20;
[Test]
public void Ensure_All_Threads_Execute_Successfully_Content_Service()
@@ -68,7 +77,7 @@ namespace Umbraco.Tests.Services
Debug.WriteLine("Starting test...");
for (var i = 0; i < _maxThreadCount; i++)
for (var i = 0; i < MaxThreadCount; i++)
{
var t = new Thread(() =>
{
@@ -81,14 +90,14 @@ namespace Umbraco.Tests.Services
var content1 = contentService.CreateContent(-1, "umbTextpage", 0);
content1.Name = "test" + Guid.NewGuid();
Debug.WriteLine("Saving content1 on thread: " + Thread.CurrentThread.ManagedThreadId);
contentService.Save(content1);
contentService.Save(content1);
//Thread.Sleep(100); //quick pause for maximum overlap!
Thread.Sleep(100); //quick pause for maximum overlap!
//var content2 = contentService.CreateContent(-1, "umbTextpage", 0);
//content2.Name = "test" + Guid.NewGuid();
//Debug.WriteLine("Saving content2 on thread: " + Thread.CurrentThread.ManagedThreadId);
//contentService.Save(content2);
var content2 = contentService.CreateContent(-1, "umbTextpage", 0);
content2.Name = "test" + Guid.NewGuid();
Debug.WriteLine("Saving content2 on thread: " + Thread.CurrentThread.ManagedThreadId);
contentService.Save(content2);
}
catch(Exception e)
{
@@ -130,7 +139,7 @@ namespace Umbraco.Tests.Services
Debug.WriteLine("Starting test...");
for (var i = 0; i < _maxThreadCount; i++)
for (var i = 0; i < MaxThreadCount; i++)
{
var t = new Thread(() =>
{
@@ -193,18 +202,16 @@ namespace Umbraco.Tests.Services
}
/// <summary>
/// Creates a Database object per thread, this mimics the web context which is per HttpContext
/// Creates a Database object per thread, this mimics the web context which is per HttpContext and is required for the multi-threaded test
/// </summary>
internal class PerThreadPetaPocoUnitOfWorkProvider : DisposableObject, IDatabaseUnitOfWorkProvider
{
private readonly ConcurrentDictionary<int, Database> _databases = new ConcurrentDictionary<int, Database>();
internal class PerThreadDatabaseFactory : DisposableObject, IDatabaseFactory
{
private readonly ConcurrentDictionary<int, UmbracoDatabase> _databases = new ConcurrentDictionary<int, UmbracoDatabase>();
public IDatabaseUnitOfWork GetUnitOfWork()
public UmbracoDatabase CreateDatabase()
{
//Create or get a database instance for this thread.
//var db = _databases.GetOrAdd(Thread.CurrentThread.ManagedThreadId, i => new Database(Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName));
return new PetaPocoUnitOfWork(new Database(Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName));
var db = _databases.GetOrAdd(Thread.CurrentThread.ManagedThreadId, i => new UmbracoDatabase(Umbraco.Core.Configuration.GlobalSettings.UmbracoConnectionName));
return db;
}
protected override void DisposeResources()
@@ -214,5 +221,31 @@ namespace Umbraco.Tests.Services
}
}
/// <summary>
/// Creates a UOW with a Database object per thread
/// </summary>
internal class PerThreadPetaPocoUnitOfWorkProvider : DisposableObject, IDatabaseUnitOfWorkProvider
{
private readonly PerThreadDatabaseFactory _dbFactory;
public PerThreadPetaPocoUnitOfWorkProvider(PerThreadDatabaseFactory dbFactory)
{
_dbFactory = dbFactory;
}
public IDatabaseUnitOfWork GetUnitOfWork()
{
//Create or get a database instance for this thread.
var db = _dbFactory.CreateDatabase();
return new PetaPocoUnitOfWork(db);
}
protected override void DisposeResources()
{
//dispose the databases
_dbFactory.Dispose();
}
}
}
}