Previous posts in series:
- FubuMVC From Scratch Part 1 ? Basic project structure setup
- FubuMVC From Scratch Part 2 ? FubuMVC configuration and Controller setup
- FubuMVC From Scratch ? Part 3 (Adding View to project)
Persistence is a requirement in virtually every application that we write today, and now the time has come for us to add a persistence model to our FubuMVC app. I know I mentioned moving my samples to the FubuCart source, but I am going to go ahead and implement the persistence piece in the same project the previous articles using, FubuSample. I have also added all the code for this series to the FubuMVC-Contrib project
We will be implementing our persistence model to use NHibernate, Fluent NHibernate, Repository Pattern and UnitOfWork pattern all on top of SQL Express or standard. This will also work for sqllite and other database servers.
Class and Interfaces we have to implement:
- ISessionSourceConfiguration
- SQLServerSessionSourceConfiguration
- IUnitOfWork
- INHibernateUnitOfWork
- NHibernateUnitOfWork
- IRepository
- NHibernateRepository
- DomainEntity
- IDomainQuery
- FubuSamplePersistenceModel
Note: I put the first two in the list in my Core.Config namespace, and the rest in Core.Persistence
Before we start you probably want to go ahead and add the following references to your core project and Web project
Let?s just work our way down the list and get these files implemented.
ISessionSourceConfiguration
public interface ISessionSourceConfiguration
{
bool IsNewDatabase { get; }
ISessionSource CreateSessionSource(PersistenceModel model);
}
SQLServerSessionSourceConfiguration
public class SQLServerSessionSourceConfiguration : ISessionSourceConfiguration
{
#region Implementation of ISessionSourceConfiguration
public bool IsNewDatabase
{
get { return false; }
}
public ISessionSource CreateSessionSource(PersistenceModel model)
{
var properties = GetProperties();
var source = new SessionSource(properties, model);
create_schema_if_it_does_not_already_exist(source);
return source;
}
private void create_schema_if_it_does_not_already_exist(ISessionSource source)
{
if (IsNewDatabase) source.BuildSchema();
}
protected IDictionary<string, string> GetProperties()
{
MsSqlConfiguration config = MsSqlConfiguration.MsSql2005;
config.ConnectionString.FromConnectionStringWithKey(?MYDBKEY?);
config.ShowSql();
config.UseOuterJoin();
return config.ToProperties();
}
#endregion
}
A quick note here: We told SQLServerSessionSourceConfiguration to use a connection string from the app settings file (Web.Config) with a key of ?MYDBKEY? This will be important later.
IUnitOfWork
public interface IUnitOfWork : IDisposable
{
void Initialize();
void Commit();
void Rollback();
}
INHibernateUnitOfWork
public interface INHibernateUnitOfWork : IUnitOfWork
{
ISession CurrentSession { get; }
}
NHibernateUnitOfWork
public class NHibernateUnitOfWork : INHibernateUnitOfWork
{
private ITransaction _transaction;
private bool _isDisposed;
private readonly ISessionSource _source;
private bool _isInitialized;
public NHibernateUnitOfWork(ISessionSource source)
{
_source = source;
}
public void Initialize()
{
should_not_currently_be_disposed();
CurrentSession = _source.CreateSession();
begin_new_transaction();
_isInitialized = true;
}
public ISession CurrentSession { get; private set; }
public void Commit()
{
should_not_currently_be_disposed();
should_be_initialized_first();
_transaction.Commit();
begin_new_transaction();
}
private void begin_new_transaction()
{
if (_transaction != null)
{
_transaction.Dispose();
}
_transaction = CurrentSession.BeginTransaction();
}
public void Rollback()
{
should_not_currently_be_disposed();
should_be_initialized_first();
_transaction.Rollback();
begin_new_transaction();
}
private void should_not_currently_be_disposed()
{
if (_isDisposed) throw new ObjectDisposedException(GetType().Name);
}
private void should_be_initialized_first()
{
if (!_isInitialized) throw new InvalidOperationException("Must initialize (call Initialize()) on NHibernateUnitOfWork before commiting or rolling back");
}
public void Dispose()
{
if (_isDisposed || !_isInitialized) return;
_transaction.Dispose();
CurrentSession.Dispose();
_isDisposed = true;
}
}
IRepository
public interface IRepository
{
void Save<ENTITY>(ENTITY entity)
where ENTITY : DomainEntity;
ENTITY Load<ENTITY>(Guid id)
where ENTITY : DomainEntity;
IQueryable<ENTITY> Query<ENTITY>()
where ENTITY : DomainEntity;
IQueryable<ENTITY> Query<ENTITY>(IDomainQuery<ENTITY> whereQuery)
where ENTITY : DomainEntity;
void Delete<ENTITY>(ENTITY entity);
void DeleteAll<ENTITY>();
}
NHibernateRepository
public class NHibernateRepository : IRepository
{
private readonly INHibernateUnitOfWork _unitOfWork;
public NHibernateRepository(INHibernateUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public void Save<ENTITY>(ENTITY entity) where ENTITY : DomainEntity
{
_unitOfWork.CurrentSession.SaveOrUpdate(entity);
}
public ENTITY Load<ENTITY>(Guid id) where ENTITY : DomainEntity
{
return _unitOfWork.CurrentSession.Load<ENTITY>(id);
}
public IQueryable<ENTITY> Query<ENTITY>() where ENTITY : DomainEntity
{
return _unitOfWork.CurrentSession.Linq<ENTITY>();
}
public IQueryable<ENTITY> Query<ENTITY>(IDomainQuery<ENTITY> whereQuery) where ENTITY : DomainEntity
{
return _unitOfWork.CurrentSession.Linq<ENTITY>().Where(whereQuery.Expression);
}
public void Delete<ENTITY>(ENTITY entity)
{
_unitOfWork.CurrentSession.Delete(entity);
}
public void DeleteAll<ENTITY>()
{
var query = String.Format("from {0}", typeof(ENTITY).Name);
_unitOfWork.CurrentSession.Delete(query);
}
}
DomainEntity
Note: I put both of these classes in my Core.Domain namespace, also XML comments are in Source Repository
public class DomainEntity : IEquatable<DomainEntity>
{
public virtual Guid ID { get; set; }
public virtual bool Equals(DomainEntity other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.ID.Equals(ID);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DomainEntity)obj);
}
public override int GetHashCode()
{
return ID.GetHashCode();
}
public static bool operator ==(DomainEntity left, DomainEntity right)
{
return Equals(left, right);
}
public static bool operator !=(DomainEntity left, DomainEntity right)
{
return !Equals(left, right);
}
}
IDomainQuery
public interface IDomainQuery<ENTITY>
where ENTITY : DomainEntity
{
Expression<Func<ENTITY, bool>> Expression { get; }
}
FubuSamplePersistenceModel
namespace FubuSample.Core.Domain.Persistence
{
public class FubuSamplePersistenceModel : PersistenceModel
{
public FubuSamplePersistenceModel()
{
addMappingsFromThisAssembly();
}
}
}
Now that we have all the puzzle pieces in place we need to hook them up and make them do some work. Open up FubuSampleWebRegistry
And add the following code:
protected override void configure()
{
ForRequestedType<ISessionSourceConfiguration>().AsSingletons()
.TheDefault.Is.OfConcreteType<SQLServerSessionSourceConfiguration>();
ForRequestedType<ISessionSource>().AsSingletons()
.TheDefault.Is.ConstructedBy(ctx =>
ctx.GetInstance<ISessionSourceConfiguration>()
.CreateSessionSource(new FubuSamplePersistenceModel()));
ForRequestedType<IUnitOfWork>().TheDefault.Is.ConstructedBy(ctx => ctx.GetInstance<INHibernateUnitOfWork>());
ForRequestedType<INHibernateUnitOfWork>().CacheBy(InstanceScope.Hybrid)
.TheDefault.Is.OfConcreteType<NHibernateUnitOfWork>();
ForRequestedType<IRepository>().TheDefault.Is.OfConcreteType<NHibernateRepository>();
}
With a little luck and all of our mad skills, we should be able to talk to the database now. So we need to plumb our simple product object up to talk to NHibernate. We will do this with a mapping file, and since we are using FluentNHibernate we can do this strongly typed.
Make the following modifications to Product: Extend DomainEntity and get rid of Id Property, it is inherited
public class Product : DomainEntity
{
public virtual string Name { get; set; }
public virtual string Description { get; set; }
}
Add mapping file for Product:
namespace FubuSample.Core.Domain.Mapping
{
public class ProductMap : ClassMap<Product>
{
public ProductMap()
{
Id(e => e.ID).GeneratedBy.GuidComb();
Map(p => p.Name);
Map(p => p.Description);
}
}
}
You may also want to setup your connection string at this point in the Web.Config file, mine looks like this:
<add name="MYDBKEY" connectionString="Data Source=LOCALHOST;initial catalog=FubuSample;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
I also created an empty database in my local SQL Server instance called ?FubuSample? And then I went back to SQLServerSessionSourceConfiguration and changed the value of IsNewDatabase to return true. This will create the schema for the database when the application first runs. There is a multitude of different ways that we can handle this, but this was easy for the tutorial purposes.
Next we will add a behavior to our core project and tell FubuMVC to apply that behavior to all or our ControllerActions.
Create a class named ?access_the_database_through_a_unit_of_work? in your Core.Web.Behaviors namespace. The contents will look like this:
public class access_the_database_through_a_unit_of_work : IControllerActionBehavior
{
private readonly IUnitOfWork _unitOfWork;
public access_the_database_through_a_unit_of_work(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public IControllerActionBehavior InsideBehavior { get; set; }
public IInvocationResult Result { get; set; }
public OUTPUT Invoke<INPUT, OUTPUT>(INPUT input, Func<INPUT, OUTPUT> func)
where INPUT : class
where OUTPUT : class
{
_unitOfWork.Initialize();
try
{
var output = InsideBehavior.Invoke(input, func);
Result = InsideBehavior.Result;
_unitOfWork.Commit();
return output;
}
catch
{
_unitOfWork.Rollback();
throw;
}
finally
{
_unitOfWork.Dispose();
}
}
}
Now to tell FubuMVC what to do, open up Global.asax.cs or your Bootstrapper, wherever you are doing the configuration of FubuMVC. In this project we are doing it in Global.asax.cs Add a new behavior to the section that starts with ?x.ByDefault.EveryControllerAction? so it looks like this:
x.ByDefault.EveryControllerAction(d =>
{
d.Will<execute_the_result>();
d.Will<access_the_database_through_a_unit_of_work>();
});
The last thing we will do is open up HomeController and add this code above the Index action:
private IRepository _repository;
public HomeController(IRepository repository)
{
_repository = repository;
}
I also modified the Index action to go ahead and create some database values before we query for them just so we can test this thing. (I only did this for the tutorial, I swear) So modify the Index action to look like this:
public IndexViewModel Index(IndexSetupViewModel inModel)
{
var prod1 = new Product {Name = "TestProduct1", Description = "This is a test product"};
_repository.Save(prod1);
var prod2 = new Product {Name = "TestProduct2", Description = "This is a test product"};
_repository.Save(prod2);
var outModel = new IndexViewModel();
var productList = _repository.Query<Product>();
outModel.Products = productList.ToList().Select(x => new ProductDisplay(x));
return outModel;
}
Now with you should be able to run your web app and browse to /home and all this will be executed and you will be on your way to creating a whole bunch of persist able objects for use with your FubuMVC app.
As always, feedback is welcome.
Generic interface for repositories is a longly discussed topic, i prefer not to have generic one.
Good Stuff!
Tuna, Would love to hear you elaborate more on this. I have used both in production apps with success. For a very simple app, and especially for sample this interface for Repository is simple and works. The other thing to note is, you can plug in your own repository implementation and interface, with no change to FubuMVC or even the sample app.
It works is the key, there is no harm using it. As the domain gets larger, however, instead of using generic interface with inheritence it may be more useful to have some kind of composition and named queries on repositories.
As I said, if it works, then there is no need to complicate the things 🙂
Just my 2 cents
Hello,
This is excellent stuff, however, I cannot get it to compile. Is it possible to get copy of the code.
TIA
Yaz
Hi again,
I downloaded the code from FubuMVC-Contrib project and it compile and I cannot get it to work.
I am getting on the Index.aspx at line 6
<%= this.RenderPartial().Using().ForEachOf(Model.Products) %>
Object reference not set to an instance of an object
Is there anything I need.
TIA
Yaz
Yazid,
My line 6 looks like:().ForEachOf(Model.Products) %>
<%= this.RenderPartial().Using
You should also make sure that you have products in your Model.Products list. Also ensure that ProductInfo is a user control in the Shared folder and that it is declared in view_Page_Type_Declarations
Let me know if this helps.
Ryan,
My line 6 is exactly the same as yours and I have the productInfo as a user control and it is in the sahred folder and declared in view_Page_Type_Declarations.
There is a FubuSample database, is there some script for tha database?
TIA
Yaz
no, but you should check in the core in SQLServerSessionSourceConfiguration and make sure that IsNewDatabase is equal to True. You do have to make sure the database exists however, so just create it. NHibernate will generate the schema.
Ryan,
The code I have been using is from the FubuMVC-Contrib project.
I have created the database. and this is the StackTrace
at ASP.views_home_index_aspx.__RenderindexContent(HtmlTextWriter __w, Control parameterContainer) in c:\Downloads\FubuMVC\FubuSample\samples\FubuSample-Series\Part4\FubuSample\src\FubuSample.Web\Views\Home\Index.aspx:line 6
at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
at System.Web.UI.Control.Render(HtmlTextWriter writer)
at System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter)
at System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter)
at System.Web.UI.Control.RenderControl(HtmlTextWriter writer)
at System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children)
at System.Web.UI.Control.RenderChildren(HtmlTextWriter writer)
at System.Web.UI.HtmlControls.HtmlForm.RenderChildren(HtmlTextWriter writer)
TIA
Yaz