Skip to content

Commit 2e50852

Browse files
PascalSennmichaelstaibglen-84
authored
Added predicates to DataLoader (#7589)
Co-authored-by: Michael Staib <[email protected]> Co-authored-by: Glen <[email protected]>
1 parent fd48659 commit 2e50852

File tree

46 files changed

+1267
-74
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1267
-74
lines changed

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "9.0.100-rc.1.24452.12",
3+
"version": "9.0.100-rc.2.24474.11",
44
"rollForward": "latestMinor"
55
}
66
}

src/GreenDonut/src/Core/DataLoaderFetchContext.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Immutable;
22
using System.Diagnostics.CodeAnalysis;
3-
using GreenDonut.Projections;
3+
using GreenDonut.Predicates;
4+
using GreenDonut.Selectors;
45

56
namespace GreenDonut;
67

@@ -141,7 +142,7 @@ public TState GetStateOrDefault<TState>(string key, TState defaultValue)
141142
/// <returns>
142143
/// Returns the selector builder if it exists.
143144
/// </returns>
144-
[Experimental(Experiments.Projections)]
145+
[Experimental(Experiments.Selectors)]
145146
public ISelectorBuilder GetSelector()
146147
{
147148
if (ContextData.TryGetValue(typeof(ISelectorBuilder).FullName!, out var value)
@@ -154,4 +155,25 @@ public ISelectorBuilder GetSelector()
154155
// a new default selector builder.
155156
return new DefaultSelectorBuilder();
156157
}
158+
159+
/// <summary>
160+
/// Gets the predicate builder from the DataLoader state snapshot.
161+
/// The state builder can be used to create a predicate expression.
162+
/// </summary>
163+
/// <returns>
164+
/// Returns the predicate builder if it exists.
165+
/// </returns>
166+
[Experimental(Experiments.Predicates)]
167+
public IPredicateBuilder GetPredicate()
168+
{
169+
if (ContextData.TryGetValue(typeof(IPredicateBuilder).FullName!, out var value)
170+
&& value is DefaultPredicateBuilder casted)
171+
{
172+
return casted;
173+
}
174+
175+
// if no predicate was found we will just return
176+
// a new default predicate builder.
177+
return new DefaultPredicateBuilder();
178+
}
157179
}

src/GreenDonut/src/Core/Experiments.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ namespace GreenDonut;
22

33
internal static class Experiments
44
{
5-
public const string Projections = "GD0001";
5+
public const string Selectors = "GD0001";
6+
public const string Predicates = "GD0002";
67
}

src/GreenDonut/src/Core/Projections/ExpressionHelpers.cs renamed to src/GreenDonut/src/Core/ExpressionHelpers.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System.Linq.Expressions;
22
using System.Reflection;
33

4-
namespace GreenDonut.Projections;
4+
namespace GreenDonut;
55

