The right dependency level
This post was prompt by an email that was
I've read many things on forums regarding the architecture of an ASP.NET 2.0 application which is using NHibernate. Personally I like most the idea with service module (not necessary web service, just an assembly with the role of a service). The main idea for me is to make the asp.net application independent of the NH.
Therefore I consider that asp.net app should be aware only of the Model, Service.My main concern is how can I make this service to force NH to be transparent for the main application. What I mean with this.
Some of the methods from this assembly, that will return Model objects, will use directly HQL (these ones are no problem).
But I will need for sure a wrapper over the NH session, in this way NH will became unknown to the main project.The problem in creating this wrapper.
Some of the methods are very simple...for example Delete, Save, GetById.
The main problem is on creating the methods for retrieving the data based on a list of conditions.
To Most of everyone agree that having something like new SqlConnection() in your business level code is not a good idea. Why then people have much less trouble with code such as this?
IList<Customer> customers =
session.CreateCriteria(typeof(Customer)) .List<Customer>(); //process customers
Yes, this is not a valid example, because this may be a sign that you are mixing your concerns here, but bear with me here, I am not going to build a 9 layer application just for demonstration purposes. If we frown on using a SqlConnection, why is using the session much more acceptable? Isn't something like this preferred?
public IList<Customer> GetCustomers() { // do query here }
Well, it is certainly much more explicit than the previous example, isn't it? The problem that I have with the code above is that it usually turn to something like:
public IList<Customer> GetCustomers() { using(ISession session = factory.OpenSession()) { //transaction handling omitted for brevity return session.CreateCriteria(typeof(Customer)) .List<Customer>(); } }
The problem with that is that it misses out on one of the more important features of the platform, the Unit of Work implementation. It relegate the OR/M into a stupid tool getting/saving data from a persistence store. So, then you take the next step, and create something like IUnitOfWork, and abstract the Unit of Work implementation from the rest of the application. Problem solved, right?
Well, no, it isn't. It is only "solved" for the very simple case. What happens when you want to do a more complex query? Or utilize features of the underlying OR/M to your best advantage.
I am going to pick LLBLGen and NHibernate as two OR/M products that have roughly similar capabilities, but radically different design. LLBLGen doesn't use the Unit of Work pattern, for instance, so the abstraction above it fairly useless in this scenario. A lot of the stuff that you would do in one tool would be actively harmful in another. Trying to build an application in a way that would take advantage of only the basic features of the tool that you are using is a big disservice for the application, it complicate the programming model, and it wouldn't gain you much flexibility in the end.
In my Rhino Commons library, I have two implementations of the IUnitOfWork interface, one for NHibernate, and the second for Active Record, which is NHibernate. I have explicitly made the choice to being able to take full advantage on the capabilities of NHibernate, which means that I can't take the same code base and try to use that against another OR/M, even one that is using the Unit of Work pattern.
NHibernate is explicitly designed to be used in a way that doesn't force a dependency between the domain model and NHibernate, but trying to abstract NHibernate from the service layer is a losing proposition. The service layer now does a translation from the NHibernate's concepts to terms that are really NHibernate's terms, but in the service layer's namespace. You can't port this code:
FindAll<Customers>( new Predicate(Operator.Equality, "City", "London" );
As seen above, this is usually more awkward than the original syntax.
To be clear, this doesn't mean that you can now freely scatter code all over the place that uses NHibernate directly. SoC is still king, but you should accept that trying to replace a core part of the system such as NHibernate is going to be a very costly endeavor. I have seen a few efforts where people have decided that they want to use NHibernate, but without taking a dependency on it. It was costly, confusing, and eventually abandoned, because the translation layer always had this one more feature that a developer needed that NHibernate supported. And adding this feature to the translation layer would have created 1:1 mapping between NHibernate concepts and the service layer concepts, making the whole effort pointless.
That approach also meant that you couldn't take advantage of NHibernate's tools such as NHibernate Query Generator, which can really make the syntax smooth.
What do I recommend, then?
- Evaluate the various OR/M implementations, and find out what fits your scenarios best.
- Make full use of all the OR/M capabilities, don't limit yourself to a strict subset because in the future you may want to replace it.
- If independence from the OR/M is important to you, understand that no matter how you try, you would still have some dependencies, usually in a place that you wouldn't look for, and that you will likely spend a lot of time on translation layer needlessly.
- However, in that case, be sure to create a service layer that can express what you want to do in the terms of the domain.
What do I mean by that?
Instead of:
session.CreateCriteria(typeof(Customer))
.Add(Expression.Eq("Name", london))
.List<Customer>();
Or even this:
FindAll<Customer>(Where.Customer.City == london);
Which is fairly clear to read even if you are not really familiar with the syntax, try to do this:
FindAll<Customer>( CustomerByCityFinder.With(london) );
This is an approach that can help isolate you from the complexity of the OR/M layer, but it means that you now separate the place where you do the query from the query itself. This is not a bad thing on itself, except that this is already done for you by the OR/M itself. Using NHQG, those queries are very expressive in the terms of the domain.
I reserve this approach for the more complex queries. Where the query is complex enough to have its own responsibilities.
Comments
Why is this
FindAll<Customer>( CustomerByCityFinder.With(london) );
Really any better than
DataAccess.GetAllCustomers() or
DataAccess.GetCustomersByCity("London")
Public static data access methods are easier to read, write, maintain, and re-implement in another library when switching OR/M implementations.
I think your implementation is geeky cool, but really not any better in reality.
Where was this advice 6 months ago???
I am guilty and I was a failure at trying to remove dependencies from NHibernate. It the end its far easier to keep the dependency and isolate usage in certain areas of the application.
@El Gaupo
1) It prevents from dealing directly with the DAL in your services and domain.
2) It alleviates littering your DAL with methods like FindByXXX
3) It encapsulates the logic of the DAL in the FInder so your service and domain code stay cleaner.
Might be a little off topic.
Just curious about what you think of the approach that is mention in this article old http://www.codeproject.com/aspnet/NHibernateBestPractices.asp.
You didn't make much of a comment on it when you refered to it some time ago not what I can remember anyway.
1) It prevents from dealing directly with the DAL in your services and domain.
Either approach is calling into the DAL, correct?
2) It alleviates littering your DAL with methods like FindByXXX
I have never had a problem reading ReadByXXX, have you? They show up nicely in the VS class browser.
3) It encapsulates the logic of the DAL in the FInder so your service and domain code stay cleaner.
I think you just made that up :)
@ Andreas,
It is a valid approach to using NHibernate. I am following a parallel, but slightly different tack, but I like it.
@El Guapo,
Opened Closed Principal. I can extend the query mechanism without touching the DAL.
In any real world system, you need a LOT of queries, and queries options.
For instance:
FindAll<Customer>( CustomerByCityFinder.With(london).EagerLoad("Orders") );
This is the difference between a service and a framework. A service, like a Data Access Object, can usually be wrapped or replaced easily because it doesn't change how you structure your application. A service just sits on the side-lines waiting to be called into action. A framework, like NHibernate, is very different. A framework wants to be a layer that sits underneath your application and empowers it to do wondrous things.
[I'm sure someone can come up with a crazy sports analogy here...]
Rule of thumb: Don't ever try to put a framework in a box unless portability really is your primary concern. If you find you need to do that, quite probably you need to revisit your assumptions or find/build a service on the side instead.
:-)
Flashback! Your recommendations have a lot of similarities with the pro/con debate of relational DB-engine dependence/independence from years ago - ie, to couple code to DB specific features, or not. I find that a good thing, we've come a long way debating the same arguments again but this time on a higher persistance abstraction level.
My real problem is not the dependency on NHibernate from the service layer and beyond. The problem is generalizing the queries I want to make from the client to the service. If we accept the service as being the real application on which I can independently write any kind of client I want (web client, win client), how do I give the service the ability to accept any arbitrary query from the client, without the client being aware of NHibernate or whatever OR/M I use behind the service?
aCoder,
You use the finder approach outlined above.
That would encapsulate the specifics of NHibernate, while taking full advantage on it.
aCoder,
Or you could use Linq.
/Mats
@El Guapo
Really?
FindAll<Customer>( CustomerByCityFinder.With(london).EagerLoad("Orders") );
In the above where is code for querying the DAL, not encapsulated inside the Finder? What is my service dealing with, a Repo and Finder, not the DAL.
Maybe points 1 and 3 are too similar. Weather your DAL is ADO.Net or NHibernate it doesn't matter, the logic to build the query is encapsulated and isolated in one spot, the Finder.
Point 2 was probably personal preference, I try and eliminate code duplication as much as I can, how much copy and paste is involved in maintaining FindByXXX for every scenario possible?
I would much rather use the specification pattern when building complex queries.
LLBLGen Pro has a fairly sophisticated UnitOfWork class, Oren. It can track all your work for you and is serializable so you can send it over a wire to get it persisted there. It can take part in a transaction (so you can have multiple unitofworks in one transaction) or in a single transaction. You can store delegate calls to procs, direct batch actions, entity saves, deletes into the unitofwork instance.
So it doesn't the session approach nhibernate does. That doesn't mean it doesn't support unitofwork, on the contrary. Actually, you can use our unitofwork on the client or in say the UI tier, BL tier, send it over the wire to another service, (remoting), then add more actions to the unitofwork object and then commit it in your dal layer where you have the availability of the adapter to the db of choice. In all the other layers I mentioned you don't have the ability to commit it if you don't want to. That's the best part.
Frans, that wasn't the case when we last spoke about it, no?
I think that I am confused, because I am associating the unit of work pattern with the NH implementation. Can you explain what the differences are between the two? Maybe I was thinking about persistence by reachability or cascades?
The last time I looked at LLBLGen was about a year ago, sorry.
Is my basic premise wrong then? Would you say that it is possible to move an app from NHibernate to LLBLGen or vice versa without a lot of hassle?
Oren, my appologies, I shouldn't reply to blogposts after a full day at a conference and being too tired :).
The 'unit of work' pattern is a misnomer: it resembles a unit which contains work. Now, in most o/r mappers, it's implemented as a session or context, the central object which knows all the mappings/meta-data and governs everything. That's a pattern LLBLGen Pro doesn't implement, totally correct.
That's not to say we don't support a unit of work pattern, we do: like I described above (which is implemented since 2004 I believe, but I have to check). Our design (let's ignore our selfservicing paradigm for now which has persistence on the entiites, like customer.Save(), some people want that) of our adapter paradigm (which uses a service object to perform actions you want, like commit a unit of work object with work :)) is a decentral design: so there's no central object which controls everything, you don't need that. Of course, if you want to collect work to be performed in one transaction, for example insert a couple of entties, delete a couple, call a proc to delete some entities etc., you can all collect that work in the unit of work object we have so that that unit contains ... work :).
You then can call Commit() on it by passing in an adapter object. E.g. one which you create using a little factory so you can decide on the fly to which db you want to commit all the work at that moment, not when the unit of work class is created, because that's perhaps somewhere in the BL tier which has no idea which db's are supported by the app.
So my post was a misunderstanding of what you wanted to say. You are totally correct: we don't support the context/session pattern as we have a design which doesn't need that. We do support the pattern to gather work in a unit, e.g. the unit of work pattern :).
Would it be more accurate to call what you do batching? Checking Fowler's definition, it looks like that was the original purpose of the UoW pattern, to batch a set of changes to the DB, which was later expanded by users to include all context/session stuff.
I guess that require a separate discussion about the different patterns, similar to state vs. strategy or decorator vs. proxy.
Unit of Work just means Unit of Work, it doesn't imply a session/context. In NPersist, there is a separate UnitOfWork class. The context object holds an instance of that, just like it holds an instance of an IdentityMap and other things.
/Mats
I would say that using a unit of work class which is used to gather work (thats its purpose) also for persistence work, e.g. all the session work is a combination of concerns actually. So the session should IMHO utilize a unitofwork class, not be one. (Like what Mats explained is IMHO better)
I've used the xyzRepository.FindXyzByAbc(...) approach in quite a few projects now, and in retrospect there is something a bit smelly about it. Good points made by Ayende and Tybor: I think the smell I was experiencing came from the bloated class interface, the business logic in finder methods (SRP violation), and the modifying of the repository class each time you need another finder method (OCP violation).
@Ayende, @El Guapo
It seems this:
FindAll<Customer>(Where.Customer.City == london);
is an example of simple specification objects, as discussed in Eric Evans's Domain Driven Design book? Might be worth El Guapo taking a look if he's interested.
@Ayende
I love this code!
FindAll<Customer>( CustomerByCityFinder.With(london).EagerLoad("Orders") );
How do I get that in my ActiveRecord project please!?
Also, whilst I'm here, can I get your unit of work class with ActiveRecordtoo? Sorry, don't mean to demand info, just brief pointers in the right direction would be much appreciated ;)
Tobin
Tobin, yes, the UoW can work with AR, you can check Exesto for the details, or ask in the rhino-tools-dev group.
CustomerByCityFinder.With(london).EagerLoad("Orders")
All you need is to create something that will return a detached criteria, AR can consume that.
Take into account that doing this in a general fashion may be a PITA, that is why I have NHQG.
Comment preview