Expression Power Tools is an experiment I created to better understand and more easily work with expressions. The project started with a vision: the goal to be able to write a query on a client (like Blazor WebAssembly) and execute it remotely but securely on the server, like this:
var list = await DbClientContext<ThingContext>.Query(context => context.Things)
.Where(t => t.IsActive == ActiveFlag &&
EF.Functions.Like(t.Name, $"%{nameFilter}%"))
.OrderBy(t => EF.Property<DateTime>(t, nameof(Thing.Created)))
.ExecuteRemote()
.ToListAsync();
You can learn more about the implementation of this in the Blazor Web Assembly quickstart.
The central feature of the Core library is a set of extensions for working with queries and expressions. For example, the following code uses an extension to create the ConstantExpression
parameters for the BinaryExpression
then uses IExpressionEnumerable
to display the expression tree:
var expr = Expression.GreaterThan(
Expression.Constant(40),
Expression.Constant(2));
var tree = expr.AsEnumerable(); // casts to IExpressionEnumerable
var treeDisplay = tree.ToString();
The example sets treeDisplay
to:
[LogicalBinaryExpression:GreaterThan] : Boolean => (40 > 2)
[ConstantExpression:Constant] : Int32 => 40
[ConstantExpression:Constant] : Int32 => 2
See the Jupyter notebook example
A more involved example uses queries. Here is a query and its tree display:
var query = querySource
.Where(e => e.Id.StartsWith("a"))
.Select(e => new { CapturedId = e.Id })
.OrderBy(anonType => anonType.CapturedId)
.Take(5);
var tree = query.AsEnumerableExpression();
Calling tree.ToString()
will result in:
[MethodCallExpression2:Call] : IQueryable`1 => System.Collections.Generic.List`1[Submission#8+Example].Where(e => e.Id.StartsWith("a")).Select(e => new <>f__AnonymousType0#1`1(CapturedId = e.Id)).OrderBy(anonType => anonType.CapturedId).Take(5)
[MethodCallExpression2:Call] : IOrderedQueryable`1 => System.Collections.Generic.List`1[Submission#8+Example].Where(e => e.Id.StartsWith("a")).Select(e => new <>f__AnonymousType0#1`1(CapturedId = e.Id)).OrderBy(anonType => anonType.CapturedId)
[MethodCallExpression2:Call] : IQueryable`1 => System.Collections.Generic.List`1[Submission#8+Example].Where(e => e.Id.StartsWith("a")).Select(e => new <>f__AnonymousType0#1`1(CapturedId = e.Id))
[MethodCallExpression2:Call] : IQueryable`1 => System.Collections.Generic.List`1[Submission#8+Example].Where(e => e.Id.StartsWith("a"))
[ConstantExpression:Constant] : EnumerableQuery`1 => System.Collections.Generic.List`1[Submission#8+Example]
[UnaryExpression:Quote] : Expression`1 => e => e.Id.StartsWith("a")
[Expression1`1:Lambda] : Func`2 => e => e.Id.StartsWith("a")
[TypedParameterExpression:Parameter] : Example => e
[InstanceMethodCallExpression1:Call] : Boolean => e.Id.StartsWith("a")
[PropertyExpression:MemberAccess] : String => e.Id
[TypedParameterExpression:Parameter] : Example => e
[ConstantExpression:Constant] : String => "a"
[UnaryExpression:Quote] : Expression`1 => e => new <>f__AnonymousType0#1`1(CapturedId = e.Id)
[Expression1`1:Lambda] : Func`2 => e => new <>f__AnonymousType0#1`1(CapturedId = e.Id)
[TypedParameterExpression:Parameter] : Example => e
[NewExpression:New] : <>f__AnonymousType0#1`1 => new <>f__AnonymousType0#1`1(CapturedId = e.Id)
[PropertyExpression:MemberAccess] : String => e.Id
[TypedParameterExpression:Parameter] : Example => e
[UnaryExpression:Quote] : Expression`1 => anonType => anonType.CapturedId
[Expression1`1:Lambda] : Func`2 => anonType => anonType.CapturedId
[TypedParameterExpression:Parameter] : <>f__AnonymousType0#1`1 => anonType
[PropertyExpression:MemberAccess] : String => anonType.CapturedId
[TypedParameterExpression:Parameter] : <>f__AnonymousType0#1`1 => anonType
[ConstantExpression:Constant] : Int32 => 5
To get the "Take" value (results in take
with value of 5
):
var take = tree.MethodsWithName(nameof(Queryable.Take))
.SelectMany(m => m.AsEnumerable().ConstantsOfType<int>())
.First().Value; // 5
You can inspect branches of the expression tree:
var skip = query.HasFragment(q => q.Skip(5)); // false
var take5 = query.HasFragment(q => q.Take(5)); // true
var take10 = query.HasFragment(q => q.Take(10)); // false
And even make comparisons:
var query1 = querySource
.Where(e => e.Id.StartsWith("a"))
.Select(e => new { CapturedId = e.Id })
.OrderBy(anonType => anonType.CapturedId)
.Take(6);
var query2 = querySource
.Where(e => e.Id.StartsWith("b"))
.Select(e => new { CapturedId = e.Id })
.OrderBy(anonType => anonType.CapturedId)
.Take(5);
var query3 = querySource
.Where(e => e.Id.StartsWith("a"))
.Select(e => new { CapturedId = e.Id })
.OrderBy(anonType => anonType.CapturedId)
.Take(5);
var query1eq = query1.IsEquivalentTo(query); // false: Take(6) != Take(5)
var query2eq = query2.IsEquivalentTo(query); // false: StartsWith("b") != StartsWith("a")
var query3eq = query3.IsEquivalentTo(query); // true
See these examples in a Jupyter notebook
There are many more features so be sure to check out the documentation!
There are a few options for documentation, including:
Documentation Tip Because the documentation is stored in this repo, you can use the GitHub "Go to file" feature to quickly find documentation for a type or member. Just click Go to File and type the name of the method or type and look for the markdown file. You can infer the type of documentation from the extension:
.a.md
- assemblies.ns.md
- namespaces.cs.md
- types (classes).i.md
- types (interfaces).ctor.md
- constructors.m.md
- methods.prop.md
- properties
The following libraries are available to use.
Install-Package ExpressionPowerTools.Core -Version 0.9.4-alpha
Power tools for working with IQueryable
and Expression trees. Enables enumeration of the tree, comparisons ("similar" and "equivalent") and interception to take snapshots and/or mutate expressions.
Install-Package ExpressionPowerTools.Serialization -Version 0.9.4-alpha
Power tools for writing client-side queries that can be safely serialized to run on the server.
Install-Package ExpressionPowerTools.Serialization.EFCore.AspNetCore -Version 0.9.4-alpha
Power tools for deserializing queries initiated by remote clients.
Install-Package ExpressionPowerTools.Serialization.EFCore.Http -Version 0.9.4-alpha
Power tools for running remote queries over HTTP, for example a Blazor WebAssembly client.
The documentation generator is an internal utility that auto-generates API documentation based on comments and reflection. IT directly generates markdown. Also self-documents.