The required infrastructure frees you from infrastructure decisions
One of the things that I try to do whenever I design a system is to have just enough infrastructure in place to make sure that I don’t have to make any infrastructure decisions.
What does this means? It means that the code that actually does the interesting things, the code that actually provide a business value, isn’t really aware of the context that it is running on. It can make assumptions about the state of the world in which it runs.
For example, let us take this controller, it inherits from RavenController, and it assumes that when it runs, it will have a documentSession instance primed & ready for it:
public ActionResult Validate(Guid key) { var license = documentSession.Query<License>() .Customize(x=>x.Include<License>(y=>y.OrderId)) .Where(x=>x.Key == key) .FirstOrDefault(); if (license == null) return HttpNotFound(); var orderMessage = documentSession.Load<OrderMessage>(license.OrderId); switch (orderMessage.OrderType) { case "Perpetual": return Json(new {License = "Valid"}); } return HttpNotFound(); }
What is important about that is that it isn’t actively doing something, it just assumes that it is there.
Why is that important? It is important because instead of going ahead and creating the session, we assume it is there, we are unaware of how it got there, who did it, etc. If we wanted to execute this code in a unit test, all we would need to do is to plug a session in, and then execute the code.
But that is just part of this. Let us look at a more complex scenario, the processing of an order:
public class ProcessOrderMessageTask : BackgroundTask { public string OrderId { get; set; } public override void Execute() { var orderMessage = documentSession.Load<OrderMessage>(OrderId); var license = new License { Key = Guid.NewGuid(), OrderId = orderMessage.Id }; documentSession.Store(license); TaskExecuter.ExecuteLater(new SendEmailTask { From = "orders@tekpub.com", To = orderMessage.Email, Subject = "Congrats on your new tekpub thingie", Template = "NewOrder", ViewContext = new { orderMessage.OrderId, orderMessage.Name, orderMessage.Amount, LiceseKey = license.Key } }); orderMessage.Handled = true; } public override string ToString() { return string.Format("OrderId: {0}", OrderId); } }
There are several things that are happening here:
- Again, we are assuming that when we run, we are actually going to run with the documentSession set to a valid value.
- Note that we have the notion of TaskExecuter.ExecuteLater.
That is actually quite an important aspect of infrastructure ignorance.
ExecuteLater is just that, a promise to execute something later, it doesn’t specify how or when. In fact, it takes this a bit farther than that. Only if the current operation will complete successfully will this task run. So if we failed to commit the transaction, the SendemailTask won’t get executed. Those sort of details can only happen when we let go of trying to control everything and let the infrastructure support us.
During the Full Throttle episode in which this code was written, we moved the task execution from a background thread to a different process, and nothing in the code had to change, because the only thing that we had to do is to setup the environment that the code expected.
I really like this sort of behavior, because it frees the code from all the nasty infrastructure concerns, at the same time as it gives you an incredibly powerful platform to work on.
And just for fun, this result in a system that you can play with with great ease, you don’t have to worry about your choice. You can modify them at any time, because you don’t really care how those things are done.
Comments
Separation of concerns - that's how I'd call it. Speaking about unit tests of 'derived' classes, I do prefer testing code based on composition rather than derivation and in the Validate method test, I'd simply take the session as parameter (custom binder for document session will take care of binding it to the param), of course if the session is not used 'much' in the other methods.
Nice! the question is of course ;-). How did you implement the transaction support in the ExecuteLater method?
Loving your posts.
I dont think this is the right place to post this, but keep getting the below silverlight error when creating and editing a document in ravendb management studio. Any ideas? Can see and create sample data, just cant edit the actual document. Just upgraded silverlight.
[Arg_TargetInvocationException] Arguments: Debugging resource strings are unavailable. Often the key and arguments provide sufficient information to diagnose the problem. See http://go.microsoft.com/fwlink/?linkid=106663&Version=4.0.60831.0&File=mscorlib.dll&Key=Arg_TargetInvocationException.... more info available upon request
Thanks and apologize for posting here, but I always see how you reply to commenters.
Tyrone
While I agree with this, and use it myself, I am confused on how the BackgroundTask actually creates a document session in a way that supports dependency injection with coded unit/integration testing.
Usually I would pass a document session into the constructor, which (unfortunately) then requires me to also pass it into the constructor for all sub-classes as well. I assume that since your ProcessOrderMessageTask does not contain an explicit constructor that the BackgroundTask talks to the IoC engine directly to retrieve the document session? If so that seems like a lot of work required to set up coded unit and integration tests.
@Matthew, if you look at his RacoonBlog project, he uses property injection to set IDocumentSession. He has an action filter that sets it.
So I imagine he's doing something similar here. His main point is that our code shouldn't care. We should just assume it'll be set somewhere along the way.
If we're unit testing, then new up the controller, set document session and call our code. In a unit test, we can control the context.
I'm confused by this comment:
"What is important about that is that it isn’t actively doing something, it just assumes that it is there."
Correct me if I'm wrong, but the Controller is creating the session, right? Just because it's in the base class doesn't mean that it's not there.
@Matthew: It seems handling the session for tasks is done by the TaskExecuter: https://github.com/ayende/TekPub.Profiler.BackOffice/blob/master/TekPub.Profiler.BackOffice/Tasks/TaskExecuter.cs So that class would be part of the infrastructure. As far as I can see this makes it easy to test background tasks.
@Kiliman: Ah I hadn't thought about using property injection rather than constructor injection.
@Marcus, That is the whole point though. You could be using the base class to create the session. Or it could be done through IoC property injection. Or it could be a global action filter. Or it might be an action filter on the class. Or it might be something else entirely. Any of these is valid. And we don't care which one is used. It is an infrastructure concern and our business code shouldn't be worried about it.
People .. .people!!! None of you guys are asking the right question(s).
=> What is Ayende doing with RobCon + Tekpub + Code !! :) https://github.com/ayende/TekPub.Profiler.BackOffice/tree/master/TekPub.Profiler.BackOffice/Tasks
My spidersense is tingling and it's felling good .. like when i spread awesomesauce over my latest RavenDB project.
Is this one of Rob's secret new series, to be coming out in the future?
May world-domination finally begin.
@JustinA - No it's one of Rob's already existing productions: http://tekpub.com/productions/ft_triage_oren - and very good it is too.
Marco, That really depend on how you are implementing your transactions. In that case, we hooked into the post tx event
Tyrone, Known issue, was fixed today, you can download build 531 to get it.
Matthew, I tend to use a publicly settable property, who ever is executing the task can provide the session then
Marcus, It isn't your controller. It is in the base class infrastructure. I made it the OnActionExecuted and OnActionExecuting for simplicity sake, but it could be a separate action filter just as easily
@Ayende: Out of curiosity do you tend to use properties for all dependency injected objects or just for deep infrastructure objects (such as sessions or db contexts)?
Matthew, I don't have hard & fast rules.
Comment preview