Skip to content

JeremyLikness/ExpressionPowerTools

Repository files navigation

ExpressionPowerTools

.NET Core .NET Core Tests Generate and Publish Documentation Pack and Publish NuGet

Skip ahead to documentation

What Expression Power Tools does

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!

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

Libraries

The following libraries are available to use.

Core

NuGet Badge Tests Coverage

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.

API Documentation

Serialization

NuGet Badge Tests Coverage

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.

API Documentation

ASP.NET Core Middleware for EF Core

NuGet Badge Tests Coverage

Install-Package ExpressionPowerTools.Serialization.EFCore.AspNetCore -Version 0.9.4-alpha

Power tools for deserializing queries initiated by remote clients.

API Documentation

Http Client

NuGet Badge Tests Coverage

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.

API Documentation

Documentation Generator

The documentation generator is an internal utility that auto-generates API documentation based on comments and reflection. IT directly generates markdown. Also self-documents.

API Documentation

About

Power tools for working with IQueryable and Expression trees.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published