A little Persistence Ignorance framework that features those Unit of Work, Repository, and Inversion of Control patterns you keep hearing so much about.
Only, now you're not pecking them out and maintaining them in your application code, because Ignorance already did that.
So, in much the same way that your business logic will be carefree and clueless about how your data is persisted, you get to skip down the hallway and be all, "la la, business code, business code", and not give two squirts about implementing another IRepository.
Yep. Because that's exactly what I said.
But let's say you have this amazing web app, and now you want to build out a occasionally-connected native client for it. Well, now your logic needs to work with local storage. And that local storage doesn't do Entity Framework. If your domain and client logic is peppered with Entity Framework-specific hooks, you can't just reuse that DLL. That DLL that already contains your carefully curated and tested and trusted domain logic. That DLL that is the heart and identity of your application.
Persistence Ignorance makes that logic portable, self-contained, instantly reusable. What's more, it's dead simple to test what's unique and important about your app. Mock up your database, don't muck up your database.
Okay. So your client code will be all:
// hey kids, let's add a new contact
using (var work = Ignorance.Create.Work())
{
// create a service object,
// which houses any logic around data types
var contacts = new ContactService(work);
// instantiate & init a new contact object
var c = contacts.Create();
c.FirstName = "Merlin";
c.LastName = "Mann";
c.Title = "Hi. Can I Axe You A Question?";
// add it to your list of contacts (repository)
contacts.Add(c);
// validate and commit changes
work.Save();
}
Then, your logic/business/domain code is all:
using System;
using System.Collections.Generic;
using Ignorance;
using Ignorance.Domain;
using YourApplicationHere.Data;
namespace YourApplicationHere.Domain
{
public class ContactService : Service<Contact>
{
public ContactService(IWork work) : base(work) { }
protected override void OnCreated(Contact entity)
{
entity.FirstName = "New";
entity.LastName = "Guy";
entity.Suffix = "Esq.";
}
protected override void OnSaving(Contact entity)
{
if (!entity.EmailAddress.Contains("@"))
throw new ApplicationException("That's not an email address.");
}
protected override void OnDeleting(Contact entity)
{
if (entity.LastName == "Fazzaro")
throw new ApplicationException("Don't delete me or my relatives!");
}
public IEnumerable<Contact> GetContactsWithSuffixesBecauseTheyAreAwesome()
{
var repo = GetStore();
return repo.Some(p => p.Suffix != null);
}
public void JustGoAheadAndMakeSureEveryoneHasSuffixesNow()
{
var repo = GetStore();
var unsuffixedPeeps = repo.Some(p => p.Suffix == null);
foreach (var peep in unsuffixedPeeps)
{
peep.Suffix = "Esq.";
}
}
}
}
And that's more or less all you're left holding the bag for. I know, right?
That Contact object (and all of its brother and sister data objects) is just a POCO, in this instance generated by Entity Framework's new DbContext T4. But of course, a POCO is a POCO is a POCO--ignorant of its comings and goings, just like we like 'em here in DDD-ville.
The magic is all in Ignorance's dependence on Dependency Injection, courtesy of Unity. Here's the relevant config section for the above app that glues everything together:
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <alias alias="IWork" type="Ignorance.IWork, Ignorance" /> <alias alias="IStore" type="Ignorance.IStore`1, Ignorance" /> <alias alias="EntityFrameworkWork" type="Ignorance.EntityFramework.Work, Ignorance.EntityFramework" /> <alias alias="EntityFrameworkStore" type="Ignorance.EntityFramework.Store`1, Ignorance.EntityFramework" /> <alias alias="LinqToSqlWork" type="Ignorance.LinqToSql.Work, Ignorance.LinqToSql" /> <alias alias="LinqToSqlStore" type="Ignorance.LinqToSql.Store`1, Ignorance.LinqToSql" /> <alias alias="AdventureWorksEntities" type="YourApplicationHere.Data.EntityFramework.AdventureWorksEntities, YourApplicationHere" /> <alias alias="DbContext" type="System.Data.Entity.DbContext, EntityFramework" /> <container> <register type="IWork" mapTo="EntityFrameworkWork"> <constructor> <param name="context" dependencyType="AdventureWorksEntities" type="DbContext" /> </constructor> </register> <register type="IStore" mapTo="EntityFrameworkStore" /> </container> </unity>
Ignorance comes with pre-implemented Work/Store pairs for Entity Framework and LINQ To SQL storage. But if your fav ORM is other, it's pretty straightforward to cook one up--just implement the Ignorance.IWork and Ignorance.IStore interfaces, or inherit the Ignorance.Work and Ignorance.Store classes.
For instance, here's all I had to do for the LINQ to SQL pair:
using System;
using System.Collections;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;
using AutoMapper;
namespace Ignorance.LinqToSql
{
public class Work : Ignorance.Work
{
public DataContext DataContext { get; set; }
public Work(DataContext context)
{
this.DataContext = context;
}
protected override void Commit()
{
this.DataContext.SubmitChanges();
}
public override void Dispose()
{
this.DataContext.Dispose();
}
public override ICollection Added
{
get { return this.DataContext.GetChangeSet().Inserts as ICollection; }
}
public override ICollection Updated
{
get { return this.DataContext.GetChangeSet().Updates as ICollection; }
}
public override System.Collections.ICollection Deleted
{
get { return this.DataContext.GetChangeSet().Deletes as ICollection; }
}
}
public abstract class Store<T> : Ignorance.Store<T> where T : class
{
public Store(Work work) : base(work) { }
protected virtual DataContext Context
{
get { return (this.Work as LinqToSql.Work).DataContext; }
}
protected override IQueryable<T> Data
{
get { return this.Table as IQueryable<T>; }
}
protected virtual Table<T> Table
{
get { return this.Context.GetTable<T>(); }
}
protected abstract Expression<Func<T, object>> Key { get; }
protected virtual T GetAttachedEntity(T entity)
{
var key = Key.Compile().Invoke(entity);
Expression<Func<T, bool>> exp = (p) => Key.Compile().Invoke(p).Equals(key);
return this.Table.SingleOrDefault(exp.Compile());
}
public override void Remove(T entity)
{
var original = GetAttachedEntity(entity);
if (original != null)
this.Table.DeleteOnSubmit(original);
}
public override void Attach(T entity)
{
var original = GetAttachedEntity(entity);
if (original != null)
{
Mapper.CreateMap<T, T>().ForMember(Key, o => o.Ignore());
Mapper.AssertConfigurationIsValid();
Mapper.Map<T, T>(entity, original);
}
}
public override void Add(T entity)
{
this.Table.InsertOnSubmit(entity);
}
}
}
Seriously.
Copyright © 2012, Jon Fazzaro
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.