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

lucaslorentz / auto-compute / 11758855041

09 Nov 2024 07:22PM UTC coverage: 77.28% (+0.07%) from 77.208%
11758855041

push

github

lucaslorentz
Add initial support for computed navigations and observers

166 of 431 new or added lines in 25 files covered. (38.52%)

41 existing lines in 7 files now uncovered.

1466 of 1897 relevant lines covered (77.28%)

109.91 hits per line

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

98.29
/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
        entityContext.ValidateAll();
60✔
145

146
        return entityContext;
60✔
147
    }
148

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

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

160
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
21✔
161

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

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

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

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

186
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
21✔
187

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

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

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

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

212
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
9✔
213

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

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

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

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

238
        newBody = PrepareComputedOutputExpression(computedExpression.ReturnType, newBody);
9✔
239

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

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

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

263
        return computedExpression;
30✔
264
    }
265

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

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

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

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

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

312
        return value;
217✔
313
    }
314
}
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