Article summary
In part 2 of this series I made a case that switching from Stateless Single-Responsibility Objects to delegates and static methods lets us write simple, pure functions and lets us remove a lot of boilerplate.
Nevertheless, there was still one bit of boilerplate I hadn’t yet removed. It dealt with encapsulating dependencies to a method in order to match a delegate signature for use in other parts of the application.
In our example class, which calculated the hypotenuse of a right triangle and displayed it as a string, there is the following code:
public static double CalculateHypotenuseFull(Add add, Mult mult, Sqrt sqrt, double x, double y)
{
return sqrt(add(mult(x,x), mult(y,y)));
}
public delegate double CalculateHypotenuse(double x, double y);
// don't test this function directly except through integration or system tests, conforms to delegate
public static double CalculateHypotenuseImpl(double x, double y)
{
return CalculateHypotenuseFull(AddImpl, MultImpl, SqrtImpl, x, y);
}
CalculateHypotenuseImpl
doesn’t do anything interesting. It just supplies the dependencies. Additionally, there is no way to alter at run-time which implementations of Add
, Mult
, and Sqrt
get used—they are hard-coded.
You may occasionally want to use dependency injection to alter what implementation is provided, even when you aren’t writing unit tests (for which you can use CalculateHypotenuseFull
). For example, if some code fires off an asynchronous operation, even in system tests you may want to force this operation to occur in the current thread. Alternatively, if a function interfaces with a third party library, you may also want to stub that out in system tests.
That means it’s likely you will want to register your delegate types and implementations with your dependency injection system. Doing so will make our CalculateHypotenuseImpl
look something like this:
public static double CalculateHypotenuseImpl(double x, double y)
{
var addImpl = DependencyInjectionFramework.Get();
var multImpl = DependencyInjectionFramework.Get();
var sqrtImpl = DependencyInjectionFramework.Get();
return CalculateHypotenuseFull(addImpl, multImpl, sqrtImpl, x, y);
}
Uh oh—it looks like we’ve added boilerplate code. Thankfully, the above code can be automated. That’s where my library MethodToDelegate comes in.
MethodToDelegate
MethodToDelegate is a C# library that automates the process of converting methods to delegates, including injecting and partially applying dependency arguments.
To install MethodToDelegate in your project, from the Package Manager Console type “Install-Package MethodToDelegate”.
In our example, MethodToDelegate can be used to automatically generate the preceding version of CalculateHypotenuseImpl
. Additionally it can register that generated method in dependency injection as the implementation of delegate type CalculateHypotenuse
.
Here’s what our example looks like with MethodToDelegate added:
// this is the delegate outside consumers will use
public string delegate DisplayHypotenuse(double x, double y);
public static class Pythagoras
{
// delegates inside the class are used internally but public so tests can talk about them
public delegate double Add(double x, double y);
[ToDelegate(typeof(Add))]
public static double AddImpl(double x, double y)
{
return x + y;
}
public delegate double Mult(double x, double y);
[ToDelegate(typeof(Mult))]
public static double MultImpl(double x, double y)
{
return x * y;
}
public delegate double Sqrt(double x);
[ToDelegate(typeof(Sqrt))]
public static double SqrtImpl(double x)
{
return System.Math.Sqrt(x);
}
public delegate double CalculateHypotenuse(double x, double y);
[ToDelegate(typeof(CalculateHypotenuse))] // Add, Mult, and Sqrt will be injected
public static double CalculateHypotenuseFull(Add add, Mult mult, Sqrt sqrt, double x, double y)
{
return sqrt(add(mult(x,x), mult(y,y)));
}
public delegate string DoubleToString(double value);
[ToDelegate(typeof(DoubleToString))]
public static string DoubleToStringImpl(double value)
{
return value.ToString();
}
[ToDelegate(typeof(DisplayHypotenuse))] // CalculateHypotenuse and DoubleToString will be injected
public static string DisplayHypotenuseFull(CalculateHypotenuse calcH, DoubleToString dToS, double x, double y)
{
return dToS(calcH(x, y));
}
}
With those attributes in place, you can use MethodToDelegate to turn CalculateHypotenuseFull
into a CalculateHypotenuse
, and DisplayHypotenuseFull
into a DisplayHypotenuse
. You can also register AddImpl
as the implementation of Add
to be used by dependency injection (and the same for MultImpl
/Mult
, SqrtImpl
/Sqrt
, and DoubleToStringImpl
/DoubleToString
).
How it Works
MethodToDelegate converts CalculateHypotenuseFull
into a CalculateHypotenuse
in the following way:
- The
MethodInfo
forCalculateHypotenuseFull
gets converted into theFunc<>
signature that matches its arguments. In this case that isFunc<Add,Mult,Sqrt,double,double,double>
. - MethodToDelegate determines how many arguments are dependencies by taking (
MethodInfo
arguments count) – (target delegate type arguments count) = N. The first N arguments are taken to be dependencies and supplied by dependency injection. - The supplied arguments are partially applied, so in our case
Func<Add,Mult,Sqrt,double,double,double>.Apply(AddImpl,MultImpl,SqrtImpl)
returns aFunc<double,double,double>
. - The output func is converted into the desired delegate; in our case
Func<double,double,double>
becomes aCalculateHypotenuse
.
A few notes:
- MethodToDelegate exposes the partial application functions on
Func<>
andAction<>
if you want to use them for other purposes in your code. - The performance of the output delegate is almost identical to our initial version where dependencies were provided manually.
Wiring
With your ToDelegate
attributes in place, you can hook up MethodToDelegate to your dependency injection framework. To avoid worrying about the order in which you register delegates/methods and their dependencies, you will want to bind the delegate type to a callback. That way all your delegates can be registered before any one of them is requested.
Here’s an example of hooking up MethodToDelegate to dependency injection:
var classTypesToRegister = new [] { typeof(Pythagoras) }; // in a larger application, this list is probably automatically generated
foreach (var delegateInfo in classTypesToRegister.SelectMany(DelegateHelper.GetDelegateTypesAndMethod))
{
// This initial work can and should be done before making a request for the delegate type, as it involves reflection:
var buildInfo = DelegateHelper.CreateBuildInfo(delegateInfo);
// Now use your dependency injection framework to supply the delegate type. I assume to basic operation for your dependency framework:
// 1) DependencyInjectionFramework.Bind(Type, Func
If you are using Ninject, you can include MethodToDelegate.Ninject to provide access to a Ninject extension method which lets you wire up any delegate type to any MethodInfo (although normally you would want to retrieve these delegate types and MethodInfo
s based on ToDelegate
attributes using DelegateHelper
).
// inside a NinjectModule...
Bind(typeof(SomeDelegateType)).ToPartiallyAppliedMethodInfo(someMethodInfo);
If you’d like to see some more examples of using MethodToDelegate (and delegate-based application composition in general) you can look at the tests in the source for MethodToDelegate or MethodToDelegate.Ninject.
Take Me to Func-y Town
I’m really excited about using delegates and pure static functions to compose applications. Unfortunately, changing to this new approach requires a bit too much refactoring for my existing projects. But I am stoked to give this approach a try on my next C# project.
If you’re starting a new C# project and interested in this approach, I’d encourage you to try and beat me to the punch. And give MethodToDelegate a try too. Happy (slightly more functional) coding!
This is the third post in a series on functional C# application composition: