Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ The aggregate package deliberately uses one canonical invocation vocabulary. Rep

See the [migration guide](https://github.com/Raffinert/Raffinert.Expressions/blob/main/docs/migration.md) for details.

## Examples

- [LINQKit comparison](examples/LinqKitComparison/README.md) — the LINQKit README scenarios implemented three
ways in a .NET 10 / EF Core 10 console app: plain EF Core lambdas, LINQKit, and Raffinert.Expressions.

## Useful reading

Expression composition and reusable query logic:
Expand Down
3 changes: 3 additions & 0 deletions Raffinert.Expressions.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<Solution>
<Folder Name="/examples/">
<Project Path="examples/LinqKitComparison/LinqKitComparison.csproj" />
</Folder>
<Project Path="src/Raffinert.Expressions/Raffinert.Expressions.csproj" />
<Project Path="src/Raffinert.Expressions.QuerySyntax/Raffinert.Expressions.QuerySyntax.csproj" />
<Project Path="tests/Raffinert.Expressions.IntegrationTests/Raffinert.Expressions.IntegrationTests.csproj" />
Expand Down
127 changes: 127 additions & 0 deletions examples/LinqKitComparison/ComparisonRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using Microsoft.EntityFrameworkCore;

namespace LinqKitComparison;

public static class ComparisonRunner
{
public static async Task RunAsync(ExampleDbContext db, bool showSql)
{
const decimal minimumPrice = 1_000m;
var asOf = new DateTime(2026, 8, 30);
var recentSaleCutoff = asOf.AddDays(-30);

await CompareAsync(
"Expression predicate inside a navigation collection",
PureDotNetExamples.CustomersWithQualifyingNavigationPurchase(db, minimumPrice),
LinqKitExamples.CustomersWithQualifyingNavigationPurchase(db, minimumPrice),
RaffinertExamples.CustomersWithQualifyingNavigationPurchase(db, minimumPrice),
showSql);

await CompareAsync(
"Expression predicate inside an ad-hoc correlated subquery",
PureDotNetExamples.CustomersWithQualifyingAdHocPurchase(db, minimumPrice),
LinqKitExamples.CustomersWithQualifyingAdHocPurchase(db, minimumPrice),
RaffinertExamples.CustomersWithQualifyingAdHocPurchase(db, minimumPrice),
showSql);

await CompareAsync(
"Combining reusable purchase criteria",
PureDotNetExamples.CombinedPurchaseCriteria(db, minimumPrice),
LinqKitExamples.CombinedPurchaseCriteria(db, minimumPrice),
RaffinertExamples.CombinedPurchaseCriteria(db, minimumPrice),
showSql);

await CompareAsync(
"Dynamic keyword search: all keywords",
PureDotNetExamples.ProductsMatchingAllKeywords(db, "classic", "phone"),
LinqKitExamples.ProductsMatchingAllKeywords(db, "classic", "phone"),
RaffinertExamples.ProductsMatchingAllKeywords(db, "classic", "phone"),
showSql);

await CompareAsync(
"Dynamic keyword search: any keyword",
PureDotNetExamples.ProductsMatchingAnyKeyword(db, "BlackBerry", "iPhone"),
LinqKitExamples.ProductsMatchingAnyKeyword(db, "BlackBerry", "iPhone"),
RaffinertExamples.ProductsMatchingAnyKeyword(db, "BlackBerry", "iPhone"),
showSql);

await CompareAsync(
"Nested predicates",
PureDotNetExamples.NestedProductCriteria(db),
LinqKitExamples.NestedProductCriteria(db),
RaffinertExamples.NestedProductCriteria(db),
showSql);

await CompareAsync(
"Reusable predicate library",
PureDotNetExamples.ProductsFromReusableRuleScenario(db, recentSaleCutoff),
LinqKitExamples.ProductsFromReusableRuleScenario(db, recentSaleCutoff),
RaffinertExamples.ProductsFromReusableRuleScenario(db, recentSaleCutoff),
showSql);

await CompareAsync(
"Generic reusable validity predicate",
PureDotNetExamples.CurrentPriceListsStartingWith(db, asOf, "A"),
LinqKitExamples.CurrentPriceListsStartingWith(db, asOf, "A"),
RaffinertExamples.CurrentPriceListsStartingWith(db, asOf, "A"),
showSql);

await CompareAsync(
"Reusable aggregate expression",
PureDotNetExamples.DailyOrderAverages(db),
LinqKitExamples.DailyOrderAverages(db),
RaffinertExamples.DailyOrderAverages(db),
showSql);

Console.WriteLine();
Console.WriteLine("All three implementations returned identical results.");
}

private static async Task CompareAsync<T>(
string title,
IQueryable<T> pureDotNet,
IQueryable<T> linqKit,
IQueryable<T> raffinert,
bool showSql)
{
Console.WriteLine();
Console.WriteLine($"=== {title} ===");

if (showSql)
{
WriteSql("Pure .NET", pureDotNet);
WriteSql("LINQKit", linqKit);
WriteSql("Raffinert.Expressions", raffinert);
}

var pureResults = await pureDotNet.ToArrayAsync();
var linqKitResults = await linqKit.ToArrayAsync();
var raffinertResults = await raffinert.ToArrayAsync();

EnsureSame(title, pureResults, linqKitResults, raffinertResults);

Console.WriteLine($"Pure .NET : {Format(pureResults)}");
Console.WriteLine($"LINQKit : {Format(linqKitResults)}");
Console.WriteLine($"Raffinert.Expressions : {Format(raffinertResults)}");
}

private static void EnsureSame<T>(
string title,
IReadOnlyList<T> pureDotNet,
IReadOnlyList<T> linqKit,
IReadOnlyList<T> raffinert)
{
if (!pureDotNet.SequenceEqual(linqKit) || !pureDotNet.SequenceEqual(raffinert))
throw new InvalidOperationException($"The three implementations disagreed for '{title}'.");
}

private static string Format<T>(IEnumerable<T> values) =>
string.Join(", ", values.Select(value => value?.ToString() ?? "<null>"));

private static void WriteSql<T>(string label, IQueryable<T> query)
{
Console.WriteLine();
Console.WriteLine($"-- {label}");
Console.WriteLine(query.ToQueryString());
}
}
13 changes: 13 additions & 0 deletions examples/LinqKitComparison/LinqKitComparison.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LinqKit.Microsoft.EntityFrameworkCore" Version="10.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Raffinert.Expressions.QuerySyntax\Raffinert.Expressions.QuerySyntax.csproj" />
</ItemGroup>
</Project>
160 changes: 160 additions & 0 deletions examples/LinqKitComparison/LinqKitExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
using System.Linq.Expressions;
using LinqKit;

namespace LinqKitComparison;

public static class LinqKitExamples
{
public static IQueryable<string> CustomersWithQualifyingNavigationPurchase(
ExampleDbContext db,
decimal minimumPrice)
{
Expression<Func<Purchase, bool>> purchaseCriteria = purchase => purchase.Price > minimumPrice;

return db.Customers
.AsExpandable()
.Where(customer => customer.Purchases.Any(purchaseCriteria.Compile()))
.OrderBy(customer => customer.Name)
.Select(customer => customer.Name);
}

public static IQueryable<string> CustomersWithQualifyingAdHocPurchase(
ExampleDbContext db,
decimal minimumPrice)
{
Expression<Func<Purchase, bool>> purchaseCriteria = purchase => purchase.Price > minimumPrice;

return
from customer in db.Customers.AsExpandable()
where db.Purchases
.Where(purchase => purchase.CustomerId == customer.Id)
.Any(purchaseCriteria)
orderby customer.Name
select customer.Name;
}

public static IQueryable<string> CombinedPurchaseCriteria(ExampleDbContext db, decimal minimumPrice)
{
Expression<Func<Purchase, bool>> expensive = purchase => purchase.Price > minimumPrice;
Expression<Func<Purchase, bool>> combined = purchase =>
expensive.Invoke(purchase) || purchase.Description.Contains("service");

return db.Purchases
.Where(combined.Expand())
.OrderBy(purchase => purchase.Description)
.Select(purchase => purchase.Description);
}

public static IQueryable<string> ProductsMatchingAllKeywords(
ExampleDbContext db,
params string[] keywords)
{
var predicate = PredicateBuilder.New<Product>(true);

foreach (var keyword in keywords)
predicate = predicate.And(product => product.Description.Contains(keyword));

return db.Products
.AsExpandable()
.Where(predicate)
.OrderBy(product => product.Description)
.Select(product => product.Description);
}

public static IQueryable<string> ProductsMatchingAnyKeyword(
ExampleDbContext db,
params string[] keywords)
{
var predicate = PredicateBuilder.New<Product>();

foreach (var keyword in keywords)
predicate = predicate.Or(product => product.Description.Contains(keyword));

return db.Products
.AsExpandable()
.Where(predicate)
.OrderBy(product => product.Description)
.Select(product => product.Description);
}

public static IQueryable<string> NestedProductCriteria(ExampleDbContext db)
{
var descriptions = PredicateBuilder.New<Product>()
.Start(product => product.Description.Contains("foo"))
.Or(product => product.Description.Contains("far"));

var predicate = PredicateBuilder.New<Product>()
.Start(product => product.Price > 100m)
.And(product => product.Price < 1_000m)
.And(descriptions);

return db.Products
.AsExpandable()
.Where(predicate)
.OrderBy(product => product.Description)
.Select(product => product.Description);
}

public static IQueryable<string> ProductsFromReusableRuleScenario(
ExampleDbContext db,
DateTime recentSaleCutoff)
{
var newKids = ContainsInDescription("BlackBerry", "iPhone");
var classics = ContainsInDescription("Nokia", "Ericsson")
.And(IsSelling(recentSaleCutoff));

return db.Products
.AsExpandable()
.Where(newKids.Or(classics))
.OrderBy(product => product.Description)
.Select(product => product.Description);
}

public static IQueryable<string> CurrentPriceListsStartingWith(
ExampleDbContext db,
DateTime asOf,
string prefix)
{
var predicate = IsCurrent<PriceList>(asOf)
.And(priceList => priceList.Name.StartsWith(prefix));

return db.PriceLists
.AsExpandable()
.Where(predicate)
.OrderBy(priceList => priceList.Name)
.Select(priceList => priceList.Name);
}

public static IQueryable<DailyAverage> DailyOrderAverages(ExampleDbContext db)
{
Expression<Func<IQueryable<Order>, double?>> average = orders =>
orders.Average(order => (double?)order.Amount);

return
from order in db.Orders.AsExpandable()
group order by order.OrderDate into orders
orderby orders.Key
select new DailyAverage(
orders.Key,
average.Invoke(orders.AsQueryable()));
}

private static Expression<Func<TEntity, bool>> IsCurrent<TEntity>(DateTime asOf)
where TEntity : IValidFromTo =>
entity =>
(entity.ValidFrom == null || entity.ValidFrom <= asOf) &&
(entity.ValidTo == null || entity.ValidTo >= asOf);

private static Expression<Func<Product, bool>> ContainsInDescription(params string[] keywords)
{
var predicate = PredicateBuilder.New<Product>();

foreach (var keyword in keywords)
predicate = predicate.Or(product => product.Description.Contains(keyword));

return predicate;
}

private static Expression<Func<Product, bool>> IsSelling(DateTime recentSaleCutoff) =>
product => !product.Discontinued && product.LastSale > recentSaleCutoff;
}
Loading