I’ve worked on quite a lot of projects over the years, with many different teams, and one of the questions that keeps coming back to me over and over with a high degree of regularity is how to load a C# lambda from a string - for example from a configuration file.
This is not surprising, because being able to do that can give you a tremendous amount of flexibility in your code, as it would (for the lack of better word) unlock the possibility to alter business logic from the configuration level, without having to recompile and redeploy your application.
Historically, this has been possible but also quite a painful task. Today I wanted to show you a remarkably simple solution to this problem - with the help of the Roslyn compiler Nuget packages.
The problem π
Let’s use an extremely simple piece code to illustrate the problem. Imagine we have a class Album and some collection of Album objects.
public class Album
{
public int Quantity { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
}
var albums = new List<Album>
{
new Album { Quantity = 10, Artist = "Betontod", Title = "Revolution" },
new Album { Quantity = 50, Artist = "The Dangerous Summer", Title = "The Dangerous Summer" },
new Album { Quantity = 200, Artist = "Depeche Mode", Title = "Spirit" },
};
We’d like to dynamically provide a lambda expression to filter the list of albums. This could - for example - be used to dynamically determine which items should be sold at a discounted price. In our case, let’s assume the following - if we have more than 100 albums in stock, let’s sell them at a discount.
In other words, the code we’ll be trying to achieve is as follows:
var discountedAlbums = albums.Where(x => x.Quantity > 100);
Of course the tricky piece is that the filtering lambda expression is supposed to come from configuration (so from a string).
var discountFilter = "album => album.Quantity > 100";
// the real challenge, eh?
Func<Album, bool> discountFilterExpression = ?????;
var discountedAlbums = albums.Where(discountFilterExpression);
Building LINQ Expressions by hand π
The traditional way to approach this problem, would be to try an build up a dynamic filter expression by hand. The following code creates the condition we need dynamically:
var parameter = Expression.Parameter(typeof(Album), "album");
var comparison = Expression.GreaterThan(Expression.Property(parameter, Type.GetType("ConsoleApp6.Album").GetProperty("Quantity")), Expression.Constant(100));
Func<Album, bool> discountFilterExpression = Expression.Lambda<Func<Album, bool>>(comparison, parameter).Compile();
var discountedAlbums = albums.Where(discountFilterExpression);
//hooray now we have discountedAlbums!
Of course this doesn’t even come close to solving the problem yet. All we have done so far, is we managed to generate the condition dynamically. It does work indeed, but we have not yet addressed the problem of how to provide the logical input from a raw string.
Translating our original string would require disassembling it, understanding the type of condition(s) being expressed there and mapping onto the Expression builder API and then ultimately compiling it. All of this is - as one could easily imagine - really complicated and gets extremely difficult very quickly indeed.
The point of this blog post though is to prevent you from going down that road so we will stop here. Instead of trying to build your own expression evaluator, we can forget the Expression builder API altogether and just use Roslyn to evaluate our string expression straight up.
From string to lambda with Roslyn π
One of the little known gems of Roslyn is that as part of its Scripting API, it ships with an excellent expression evaluator. And this is what we will be using - through the Roslyn scripting packages from Nuget - to solve our problem in almost literally a single line of code.
Of course if you follow this blog, or my Twitter, or my Github, you probably know that I’m deeply involved in the C# scripting community, so if you have any questions about that area I’m always happy to help.
The Nuget package to reference is called Microsoft.CodeAnalysis.CSharp.Scripting.
Once you have it referenced in the project, you can use the C# scripting engine to evaluate our delegate, represented as a raw string, into a real instance of Func<Album, bool> lambda that we require to filter our albums. The scripting APIs of Roslyn exposes a static method CSharpScript.EvaluateAsync
The usage is shown below.
var discountFilter = "album => album.Quantity > 0";
var options = ScriptOptions.Default.AddReferences(typeof(Album).Assembly);
Func<Album, bool> discountFilterExpression = await CSharpScript.EvaluateAsync<Func<Album, bool>>(discountFilter, options);
var discountedAlbums = albums.Where(discountFilterExpression);
//hooray now we have discountedAlbums!
This has remarkable simplicity in it, doesn’t it?
The only thing we need to make sure of, is to create a ScriptOptions instance which will contain references to all of the assemblies our compiled delegate needs access to. In our case, it’s only the assembly containing the Album class, so our current assembly, but depending on the complexity of the expression we are compiling there could of course be more of them (i.e. imagine wanting to process a reference to a Claim there, that would need a reference to System.Security.Claims.dll and so on). By default, the scripting engine in Roslyn only references the assembly containing the object type (which, by the way, can mean different things on different runtimes, but that’s a separate story).
On a related note, the ScriptOptions expose a method AddImports, which allows us to import the necessary namespace on which the compilation of the expression hinges.
I think it’s no hard to imagine taking this technique and applying it on a larger scale in real life scenarios i.e. in ASP.NET Core application, where the said expression would be read via the ASP.NET Core Configuration system - from appsettings.json or environment variables.
Note that the compilation of such expression is not really cheap, but then dynamic code emitting never is. Plus, depending on how you use that (perhaps at application startup, perhaps in singleton objects, perhaps on a background thread and so on) it may be possible to mitigate those effects.
In case you are interested, the source code for this post is available here on Github.