This post is really a continuation from my last post on using a method that takes an Action to address cross cutting concerns, like logging, without having to go to a full blown AOP implementation.
Someone mentioned in the comments that it wasn’t very clear exactly what was going on with the final code. I tend to agree that this:
LogOnError(_riceCooker.Cook);
… is not very clear.
Really, there are two problems with this code that I can see.
- It is not clear what this is going to do or whether or not LogOnError or Cook is the method we are concerned about.
- It’s not very self-discoverable at all. If we had a library of useful wrapper methods like these, we wouldn’t have a good intellisense way to know what they are.
I can solve both of those issues, but doing so starts to move us into a weird zone where I am not quite sure I feel comfortable. But, nevertheless, in the name of science…
Let’s start backwards
Liking fluent interfaces, here is the kind of syntax that I would prefer to be able to use:
Wrapper.Wrap(_riceCooker.Cook).With.LogOnError();
It is a little bit longer syntax, but I like it for a few reasons:
- It clearly indicates what is going on here. We are wrapping a method call using a wrapper. We are wrapping with a method called LogOnError.
- You get intellisense all the way. The correct implementation of this, should let me Type With + ‘.’ and then see a list of all the possible wrapping methods I have implemented. This makes the wrapping set of methods self-discoverable.
I really like the idea of being able to easily change the functionality of the wrapping just by changing the last part of the line. For example, if we had implemented a wrapping method that was LogAndAbortOnError(), we could change our code to use that pretty easily.
Wrapper.Wrap(_riceCooker.Cook).With.LogAndAbortOnError();
If we implement this correctly, intellisense will give us our options.
Making it so
Creating a fluent syntax in C# can often involve quite a bit of magic and voodoo. I always like to gather my reagents before embarking on such a journey.
So grab a live chicken, a stapler, and a sharp knife and let’s go!
First step, let’s simplify this. The With is nice, but it is just for flow, we don’t really need it. So let’s figure out how to implement our syntax without the With and add it in afterwards.
Wrapper.Wrap(_riceCooker.Cook).LogOnError();
First the easy way.
- Create a static Wrapper class with a Wrap method that takes an Action and returns an Action. (We’ll use this to convert whatever we pass in to an Action, so that we can use a Lambda expression or any method call there.)
- Create a static extension method that operates on an Action. Call it LogOnError.
Not too bad. Not a large amount of magic going on here. Just using an extension method.
But, we already have a problem. Using a plain old Action is going to give us too many choices in the intellisense drop down. It could make it hard to know what our real options are and when we try and add the With syntax later we will need to use a property off of an object we return from the Wrap method.
Making it better
We can fix this by actually wrapping the Action with a custom type that we can add our methods to.
Instead of Wrap returning an Action, it will return a WrappedAction.
Looking better. Now when we put a ‘.’ at the end of our Wrap call we only see LogOnError as an option.
We can be sure now that if we create an extension method for a WrappedAction, we will make sure that method is self-discoverable. Before, the generic Action extension method could make our method show up places that we don’t want it to and can get lost in the other methods on Action.
Making it done
The last thing we need to do is add the With.
Ideally, when we hit the ‘.’ on the end of the Wrap method, we want to see With as an option. When we hit the ‘.’ on the end of the With property, we want to see LogOnError as an option.
In order to accomplish this we need to:
- Add a With property to the WrappedAction.
- Have the With property be of a new type (WrappedActionTarget) so that we can add our extension methods for that new type.
- Change the extension method to operate on the new type.
Here is what we end up with:
Now we can use the syntax of:
Wrapper.Wrap(_riceCooker.Cook).With.LogOnError();
We can move that LogOnError method out to another class, or create new extension methods somewhere else. I just put it in there to avoid creating another class.
Is this really practical?
I don’t know. To be honest, I was playing around with creating extension methods that work on Actions and I came up with this way to use them.
I could see making a wrapping library that had different kinds of ways you would wrap method calls built into it. It could allow you to specify how you log in a configuration and then you would get all of this common stuff automatically.
Even if it is not practical, it’s pretty fun, and it demonstrates the power of Action, or rather functional programming in general.