Slow new's day - first crack at Base4 'n Castle

Base4 & Castle

Well I've been working with HTML & style sheets today (mostly) - It's be a long while since I've done web development, so it was a little tedious, and it did briefly cross my mind that life might be easier if we lived in some kind of dictatorship where style sheets and layout were provided for me by the "man"... at any rate I also managed to fit in a bit of monorail and base4 to keep my brain from freezing over.



I think for this post I might just talk about the way I'm using base4... basically I prototyped some stuff and came to the conclusion that I didn't like the smell of the static StorageContext class in base4 and it's default connection - too hard to test against - not to say that it's a bad idea, just that I couldn't see any easy way to test and mock code  for code that used/consumed it... so I decided to work up a simple alternative using castle's IoC and some generic interfaces...



At this point... If you've been using Castle for more then a couple of week this is all old news I'm sure, so probably better off finding something else to read ;o)

The facility

First off we have a facility:

public class Base4StorageFacility : AbstractFacility
{
protected override void Init()
{
string url = FacilityConfig.Attributes["url"];
if (string.IsNullOrEmpty(url))
{
throw new StorageException("The Base4StorageFacility requires a "url" attribute to be set");
}

Kernel.AddComponentInstance("base4.defaultContext", typeof (IItemContext), StorageContext.NewContext(url));
Kernel.AddComponent("base4.repository", typeof (IRepository<>), typeof (Base4Repository<>));
}
}



A facility extends the container with additional functionality, in this case the only reason we're using a facility (instead of registering the components themselves individually) is because we're using a static method to create our context for a specific base4 connection url.



Moving on from here we can register the facility in the containers configuration, incidentally now I have somewhere pleasant to configure what base4 server I connect to by default in my application.



type="MyProject.Core.Base4StorageFacility, MyProject.Core"
url="tcp://Server:@localhost:999/Base4_default" />



The Repository

Now what about the IRepository<> ? well here's the interface:

public interface IRepository
where T : class, IItem, new()
{
IItemListProxy List();
IItemList FindAll();
IItemList Find(ObjectQuery query);
IItemList Find(string path);
IItemList Find(string path, params string[] replaces);
IItemList Find(ObjectPath path);
IItemList Find(ObjectPath path, ObjectScope scope);
IItemList FindUsingSQL(string SQL);
IItemList FindUsingSQL(string SQL, ObjectScope scope);
T Get(T previous);
T Get(object id);
T Get(ItemKey key);
T Get(string relativeUri);
T FindOne(ObjectQuery query);
T FindOne(string opath);
T FindOne(string path, params string[] replaces);
T FindOne(ObjectPath path);
T FindOneUsingSQL(string SQL);
T FindOneUsingSQL(string SQL, ObjectScope scope);
T FindOne(string opath, ObjectScope scope);
T FindOne(ObjectPath path, ObjectScope scope);
void DeleteAll();
void Delete(ObjectPath path);
void Delete(string path);
void Delete(string path, params string[] replaces);
void Delete(T item);
void Save(T item);
T Create(string Xml);
T Create(XmlReader reader);
T Create();
}


It's pretty much works like IItemContext, except that you avoid having to pass generic parameters to the individual methods because the interface itself has a generic parameter... there's a couple of extras there that I'll cover at the end of this post too.



Conversely, the class Base4Repository implements this interface... which looks like this (or at least the first few methods, I've left the rest out for brevity) - this is similar to what Ivan has done.


public class Base4Repository : IRepository
where T : class, IItem, new()
{
private IItemContext _context;

public Base4Repository(IItemContext context)
{
if (context == null) throw new ArgumentNullException("context");
_context = context;
}

public virtual IItemListProxy List()
{
return _context.List();
}

public virtual IItemList FindAll()
{
return _context.FindAll();
}



Notice that it doesn't have a parameterless constructor, we rely on the container to inject the default IItemContext when creating instances of the Base4Repository...

The Container

Now, by default the container assumes a component has a "singleton" lifestyle, thankfully for a type with a
generic parameter it is per that parameter, so this test case below passes - incidentally if you tend to use lifecycles other then the default, I would strongly suggest adding tests to make sure the lifecycle is actually applied... you can just imagine what happens in a multi-threaded app when a "Message" class has a singleton lifecycle when you expected a transient ;o) you end up with some bizarre behavior that might not be picked up in normal unit tests.

[Test]
public void IsSingleton()
{
IRepository fileRepository1 = container.Resolve<>>();
IRepository fileRepository2 = container.Resolve<>>();
Assert.AreSame(fileRepository1, fileRepository2);

IRepository typeRepository1 = container.Resolve<>>();
IRepository typeRepository2 = container.Resolve<>>();
Assert.AreSame(typeRepository1, typeRepository2);
}



cool, clear as mud?



Right... so moving on from there, in my own components when I need to access data I now use the repository... so I could, for instance, create a monorail controller like this:


public class FileController : BaseController
{
private IRepository _repository;

public FileController(IRepository repository)
{
if (repository == null) throw new ArgumentNullException("repository");
_repository = repository;
}

public void Fetch(Guid fileId)
{
CancelLayout();
CancelView(); // very important
Response.Clear(); // ensure the response is empty

FileBase file = _repository.Get(fileId);
if (file != null)
{
Response.ContentType = file.MimeType;
file.FileContent.CopyTo(Response.OutputStream);
}
else
{
Response.StatusCode = 404;
}
}
}



And everything will just work - now in some cases this isn't convenient (the dependency injection model) so I broke down and
created a static repository class as well, which accesses the default container for the application... basically it has the same interface as IRepository, but they're static methods.. so you can code like this:


Track track = Repository.Get(id);



Personally I'm not actually that keen on this approach - The Syzmk RMP product I've worked on uses the container everywhere, but avoids ever having to access the default container statically...  and if you end up with a class being injected with a large number of dependencies it's often (but not always) a good indication that there's some violation of orthogonality - if only because a class consuming that many dependencies is probably doing more then one thing... a little difficult to pick up on otherwise.



But at any rate It seems pretty good for a website, where I can't see me using more the one container (or even child containers) within the same app domain.

Wrinkles...

Moving beyond that, the last thing I have to say is that originally I was creating new instances and then saving them with a repository like this:

Group group = new Group();
...Repository.Save(group);
...
User user = new User();
...user.Groups.Add(group);
...Repository.Save(user);


However, it all turns to 4 shades of brown when we go to add to a many-to-many collection, like this:


Goat goat = new Goat();
goat.Name = "junk";
Repository.Save(goat);

BoatOfGoats boat = new BoatOfGoats();
boat.Name = "track";
boat.Goats.Add(goat); // <-- "storageexception="" :="" default="" is="" not="" available,="" as="" no="" default="" has="" been="">
Repository.Save(track);



As far as I can tell the event handling for the Add(item) method expects a default context to be assigned... now I'd be tempted to call this a bug, but that's a bit rash till I understand the in's and out's of base4, however there's an easy way round it... and that's to assign the context yourself :) so a fix would be to add this line before the one that bombs:


track.Context = container.Resolve();


But that's pretty kludgey, so instead of using:


Goat goat = new Goat();


I added some Create(...) overloads to the repository to take care of it... and do this:


Genre genre = Repository.Create();

Last of all, though I haven't drawn on the entire implementation, I'm hoping to follow (At least in spirit) some of the work Ayende has done with his NHibernate repository concept as the project progresses, I think it will add a more natural "feel" combined with the repositories for implementing transactions vs. interacting with base4's ObjectTransaction directly, not to mention providing something I can test and mock out easily...  We shall see what actually happens as the project progresses.

Written on October 11, 2006