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

lucaslorentz / auto-compute / 23084514956

14 Mar 2026 08:43AM UTC coverage: 59.695% (-20.4%) from 80.097%
23084514956

push

github

web-flow
Merge pull request #19 from lucaslorentz/feature/migration-backfill

Automatic backfill SQL generation in migrations

21 of 148 new or added lines in 8 files covered. (14.19%)

6 existing lines in 2 files now uncovered.

1995 of 3342 relevant lines covered (59.69%)

663.35 hits per line

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

0.0
/src/LLL.AutoCompute.EFCore/Internal/AutoComputeMigrationsModelDiffer.cs
1
#pragma warning disable EF1001
2

3
using System.Linq.Expressions;
4
using System.Reflection;
5
using LLL.AutoCompute.EFCore.Metadata.Internal;
6
using Microsoft.EntityFrameworkCore;
7
using Microsoft.EntityFrameworkCore.Infrastructure;
8
using Microsoft.EntityFrameworkCore.Metadata;
9
using Microsoft.EntityFrameworkCore.Migrations;
10
using Microsoft.EntityFrameworkCore.Migrations.Internal;
11
using Microsoft.EntityFrameworkCore.Migrations.Operations;
12
using Microsoft.EntityFrameworkCore.Query;
13
using Microsoft.EntityFrameworkCore.Storage;
14
using Microsoft.EntityFrameworkCore.Update;
15
using Microsoft.EntityFrameworkCore.Update.Internal;
16

17
namespace LLL.AutoCompute.EFCore.Internal;
18

19
public class AutoComputeMigrationsModelDiffer(
20
    IRelationalTypeMappingSource typeMappingSource,
21
    IMigrationsAnnotationProvider migrationsAnnotationProvider,
22
#if NET9_0_OR_GREATER
23
    IRelationalAnnotationProvider relationalAnnotationProvider,
24
#endif
25
    IRowIdentityMapFactory rowIdentityMapFactory,
26
    CommandBatchPreparerDependencies commandBatchPreparerDependencies,
27
    ICurrentDbContext currentDbContext
NEW
28
) : MigrationsModelDiffer(
×
NEW
29
    typeMappingSource,
×
NEW
30
    migrationsAnnotationProvider,
×
NEW
31
#if NET9_0_OR_GREATER
×
NEW
32
    relationalAnnotationProvider,
×
NEW
33
#endif
×
NEW
34
    rowIdentityMapFactory,
×
NEW
35
    commandBatchPreparerDependencies)
×
36
{
37
    public override IReadOnlyList<MigrationOperation> GetDifferences(
38
        IRelationalModel? source, IRelationalModel? target)
39
    {
NEW
40
        var operations = base.GetDifferences(source, target).ToList();
×
41

NEW
42
        if (target == null || DesignTimeComputedStore.Factories == null || DesignTimeComputedStore.Factories.Count == 0)
×
NEW
43
            return operations;
×
44

NEW
45
        var dbContext = currentDbContext.Context;
×
46

47
        // Create computed members from stored factories (model is finalized now)
NEW
48
        var analyzer = dbContext.Model.GetComputedExpressionAnalyzerOrThrow();
×
NEW
49
        var computedMembers = new List<(ComputedMember Member, IProperty Property)>();
×
NEW
50
        foreach (var (property, factory) in DesignTimeComputedStore.Factories)
×
51
        {
52
            try
53
            {
NEW
54
                var computed = factory(analyzer, property);
×
NEW
55
                computedMembers.Add((computed, property));
×
56
            }
NEW
57
            catch (Exception ex)
×
58
            {
NEW
59
                Console.WriteLine($"[AutoComputeMigrationsModelDiffer] Failed to create computed member for {property.DeclaringType.ShortName()}.{property.Name}: {ex.Message}");
×
60
            }
61
        }
62

NEW
63
        foreach (var (computed, property) in computedMembers)
×
64
        {
NEW
65
            var newHash = property.FindAnnotation(AutoComputeAnnotationNames.Hash)?.Value as string;
×
NEW
66
            if (newHash == null)
×
67
                continue;
68

NEW
69
            var (oldEntityExists, oldPropertyExists, oldHash) = FindOldHash(source, property);
×
NEW
70
            if (newHash == oldHash)
×
71
                continue;
72

73
            // Property exists in old snapshot but has no hash — first time tracking, assume consistent
NEW
74
            if (oldPropertyExists && oldHash == null)
×
75
                continue;
76

77
            // Entity type is new — no existing data to backfill
NEW
78
            if (!oldEntityExists)
×
79
                continue;
80

81
            try
82
            {
NEW
83
                var sql = CaptureExecuteUpdateSql(dbContext, computed, property);
×
NEW
84
                operations.Add(new SqlOperation { Sql = sql, SuppressTransaction = true });
×
85
            }
NEW
86
            catch (Exception ex)
×
87
            {
NEW
88
                var inner = ex is TargetInvocationException { InnerException: { } ie } ? ie : ex;
×
NEW
89
                var reason = inner.Message.Contains("could not be translated")
×
NEW
90
                    ? "expression uses navigation properties not supported by ExecuteUpdate"
×
NEW
91
                    : inner.Message;
×
NEW
92
                operations.Add(new SqlOperation
×
NEW
93
                {
×
NEW
94
                    Sql = $"-- AUTO-COMPUTE BACKFILL: {property.DeclaringType.ShortName()}.{property.Name}\n" +
×
NEW
95
                          $"-- Could not auto-generate UPDATE SQL: {reason}\n" +
×
NEW
96
                          $"-- Please add the UPDATE SQL manually."
×
NEW
97
                });
×
98
            }
99
        }
100

101
        // Cleanup static state
NEW
102
        DesignTimeComputedStore.Factories = null;
×
103

NEW
104
        return operations;
×
105
    }
106

107
    private static (bool EntityExists, bool PropertyExists, string? Hash) FindOldHash(IRelationalModel? source, IProperty property)
108
    {
NEW
109
        if (source == null)
×
NEW
110
            return (false, false, null);
×
111

NEW
112
        var oldEntityType = source.Model.FindEntityType(property.DeclaringType.Name);
×
NEW
113
        if (oldEntityType == null)
×
NEW
114
            return (false, false, null);
×
115

NEW
116
        var oldProperty = oldEntityType.FindProperty(property.Name);
×
NEW
117
        if (oldProperty == null)
×
NEW
118
            return (true, false, null);
×
119

NEW
120
        return (true, true, oldProperty.FindAnnotation(AutoComputeAnnotationNames.Hash)?.Value as string);
×
121
    }
122

NEW
123
    private static readonly MethodInfo _captureHelperMethod = typeof(AutoComputeMigrationsModelDiffer)
×
NEW
124
        .GetMethod(nameof(CaptureHelper), BindingFlags.NonPublic | BindingFlags.Static)!;
×
125

126
    private static string CaptureExecuteUpdateSql(
127
        DbContext dbContext,
128
        ComputedMember computed,
129
        IProperty property)
130
    {
NEW
131
        var method = _captureHelperMethod.MakeGenericMethod(property.DeclaringType.ClrType, property.ClrType);
×
NEW
132
        return (string)method.Invoke(null, new object[] { dbContext, computed, property })!;
×
133
    }
134

135
    private static string CaptureHelper<TEntity, TProperty>(
136
        DbContext dbContext,
137
        ComputedMember computed,
138
        IProperty property) where TEntity : class
139
    {
140
        // 1. Prepare expression for database translation
NEW
141
        var preparedExpr = (Expression<Func<TEntity, TProperty>>)dbContext.PrepareComputedExpressionForDatabase(
×
NEW
142
            computed.ChangesProvider.Expression);
×
143

144
        // 2. Build property selector: e => e.PropertyName
NEW
145
        var eParam = Expression.Parameter(typeof(TEntity), "e");
×
146
        Expression propAccess;
NEW
147
        if (property.PropertyInfo != null)
×
NEW
148
            propAccess = Expression.Property(eParam, property.PropertyInfo);
×
149
        else
150
        {
NEW
151
            var efPropertyMethod = typeof(EF).GetMethod(nameof(EF.Property))!.MakeGenericMethod(typeof(TProperty));
×
NEW
152
            propAccess = Expression.Call(null, efPropertyMethod, eParam, Expression.Constant(property.Name));
×
153
        }
NEW
154
        var propSelector = Expression.Lambda<Func<TEntity, TProperty>>(propAccess, eParam);
×
155

156
        // 3. Build SetPropertyCalls expression: s => s.SetProperty(e => e.Prop, computedExpr)
157
        //    SetProperty takes Func<> params. LambdaExpression.Type returns the delegate type
158
        //    (e.g. Func<T,P>), so passing lambdas directly to Expression.Call works.
NEW
159
        var sParam = Expression.Parameter(typeof(SetPropertyCalls<TEntity>), "s");
×
160

NEW
161
        var setPropertyMethod = typeof(SetPropertyCalls<TEntity>)
×
NEW
162
            .GetMethods()
×
NEW
163
            .First(m => m.Name == "SetProperty"
×
NEW
164
                && m.IsGenericMethodDefinition
×
NEW
165
                && m.GetParameters().Length == 2
×
NEW
166
                && m.GetParameters()[1].ParameterType.IsGenericType
×
NEW
167
                && m.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>))
×
NEW
168
            .MakeGenericMethod(typeof(TProperty));
×
169

NEW
170
        var setPropertyCall = Expression.Call(
×
NEW
171
            sParam,
×
NEW
172
            setPropertyMethod,
×
NEW
173
            propSelector,
×
NEW
174
            preparedExpr);
×
175

NEW
176
        var setPropertyCallsExpr = Expression.Lambda<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>>(
×
NEW
177
            setPropertyCall, sParam);
×
178

179
        // 4. Capture SQL via interceptor
NEW
180
        using (SqlCaptureInterceptor.StartCapture())
×
181
        {
NEW
182
            dbContext.Set<TEntity>().ExecuteUpdate(setPropertyCallsExpr);
×
183
        }
184

NEW
185
        return SqlCaptureInterceptor.CapturedSql
×
NEW
186
            ?? throw new InvalidOperationException(
×
NEW
187
                "Failed to capture SQL. Ensure the database is running during migration generation.");
×
188
    }
189
}
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

© 2026 Coveralls, Inc