A first look at ConfORM - Part 2
You can find my part 1 here and all source codes in my git.
Today I will continue writing about ConfORM. I've said in the previous parts of ConfORM and the advantages of it. This part I will only concentrate on how to build a multiple session factory for ConfORM (also known as multiple database). I get my ideas mainly from Sharp Architecture. First of all, why we need multiple Session Factory? If anyone knows about NHibernate knows that build a Session Factory is very expensive. As it will be a lot of work as a validation schema, entity, preparing data for mapping, build mapping files, prepared caching, preparing relational object model, ... As we know, on a request, the NHibernate Session Factory will build it again, so would be uneconomic and effort-consuming (maybe it would be better in second times, but it is also painful). What if we just build a SessionFactory, and spend a lot of times. That is a part of the day this post. And another part is in the business applications, people often connect to multiple databases simultaneously. And the question is how can we build an engine to store multiple connections to the database? The simple things but it is often very difficult to execute. I've been doing it for ADO.NET and not really very happy. Sharp Architecture worked so well on multiple database. So why do we need to "reinventing the wheel", just use it. I joined the community of Sharp Architecture and felt that this was a really good community. Everyone here works pretty positive and have a lot of feedback every day. I do not take advantage of Sharp Architecture that only a small part of it, so I copied it and changed slightly to suit ConfORM. This is shameful.
And now it's time I start my work.
To realize the multiple databases, I will build a model for the system entity. This time I concentrated on the football model. I will not repeat how to build it, please refer to part 1 of this. I only speak rudimentary knowledge of its domain.
As a management team should be related to the club, players, match. A player will play in one or more positions. A match will be held at a location and will take place between the two clubs, and of course we will have 22 players participating in. That's it. We will have the Domain Diagram as follows:
After that we would have ConfORm database schema and we will not care about it anymore.
We'll need one to take care about transaction IDbContext, it is really a Unit of Work:
public interface IDbContext
{
void CommitChanges();
IDisposable BeginTransaction();
void CommitTransaction();
void RollbackTransaction();
}
and a ISessionStorage to store and keep the Session in memory:
public interface ISessionStorage
{
ISession GetSessionForKey(string factoryKey);
void SetSessionForKey(string factoryKey, ISession session);
IEnumerable<ISession> GetAllSessions();
}
and then we will also need a factory ITenantContext to get the key:
public interface ITenantContext
{
string GetFactory(string key);
}
and finally we will need to take care about persistence IRepository for us:
public interface IRepository<T> : IRepositoryWithTypedId<T, int> { }
public interface IRepositoryWithTypedId<T, in TId>
{
T Get(TId id);
IList<T> GetAll();
IList<T> FindAll(IDictionary<string, object> propertyValuePairs);
T FindOne(IDictionary<string, object> propertyValuePairs);
T SaveOrUpdate(T entity);
void Delete(T entity);
IDbContext GetDbContext(string factoryKey = "");
}
After completing the contract for the system, we will take steps to build it in more detail at the following:
+ DbContext:
public class DbContext : RootObject, IDbContext
{
public DbContext(string factoryKey)
{
Contract.Requires(!string.IsNullOrEmpty(factoryKey), "factoryKey may not be null or empty");
FactoryKey = factoryKey;
}
private ISession Session
{
get
{
return NHibernateSession.CurrentFor(FactoryKey);
}
}
public void CommitChanges()
{
Session.Flush();
}
public IDisposable BeginTransaction()
{
return Session.BeginTransaction();
}
public void CommitTransaction()
{
Session.Transaction.Commit();
}
public void RollbackTransaction()
{
Session.Transaction.Rollback();
}
public string FactoryKey { get; set; }
}
+ SimpleSessionStorage, it is just a simple storage that contain a Dictionary for store a pairs of key and value, in the future if you use the ASP.NET MVC, Silverlight or WPF, you can wrapping this class and adapt with your requirement:
public class SimpleSessionStorage : RootObject, ISessionStorage
{
public SimpleSessionStorage() { }
public ISession GetSessionForKey(string factoryKey)
{
ISession session;
return !_storage.TryGetValue(factoryKey, out session) ? null : session;
}
public void SetSessionForKey(string factoryKey, ISession session)
{
_storage[factoryKey] = session;
}
public IEnumerable<ISession> GetAllSessions()
{
return _storage.Values;
}
private readonly Dictionary<string, ISession> _storage = new Dictionary<string, ISession>();
}
+ SimpleTenantContext, yeah it is also a simple tenant context class, you can wrap this in the future for your need, now I just hard code this class (violated the Open-Close :D ), just keep it simple in this sample:
public class SimpleTenantContext : ITenantContext
{
private readonly ConcurrentDictionary<string, string> _tenants = new ConcurrentDictionary<string, string>();
public SimpleTenantContext()
{
InitTenants();
}
public string GetFactory(string key)
{
string value;
_tenants.TryGetValue(key, out value);
return value;
}
private void InitTenants()
{
_tenants.TryAdd("NewsMgtKey", "NewsMgtFactoryKey");
_tenants.TryAdd("FootballMgtKey", "FootballMgtFactoryKey");
}
}
+ NHibernateSession, just keep it from Sharp Architecture and improved some thing else:
public static class NHibernateSession
{
#region private variables
private static IInterceptor _registeredInterceptor;
private static readonly ConcurrentDictionary<string, ISessionFactory> SessionFactories = new ConcurrentDictionary<string, ISessionFactory>();
private static ISessionFactory _slug; // for the out slug
private static IConfigBuilder _configBuilder;
#endregion private variables
#region Init() overloads
[CLSCompliant(false)]
public static Configuration Init(
IConfigBuilder configBuilder,
ISessionStorage storage,
string connectionString,
string factoryKey
)
{
_configBuilder = configBuilder;
InitStorage(storage);
try
{
if (!string.IsNullOrEmpty(factoryKey))
{
var config = AddConfiguration(factoryKey, connectionString);
if (config != null)
DefaultFactoryKey = factoryKey;
return config;
}
return AddConfiguration(DefaultFactoryKey, connectionString);
}
catch
{
Storage = null;
throw;
}
}
#endregion Init() overloads
public static void InitStorage(ISessionStorage storage)
{
Contract.Requires(storage != null, "storage mechanism was null but must be provided");
Contract.Requires(Storage == null, "A storage mechanism has already been configured for this application");
Storage = storage;
}
[CLSCompliant(false)]
public static Configuration AddConfiguration(
string factoryKey,
string connectionString
)
{
var config = AddConfiguration(
factoryKey,
_configBuilder.BuildConfiguration(connectionString, factoryKey)
);
return config;
}
[CLSCompliant(false)]
public static Configuration AddConfiguration(
string factoryKey,
Configuration cfg)
{
var sessionFactory = CreateSessionFactoryFor(cfg);
return AddConfiguration(factoryKey, sessionFactory, cfg);
}
[CLSCompliant(false)]
public static Configuration AddConfiguration(
string factoryKey,
ISessionFactory sessionFactory,
Configuration cfg)
{
Contract.Requires(!SessionFactories.ContainsKey(factoryKey),
"A session factory has already been configured with the key of " + factoryKey);
SessionFactories.TryAdd(factoryKey, sessionFactory);
return cfg;
}
public static ISession Current
{
get
{
Contract.Requires(!IsConfiguredForMultipleDatabases(),
"The NHibernateSession.Current property may " +
"only be invoked if you only have one NHibernate session factory; i.e., you're " +
"only communicating with one database. Since you're configured communications " +
"with multiple databases, you should instead call CurrentFor(string factoryKey)");
return CurrentFor(DefaultFactoryKey);
}
}
public static void RegisterInterceptor(IInterceptor interceptor)
{
Contract.Requires(interceptor != null, "interceptor may not be null");
_registeredInterceptor = interceptor;
}
public static bool IsConfiguredForMultipleDatabases()
{
return SessionFactories.Count > 1;
}
public static ISession CurrentFor(string factoryKey)
{
Contract.Requires(!string.IsNullOrEmpty(factoryKey), "factoryKey may not be null or empty");
Contract.Requires(Storage != null, "An ISessionStorage has not been configured");
Contract.Requires(SessionFactories.ContainsKey(factoryKey), "An ISessionFactory does not exist with a factory key of " + factoryKey);
var session = Storage.GetSessionForKey(factoryKey);
if (session == null)
{
if (_registeredInterceptor != null)
{
session = SessionFactories[factoryKey].OpenSession(_registeredInterceptor);
}
else
{
session = SessionFactories[factoryKey].OpenSession();
}
Storage.SetSessionForKey(factoryKey, session);
}
return session;
}
public static void CloseAllSessions()
{
if (Storage != null)
foreach (var session in Storage.GetAllSessions().Where(session => session.IsOpen))
{
session.Close();
}
}
public static void Reset()
{
if (Storage != null)
{
foreach (var session in Storage.GetAllSessions())
{
session.Dispose();
}
}
SessionFactories.Clear();
Storage = null;
_registeredInterceptor = null;
}
public static ISessionFactory GetSessionFactoryFor(string factoryKey)
{
if (!SessionFactories.ContainsKey(factoryKey))
return null;
return SessionFactories[factoryKey];
}
public static void RemoveSessionFactoryFor(string factoryKey)
{
if (GetSessionFactoryFor(factoryKey) != null)
{
SessionFactories.TryRemove(factoryKey, out _slug);
}
}
public static ISessionFactory GetDefaultSessionFactory()
{
return GetSessionFactoryFor(DefaultFactoryKey);
}
public static string DefaultFactoryKey = PersistanceConstants.DEFAULT_FACTORY_SESSION_KEY;
public static ISessionStorage Storage { get; set; }
private static ISessionFactory CreateSessionFactoryFor(Configuration cfg)
{
return cfg.BuildSessionFactory();
}
}
+ RepositoryBase class, contain something common for alot of Repository to use (Abstract is good, I think so, but should be fine if we can use composition):
public abstract class RepositoryBase<T> : RepositoryWithTypedId<T, int>, IRepository<T> { }
public abstract class RepositoryWithTypedId<T, TId> : RootObject, IRepositoryWithTypedId<T, TId>
{
private IDbContext _dbContext;
protected virtual ISession GetSession(string factoryKey = "")
{
if (string.IsNullOrEmpty(factoryKey))
{
return NHibernateSession.CurrentFor(PersistanceConstants.DEFAULT_FACTORY_SESSION_KEY);
}
return NHibernateSession.CurrentFor(factoryKey);
}
public virtual IDbContext GetDbContext(string factoryKey = "")
{
if (string.IsNullOrEmpty(factoryKey))
{
return new DbContext(PersistanceConstants.DEFAULT_FACTORY_SESSION_KEY);
}
return _dbContext ?? (_dbContext = new DbContext(factoryKey));
}
public virtual T Get(TId id)
{
return GetSession().Get<T>(id);
}
public virtual IList<T> GetAll()
{
var criteria = GetSession().CreateCriteria(typeof(T));
return criteria.List<T>();
}
public virtual IList<T> FindAll(IDictionary<string, object> propertyValuePairs)
{
Contract.Requires(propertyValuePairs != null && propertyValuePairs.Count > 0,
"propertyValuePairs was null or empty; " +
"it has to have at least one property/value pair in it");
var criteria = GetSession().CreateCriteria(typeof(T));
foreach (var key in propertyValuePairs.Keys)
{
if (propertyValuePairs[key] != null)
{
criteria.Add(Restrictions.Eq(key, propertyValuePairs[key]));
}
else
{
criteria.Add(Restrictions.IsNull(key));
}
}
return criteria.List<T>();
}
public virtual T FindOne(IDictionary<string, object> propertyValuePairs)
{
var foundList = FindAll(propertyValuePairs);
if (foundList.Count > 1)
{
throw new NonUniqueResultException(foundList.Count);
}
return foundList.Count == 1 ? foundList[0] : default(T);
}
public virtual void Delete(T entity)
{
GetSession().Delete(entity);
}
public virtual T SaveOrUpdate(T entity)
{
Contract.Requires(!(entity is IEntity<TId>),
"For better clarity and reliability, Entities with an assigned Id must call Save or Update");
GetSession().SaveOrUpdate(entity);
return entity;
}
}
+ And we will have 2 repositories for News and Football. In this case, we will connect to 2 database for get data:
public class NewsRepository : RepositoryWithTypedId<News, Guid>, INewsRepository
{
private readonly ITenantContext _tenantContext;
public NewsRepository(ITenantContext tenantContext)
{
_tenantContext = tenantContext;
}
public override IDbContext GetDbContext(string factoryKey = "")
{
Contract.Requires(_tenantContext != null, "Tenant Context is null");
return !string.IsNullOrEmpty(factoryKey)
? base.GetDbContext(factoryKey)
: base.GetDbContext(_tenantContext.GetFactory("NewsMgtKey"));
}
protected override ISession GetSession(string factoryKey = "")
{
Contract.Requires(_tenantContext != null, "Tenant Context is null");
return !string.IsNullOrEmpty(factoryKey)
? base.GetSession(factoryKey)
: base.GetSession(_tenantContext.GetFactory("NewsMgtKey"));
}
}
and,
public class FootballResultRepository : RepositoryWithTypedId<Game, Guid>, IFootballResultRepository
{
public static readonly string FootballFactoryKey = "FootBallMsgtKey";
protected override ISession GetSession(string factoryKey = "")
{
return base.GetSession(FootballFactoryKey);
}
}
+ Finally, I have some Unit testing for this:
[TestClass]
public class ConfigurationTesting
{
private ConfORMConfigBuilder _confOrmConfigBuilder;
private string _connectionString;
private string _sessionFactoryName;
private IEnumerable<string> _assemblies;
[TestInitialize]
public void Init()
{
_connectionString = "Server=localhost;database=ConfORMSample;Integrated Security=SSPI;";
_sessionFactoryName = "SessionFactory1";
_assemblies = new List<string>
{
"ConfORMSample.NewsMgt.Entities.dll"
};
_confOrmConfigBuilder = new SqlServerConfORMConfigBuilder(_assemblies, "ConfORMSample.dbo");
NHibernateSession.Init(_confOrmConfigBuilder, new SimpleSessionStorage(), _connectionString, string.Empty);
}
[TestMethod]
public void Can_Init_Config()
{
var config = _confOrmConfigBuilder.BuildConfiguration(_connectionString, _sessionFactoryName);
Assert.IsNotNull(config);
}
...
}
and,
[TestClass]
public class FootBallConfigurationTesting
{
private ConfORMConfigBuilder _confOrmConfigBuilder;
private string _connectionString;
private IEnumerable<string> _assemblies;
[TestInitialize]
public void InitTest()
{
_connectionString = "Server=localhost;database=ConfORMSample_FootballMsgt;Integrated Security=SSPI;";
_assemblies = new List<string>
{
"ConfORMSample.Football.Entities.dll"
};
_confOrmConfigBuilder = new Football.SqlServerConfORMConfigBuilder(_assemblies, "ConfORMSample_FootballMsgt.dbo");
NHibernateSession.Init(_confOrmConfigBuilder, new SimpleSessionStorage(), _connectionString, "Football");
}
[TestMethod]
public void Can_Init_Football_Config()
{
Assert.IsNotNull(_confOrmConfigBuilder);
}
[TestMethod]
public void Can_Get_Configuration_From_NHibernate_Session()
{
var session = NHibernateSession.CurrentFor("Football");
Assert.IsNotNull(session);
}
[TestCleanup]
public void CleanUp()
{
GC.SuppressFinalize(_confOrmConfigBuilder);
}
}
and,
[TestClass]
public class MultipleDatabaseTesting : TestingBase
{
private ConfORMConfigBuilder _newsMgtConfigBuilder;
private string _newsMgtConnectionString;
private IEnumerable<string> _newsMgtAssemblies;
private ConfORMConfigBuilder _footballConfigBuilder;
private string _footballConnectionString;
private IEnumerable<string> _footballAssemblies;
private ISessionStorage _sessionStorage;
[TestInitialize]
public override void Init()
{
base.Init();
_sessionStorage = new SimpleSessionStorage();
// news management config area
_newsMgtConnectionString = "Server=localhost;database=ConfORMSample;Integrated Security=SSPI;";
_newsMgtAssemblies = new List<string>
{
"ConfORMSample.NewsMgt.Entities.dll"
};
_newsMgtConfigBuilder = new Football.SqlServerConfORMConfigBuilder(_newsMgtAssemblies, "ConfORMSample.dbo");
NHibernateSession.Init(_newsMgtConfigBuilder, _sessionStorage, _newsMgtConnectionString, "NewsMgt");
// football management config area
_footballConnectionString = "Server=localhost;database=ConfORMSample_FootballMsgt;Integrated Security=SSPI;";
_footballAssemblies = new List<string>
{
"ConfORMSample.Football.Entities.dll"
};
_footballConfigBuilder = new Football.SqlServerConfORMConfigBuilder(_footballAssemblies, "ConfORMSample_FootballMsgt.dbo");
NHibernateSession.Init(_footballConfigBuilder, _sessionStorage, _footballConnectionString, "FootballMgt");
}
[TestMethod]
public void Can_Init_All_Config()
{
Assert.IsNotNull(_newsMgtConfigBuilder);
Assert.IsNotNull(_footballConfigBuilder);
}
[TestMethod]
public void Can_Get_NewsMgt_Session()
{
Assert.IsNotNull(NHibernateSession.CurrentFor("NewsMgt"));
}
[TestMethod]
public void Can_Get_FootballMgt_Session()
{
Assert.IsNotNull(NHibernateSession.CurrentFor("FootballMgt"));
}
[TestCleanup]
public override void CleanUp()
{
base.Init();
GC.SuppressFinalize(_newsMgtConfigBuilder);
GC.SuppressFinalize(_footballConfigBuilder);
GC.SuppressFinalize(_sessionStorage);
}
}
Anyway my approach is maybe not a good, but I just want to write it as here and find some feedbacks from .NET community.
So please leave your thinking about my post.
Happy coding!