66
internal static class ExpressionHelpers
77
{
@@ -16,6 +16,17 @@ public static Expression<Func<T, T>> Combine<T>(
1616
return Expression.Lambda<Func<T, T>>(combinedBody, parameter);
1717
}
1818

19+
public static Expression<Func<T, bool>> And<T>(
20+
Expression<Func<T, bool>> first,
21+
Expression<Func<T, bool>> second)
22+
{
23+
var parameter = Expression.Parameter(typeof(T), "entity");
24+
var firstBody = ReplaceParameter(first.Body, first.Parameters[0], parameter);
25+
var secondBody = ReplaceParameter(second.Body, second.Parameters[0], parameter);
26+
var combinedBody = Expression.AndAlso(firstBody, secondBody);
27+
return Expression.Lambda<Func<T, bool>>(combinedBody, parameter);
28+
}
29+
1930
private static Expression ReplaceParameter(
2031
Expression body,
2132
ParameterExpression toReplace,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Linq.Expressions;
3+
4+
namespace GreenDonut.Predicates;
5+
6+
[Experimental(Experiments.Predicates)]
7+
internal sealed class DefaultPredicateBuilder : IPredicateBuilder
8+
{
9+
private List<LambdaExpression>? _predicates;
10+
11+
/// <inheritdoc />
12+
public void Add<T>(Expression<Func<T, bool>> selector)
13+
{
14+
_predicates ??= new List<LambdaExpression>();
15+
if (!_predicates.Contains(selector))
16+
{
17+
_predicates.Add(selector);
18+
}
19+
}
20+
21+
/// <inheritdoc />
22+
public Expression<Func<T, bool>>? TryCompile<T>()
23+
{
24+
if (_predicates is null)
25+
{
26+
return null;
27+
}
28+
29+
if (_predicates.Count == 1)
30+
{
31+
return (Expression<Func<T, bool>>)_predicates[0];
32+
}
33+
34+
if (_predicates.Count == 2)
35+
{
36+
return ExpressionHelpers.And(
37+
(Expression<Func<T, bool>>)_predicates[0],
38+
(Expression<Func<T, bool>>)_predicates[1]);
39+
}
40+
41+
var expression = (Expression<Func<T, bool>>)_predicates[0];
42+
for (var i = 1; i < _predicates.Count; i++)
43+
{
44+
expression = ExpressionHelpers.And(
45+
expression,
46+
(Expression<Func<T, bool>>)_predicates[i]);
47+
}
48+
49+
return expression;
50+
}
51+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Linq.Expressions;
3+
4+
namespace GreenDonut.Predicates;
5+
6+
/// <summary>
7+
/// The predicate builder helps you create a combined predicate expression
8+
/// by adding multiple expressions together.
9+
/// </summary>
10+
[Experimental(Experiments.Predicates)]
11+
public interface IPredicateBuilder
12+
{
13+
/// <summary>
14+
/// Adds a predicate expression to the builder.
15+
/// </summary>
16+
/// <param name="selector">
17+
/// An expression that defines how to select data from the data source.
18+
/// </param>
19+
/// <typeparam name="T">
20+
/// The type of the data source.
21+
/// </typeparam>
22+
void Add<T>(Expression<Func<T, bool>> selector);
23+
24+
/// <summary>
25+
/// Combines all the added predicate expressions into one.
26+
/// Returns null if no expressions were added.
27+
/// </summary>
28+
/// <typeparam name="T">
29+
/// The type of the data source.
30+
/// </typeparam>
31+
/// <returns>
32+
/// A combined predicate expression, or null if none were added.
33+
/// </returns>
34+
Expression<Func<T, bool>>? TryCompile<T>();
35+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace GreenDonut.Predicates;
2+
3+
/// <summary>
4+
/// A predicate DataLoader is a specialized version of a DataLoader that
5+
/// selects a subset of data based on a given predicate from the original DataLoader.
6+
/// The data retrieved by this DataLoader is not shared with other DataLoaders and
7+
/// remains isolated within this instance.
8+
/// </summary>
9+
/// <typeparam name="TKey">
10+
/// The type of the key.
11+
/// </typeparam>
12+
/// <typeparam name="TValue">
13+
/// The type of the value.
14+
/// </typeparam>
15+
public interface IPredicateDataLoader<in TKey, TValue>
16+
: IDataLoader<TKey, TValue>
17+
where TKey : notnull
18+
{
19+
/// <summary>
20+
/// Gets the root DataLoader instance from which this instance was derived.
21+
/// </summary>
22+
IDataLoader<TKey, TValue> Root { get; }
23+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace GreenDonut.Predicates;
2+
3+
internal sealed class PredicateDataLoader<TKey, TValue>
4+
: DataLoaderBase<TKey, TValue>
5+
, IPredicateDataLoader<TKey, TValue>
6+
where TKey : notnull
7+
{
8+
private readonly DataLoaderBase<TKey, TValue> _root;
9+
10+
public PredicateDataLoader(
11+
DataLoaderBase<TKey, TValue> root,
12+
string predicateKey)
13+
: base(root.BatchScheduler, root.Options)
14+
{
15+
_root = root;
16+
CacheKeyType = $"{root.CacheKeyType}:{predicateKey}";
17+
ContextData = root.ContextData;
18+
}
19+
20+
public IDataLoader<TKey, TValue> Root => _root;
21+
22+
protected internal override string CacheKeyType { get; }
23+
24+
protected override bool AllowCachePropagation => false;
25+
26+
protected override bool AllowBranching => false;
27+
28+
protected internal override ValueTask FetchAsync(
29+
IReadOnlyList<TKey> keys,
30+
Memory<Result<TValue?>> results,
31+
DataLoaderFetchContext<TValue> context,
32+
CancellationToken cancellationToken)
33+
=> _root.FetchAsync(keys, results, context, cancellationToken);
34+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Linq.Expressions;
3+
4+
namespace GreenDonut.Predicates;
5+
6+
/// <summary>
7+
/// Data loader extensions for predicates.
8+
/// </summary>
9+
[Experimental(Experiments.Predicates)]
10+
public static class PredicateDataLoaderExtensions
11+
{
12+
/// <summary>
13+
/// Branches a DataLoader and applies a predicate to filter the data.
14+
/// </summary>
15+
/// <param name="dataLoader">
16+
/// The DataLoader to branch.
17+
/// </param>
18+
/// <param name="predicate">
19+
/// The data predicate.
20+
/// </param>
21+
/// <typeparam name="TKey">
22+
/// The key type.
23+
/// </typeparam>
24+
/// <typeparam name="TValue">
25+
/// The value type.
26+
/// </typeparam>
27+
/// <returns>
28+
/// Returns a branched DataLoader with the predicate applied.
29+
/// </returns>
30+
/// <exception cref="ArgumentNullException">
31+
/// Throws if <paramref name="dataLoader"/> is <c>null</c>.
32+
/// </exception>
33+
public static IDataLoader<TKey, TValue> Where<TKey, TValue>(
34+
this IDataLoader<TKey, TValue> dataLoader,
35+
Expression<Func<TValue, bool>>? predicate)
36+
where TKey : notnull
37+
{
38+
if (dataLoader is null)
39+
{
40+
throw new ArgumentNullException(nameof(dataLoader));
41+
}
42+
43+
if (predicate is null)
44+
{
45+
return dataLoader;
46+
}
47+
48+
if (dataLoader.ContextData.TryGetValue(typeof(IPredicateBuilder).FullName!, out var value))
49+
{
50+
var context = (DefaultPredicateBuilder)value!;
51+
context.Add(predicate);
52+
return dataLoader;
53+
}
54+
55+
var branchKey = predicate.ToString();
56+
return (IDataLoader<TKey, TValue>)dataLoader.Branch(branchKey, CreateBranch, predicate);
57+
58+
static IDataLoader CreateBranch(
59+
string key,
60+
IDataLoader<TKey, TValue> dataLoader,
61+
Expression<Func<TValue, bool>> predicate)
62+
{
63+
var branch = new PredicateDataLoader<TKey, TValue>(
64+
(DataLoaderBase<TKey, TValue>)dataLoader,
65+
key);
66+
var context = new DefaultPredicateBuilder();
67+
branch.ContextData =
68+
branch.ContextData.SetItem(typeof(IPredicateBuilder).FullName!, context);
69+
context.Add(predicate);
70+
return branch;
71+
}
72+
}
73+
74+
/// <summary>
75+
/// Applies the predicate from the DataLoader state to a queryable.
76+
/// </summary>
77+
/// <param name="query">
78+
/// The queryable to apply the predicate to.
79+
/// </param>
80+
/// <param name="builder">
81+
/// The predicate builder.
82+
/// </param>
83+
/// <typeparam name="T">
84+
/// The queryable type.
85+
/// </typeparam>
86+
/// <returns>
87+
/// Returns a query with the predicate applied, ready to fetch data with the key.
88+
/// </returns>
89+
/// <exception cref="ArgumentNullException">
90+
/// Throws if <paramref name="query"/> is <c>null</c>.
91+
/// </exception>
92+
public static IQueryable<T> Where<T>(
93+
this IQueryable<T> query,
94+
IPredicateBuilder builder)
95+
{
96+
if (query is null)
97+
{
98+
throw new ArgumentNullException(nameof(query));
99+
}
100+
101+
if (builder is null)
102+
{
103+
throw new ArgumentNullException(nameof(builder));
104+
}
105+
106+
var predicate = builder.TryCompile<T>();
107+
108+
if (predicate is not null)
109+
{
110+
query = query.Where(predicate);
111+
}
112+
113+
return query;
114+
}
115+
}

src/GreenDonut/src/Core/Projections/DefaultSelectorBuilder.cs renamed to src/GreenDonut/src/Core/Selectors/DefaultSelectorBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using System.Diagnostics.CodeAnalysis;
22
using System.Linq.Expressions;
33

4-
namespace GreenDonut.Projections;
4+
namespace GreenDonut.Selectors;
55

66
/// <summary>
77
/// A default implementation of the <see cref="ISelectorBuilder"/>.
88
/// </summary>
9-
[Experimental(Experiments.Projections)]
9+
[Experimental(Experiments.Selectors)]
1010
public sealed class DefaultSelectorBuilder : ISelectorBuilder
1111
{
1212
private List<LambdaExpression>? _selectors;

0 commit comments

Comments
 (0)