• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lucaslorentz / auto-compute / 11755625072

09 Nov 2024 10:48AM UTC coverage: 79.945% (-0.09%) from 80.033%
11755625072

push

github

lucaslorentz
Add initial support for computed navigations and observers

159 of 359 new or added lines in 22 files covered. (44.29%)

41 existing lines in 7 files now uncovered.

1459 of 1825 relevant lines covered (79.95%)

113.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.28
/src/LLL.AutoCompute/ComputedExpressionAnalyzer.cs
1
using System.Collections;
2
using System.Linq.Expressions;
3
using System.Reflection;
4
using LLL.AutoCompute.Caching;
5
using LLL.AutoCompute.ChangesProviders;
6
using LLL.AutoCompute.EntityContextPropagators;
7
using LLL.AutoCompute.EntityContexts;
8
using LLL.AutoCompute.Internal;
9
using LLL.AutoCompute.Internal.ExpressionVisitors;
10

11
namespace LLL.AutoCompute;
12

13
public class ComputedExpressionAnalyzer<TInput>(
4✔
14
    IConcurrentCreationCache concurrentCreationCache,
4✔
15
    IEqualityComparer<Expression> expressionEqualityComparer
4✔
16
) : IComputedExpressionAnalyzer<TInput>
4✔
17
{
18
    private readonly IConcurrentCreationCache _concurrentCreationCache = concurrentCreationCache;
4✔
19
    private readonly IEqualityComparer<Expression> _expressionEqualityComparer = expressionEqualityComparer;
4✔
20
    private readonly IList<IEntityContextPropagator> _entityContextPropagators = [];
4✔
21
    private readonly HashSet<IEntityNavigationAccessLocator> _navigationAccessLocators = [];
4✔
22
    private readonly HashSet<IEntityMemberAccessLocator> _memberAccessLocators = [];
4✔
23
    private readonly IList<Func<LambdaExpression, LambdaExpression>> _expressionModifiers = [];
4✔
24
    private IEntityActionProvider<TInput>? _entityActionProvider;
25

26
    public ComputedExpressionAnalyzer<TInput> AddDefaults()
27
    {
28
        return AddEntityContextPropagator(new ChangeTrackingEntityContextPropagator())
4✔
29
            .AddEntityContextPropagator(new ConditionalEntityContextPropagator())
4✔
30
            .AddEntityContextPropagator(new ArrayEntityContextPropagator())
4✔
31
            .AddEntityContextPropagator(new ConvertEntityContextPropagator())
4✔
32
            .AddEntityContextPropagator(new LinqMethodsEntityContextPropagator())
4✔
33
            .AddEntityContextPropagator(new KeyValuePairEntityContextPropagator())
4✔
34
            .AddEntityContextPropagator(new GroupingEntityContextPropagator())
4✔
35
            .AddEntityContextPropagator(new NavigationEntityContextPropagator(_navigationAccessLocators));
4✔
36
    }
37

38
    public ComputedExpressionAnalyzer<TInput> AddEntityMemberAccessLocator(
39
        IEntityMemberAccessLocator<TInput> memberAccessLocator)
40
    {
41
        if (memberAccessLocator is IEntityNavigationAccessLocator nav)
4✔
42
            _navigationAccessLocators.Add(nav);
4✔
43

44
        _memberAccessLocators.Add(memberAccessLocator);
4✔
45
        return this;
4✔
46
    }
47

48
    public ComputedExpressionAnalyzer<TInput> AddEntityContextPropagator(
49
        IEntityContextPropagator propagator)
50
    {
51
        _entityContextPropagators.Add(propagator);
32✔
52
        return this;
32✔
53
    }
54

55
    public ComputedExpressionAnalyzer<TInput> AddExpressionModifier(Func<LambdaExpression, LambdaExpression> modifier)
56
    {
57
        _expressionModifiers.Add(modifier);
×
58
        return this;
×
59
    }
60

61
    public ComputedExpressionAnalyzer<TInput> SetEntityActionProvider(
62
        IEntityActionProvider<TInput> entityActionProvider)
63
    {
64
        _entityActionProvider = entityActionProvider;
4✔
65
        return this;
4✔
66
    }
67

68
    public IUnboundChangesProvider<TInput, TEntity, TChange> GetChangesProvider<TEntity, TValue, TChange>(
69
        Expression<Func<TEntity, TValue>> computedExpression,
70
        Expression<Func<TEntity, bool>>? filterExpression,
71
        IChangeCalculation<TValue, TChange> changeCalculation)
72
        where TEntity : class
73
    {
74
        filterExpression ??= static e => true;
83✔
75

76
        var key = (
83✔
77
            ComputedExpression: new ExpressionCacheKey(computedExpression, _expressionEqualityComparer),
83✔
78
            filterExpression: new ExpressionCacheKey(filterExpression, _expressionEqualityComparer),
83✔
79
            ChangeCalculation: changeCalculation
83✔
80
        );
83✔
81

82
        return _concurrentCreationCache.GetOrCreate(
83✔
83
            key,
83✔
84
            k => CreateChangesProvider(computedExpression, filterExpression, changeCalculation)
113✔
85
        );
83✔
86
    }
87

88
    private IUnboundChangesProvider<TInput, TEntity, TChange> CreateChangesProvider<TEntity, TValue, TChange>(
89
        Expression<Func<TEntity, TValue>> computedExpression,
90
        Expression<Func<TEntity, bool>> filterExpression,
91
        IChangeCalculation<TValue, TChange> changeCalculation)
92
        where TEntity : class
93
    {
94
        computedExpression = PrepareComputedExpression(computedExpression);
30✔
95

96
        var computedEntityContext = GetEntityContext(computedExpression, changeCalculation.IsIncremental);
30✔
97

98
        var computedValueAccessors = new ComputedValueAccessors<TInput, TEntity, TValue>(
30✔
99
            changeCalculation.IsIncremental
30✔
100
                ? GetIncrementalOriginalValueExpression(computedExpression).Compile()
30✔
101
                : GetOriginalValueExpression(computedExpression).Compile(),
30✔
102
            changeCalculation.IsIncremental
30✔
103
                ? GetIncrementalCurrentValueExpression(computedExpression).Compile()
30✔
104
                : GetCurrentValueExpression(computedExpression).Compile()
30✔
105
        );
30✔
106

107
        var filterEntityContext = GetEntityContext(filterExpression, false);
30✔
108

109
        return new UnboundChangesProvider<TInput, TEntity, TValue, TChange>(
30✔
110
            computedExpression,
30✔
111
            computedEntityContext,
30✔
112
            filterExpression.Compile(),
30✔
113
            filterEntityContext,
30✔
114
            RequireEntityActionProvider(),
30✔
115
            changeCalculation,
30✔
116
            computedValueAccessors
30✔
117
        );
30✔
118
    }
119

120
    private RootEntityContext GetEntityContext<TEntity, TValue>(
121
        Expression<Func<TEntity, TValue>> computedExpression,
122
        bool isIncremental)
123
        where TEntity : class
124
    {
125
        var analysis = new ComputedExpressionAnalysis();
60✔
126

127
        var rootEntityType = computedExpression.Parameters[0].Type;
60✔
128
        var entityContext = new RootEntityContext(rootEntityType);
60✔
129
        analysis.AddEntityContextProvider(computedExpression.Parameters[0], (key) => key == EntityContextKeys.None ? entityContext : null);
90✔
130

131
        new PropagateEntityContextsVisitor(
60✔
132
            _entityContextPropagators,
60✔
133
            analysis
60✔
134
        ).Visit(computedExpression);
60✔
135

136
        new CollectEntityMemberAccessesVisitor(
60✔
137
            analysis,
60✔
138
            _memberAccessLocators
60✔
139
        ).Visit(computedExpression);
60✔
140

141
        if (isIncremental)
60✔
142
            analysis.RunIncrementalActions();
9✔
143

144
        return entityContext;
60✔
145
    }
146

147
    private Expression<Func<TInput, IncrementalContext, TEntity, TValue>> GetOriginalValueExpression<TEntity, TValue>(
148
        Expression<Func<TEntity, TValue>> computedExpression)
149
    {
150
        var inputParameter = Expression.Parameter(typeof(TInput), "input");
21✔
151
        var incrementalContextParameter = Expression.Parameter(typeof(IncrementalContext), "incrementalContext");
21✔
152

153
        var newBody = new ReplaceMemberAccessVisitor(
21✔
154
            _memberAccessLocators,
21✔
155
            memberAccess => memberAccess.CreateOriginalValueExpression(inputParameter)
66✔
156
        ).Visit(computedExpression.Body)!;
21✔
157

158
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
21✔
159

160
        newBody = ReturnDefaultIfEntityActionExpression(
21✔
161
            inputParameter,
21✔
162
            computedExpression.Parameters.First(),
21✔
163
            newBody,
21✔
164
            EntityAction.Create);
21✔
165

166
        return (Expression<Func<TInput, IncrementalContext, TEntity, TValue>>)Expression.Lambda(newBody, [
21✔
167
            inputParameter,
21✔
168
            incrementalContextParameter,
21✔
169
            .. computedExpression.Parameters
21✔
170
        ]);
21✔
171
    }
172

173
    private Expression<Func<TInput, IncrementalContext, TEntity, TValue>> GetCurrentValueExpression<TEntity, TValue>(
174
        Expression<Func<TEntity, TValue>> computedExpression)
175
    {
176
        var inputParameter = Expression.Parameter(typeof(TInput), "input");
21✔
177
        var incrementalContextParameter = Expression.Parameter(typeof(IncrementalContext), "incrementalContext");
21✔
178

179
        var newBody = new ReplaceMemberAccessVisitor(
21✔
180
            _memberAccessLocators,
21✔
181
            memberAccess => memberAccess.CreateCurrentValueExpression(inputParameter)
66✔
182
        ).Visit(computedExpression.Body)!;
21✔
183

184
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
21✔
185

186
        newBody = ReturnDefaultIfEntityActionExpression(
21✔
187
            inputParameter,
21✔
188
            computedExpression.Parameters.First(),
21✔
189
            newBody,
21✔
190
            EntityAction.Delete);
21✔
191

192
        return (Expression<Func<TInput, IncrementalContext, TEntity, TValue>>)Expression.Lambda(newBody, [
21✔
193
            inputParameter,
21✔
194
            incrementalContextParameter,
21✔
195
            .. computedExpression.Parameters
21✔
196
        ]);
21✔
197
    }
198

199
    private Expression<Func<TInput, IncrementalContext, TEntity, TValue>> GetIncrementalOriginalValueExpression<TEntity, TValue>(
200
        Expression<Func<TEntity, TValue>> computedExpression)
201
    {
202
        var inputParameter = Expression.Parameter(typeof(TInput), "input");
9✔
203
        var incrementalContextParameter = Expression.Parameter(typeof(IncrementalContext), "incrementalContext");
9✔
204

205
        var newBody = new ReplaceMemberAccessVisitor(
9✔
206
            _memberAccessLocators,
9✔
207
            memberAccess => memberAccess.CreateIncrementalOriginalValueExpression(inputParameter, incrementalContextParameter)
36✔
208
        ).Visit(computedExpression.Body)!;
9✔
209

210
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
9✔
211

212
        newBody = ReturnDefaultIfEntityActionExpression(
9✔
213
            inputParameter,
9✔
214
            computedExpression.Parameters.First(),
9✔
215
            newBody,
9✔
216
            EntityAction.Create);
9✔
217

218
        return (Expression<Func<TInput, IncrementalContext, TEntity, TValue>>)Expression.Lambda(newBody, [
9✔
219
            inputParameter,
9✔
220
            incrementalContextParameter,
9✔
221
            .. computedExpression.Parameters
9✔
222
        ]);
9✔
223
    }
224

225
    private Expression<Func<TInput, IncrementalContext, TEntity, TValue>> GetIncrementalCurrentValueExpression<TEntity, TValue>(
226
        Expression<Func<TEntity, TValue>> computedExpression)
227
    {
228
        var inputParameter = Expression.Parameter(typeof(TInput), "input");
9✔
229
        var incrementalContextParameter = Expression.Parameter(typeof(IncrementalContext), "incrementalContext");
9✔
230

231
        var newBody = new ReplaceMemberAccessVisitor(
9✔
232
            _memberAccessLocators,
9✔
233
            memberAccess => memberAccess.CreateIncrementalCurrentValueExpression(inputParameter, incrementalContextParameter)
36✔
234
        ).Visit(computedExpression.Body)!;
9✔
235

236
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
9✔
237

238
        newBody = ReturnDefaultIfEntityActionExpression(
9✔
239
            inputParameter,
9✔
240
            computedExpression.Parameters.First(),
9✔
241
            newBody,
9✔
242
            EntityAction.Delete);
9✔
243

244
        return (Expression<Func<TInput, IncrementalContext, TEntity, TValue>>)Expression.Lambda(newBody, [
9✔
245
            inputParameter,
9✔
246
            incrementalContextParameter,
9✔
247
            .. computedExpression.Parameters
9✔
248
        ]);
9✔
249
    }
250

251
    private IEntityActionProvider<TInput> RequireEntityActionProvider()
252
    {
253
        return _entityActionProvider
90✔
254
            ?? throw new Exception("Entity Action Provider not configured");
90✔
255
    }
256
    private Expression<Func<TEntity, TValue>> PrepareComputedExpression<TEntity, TValue>(Expression<Func<TEntity, TValue>> computedExpression) where TEntity : class
257
    {
258
        foreach (var modifier in _expressionModifiers)
60✔
UNCOV
259
            computedExpression = (Expression<Func<TEntity, TValue>>)modifier(computedExpression);
×
260

261
        return computedExpression;
30✔
262
    }
263

264
    private Expression PrepareComputedOutputExpression(Type returnType, Expression body)
265
    {
266
        var prepareOutputMethod = GetType().GetMethod(
60✔
267
            nameof(PrepareComputedOutput),
60✔
268
            BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy)!
60✔
269
            .MakeGenericMethod(returnType);
60✔
270

271
        return Expression.Call(
60✔
272
            prepareOutputMethod,
60✔
273
            body);
60✔
274
    }
275

276
    private Expression ReturnDefaultIfEntityActionExpression(
277
        ParameterExpression inputParameter,
278
        ParameterExpression entityParameter,
279
        Expression expression,
280
        EntityAction entityAction)
281
    {
282
        var entityActionProvider = RequireEntityActionProvider();
60✔
283

284
        return Expression.Condition(
60✔
285
            Expression.Equal(
60✔
286
                Expression.Call(
60✔
287
                    Expression.Constant(entityActionProvider),
60✔
288
                    "GetEntityAction",
60✔
289
                    [],
60✔
290
                    inputParameter,
60✔
291
                    entityParameter
60✔
292
                ),
60✔
293
                Expression.Constant(entityAction)
60✔
294
            ),
60✔
295
            Expression.Default(expression.Type),
60✔
296
            expression
60✔
297
        );
60✔
298
    }
299

300
    private static T PrepareComputedOutput<T>(T value)
301
    {
302
        var type = typeof(T);
255✔
303
        if (type.IsConstructedGenericType
255✔
304
            && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)
255✔
305
            && value is IEnumerable enumerable)
255✔
306
        {
307
            return (T)(object)enumerable.ToArray(type.GetGenericArguments()[0]);
38✔
308
        }
309

310
        return value;
217✔
311
    }
312
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc