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

lucaslorentz / auto-compute / 23087159339

14 Mar 2026 11:36AM UTC coverage: 59.677% (-0.02%) from 59.695%
23087159339

Pull #20

github

lucaslorentz
Fix npm install race condition in multi-target build
Pull Request #20: Add .NET 10 support and update packages

0 of 17 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

1995 of 3343 relevant lines covered (59.68%)

994.5 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
28
) : MigrationsModelDiffer(
×
29
    typeMappingSource,
×
30
    migrationsAnnotationProvider,
×
31
#if NET9_0_OR_GREATER
×
32
    relationalAnnotationProvider,
×
33
#endif
×
34
    rowIdentityMapFactory,
×
35
    commandBatchPreparerDependencies)
×
36
{
37
    public override IReadOnlyList<MigrationOperation> GetDifferences(
38
        IRelationalModel? source, IRelationalModel? target)
39
    {
40
        var operations = base.GetDifferences(source, target).ToList();
×
41

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

45
        var dbContext = currentDbContext.Context;
×
46

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

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

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

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

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

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

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

104
        return operations;
×
105
    }
106

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

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

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

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

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

126
    private static string CaptureExecuteUpdateSql(
127
        DbContext dbContext,
128
        ComputedMember computed,
129
        IProperty property)
130
    {
131
        var method = _captureHelperMethod.MakeGenericMethod(property.DeclaringType.ClrType, property.ClrType);
×
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
141
        var preparedExpr = (Expression<Func<TEntity, TProperty>>)dbContext.PrepareComputedExpressionForDatabase(
×
142
            computed.ChangesProvider.Expression);
×
143

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

156
        // 3. Call ExecuteUpdate with SetProperty
UNCOV
157
        using (SqlCaptureInterceptor.StartCapture())
×
158
        {
159
#if NET10_0_OR_GREATER
160
            // EF Core 10: ExecuteUpdate takes Action<UpdateSettersBuilder<T>>
NEW
161
            dbContext.Set<TEntity>().ExecuteUpdate(s => s.SetProperty(propSelector, preparedExpr));
×
162
#else
163
            // EF Core 8/9: ExecuteUpdate takes Expression<Func<SetPropertyCalls<T>, SetPropertyCalls<T>>>
NEW
164
            var sParam = Expression.Parameter(typeof(SetPropertyCalls<TEntity>), "s");
×
165

NEW
166
            var setPropertyMethod = typeof(SetPropertyCalls<TEntity>)
×
NEW
167
                .GetMethods()
×
NEW
168
                .First(m => m.Name == "SetProperty"
×
NEW
169
                    && m.IsGenericMethodDefinition
×
NEW
170
                    && m.GetParameters().Length == 2
×
NEW
171
                    && m.GetParameters()[1].ParameterType.IsGenericType
×
NEW
172
                    && m.GetParameters()[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>))
×
NEW
173
                .MakeGenericMethod(typeof(TProperty));
×
174

NEW
175
            var setPropertyCall = Expression.Call(
×
NEW
176
                sParam,
×
NEW
177
                setPropertyMethod,
×
NEW
178
                propSelector,
×
NEW
179
                preparedExpr);
×
180

NEW
181
            var setPropertyCallsExpr = Expression.Lambda<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>>(
×
NEW
182
                setPropertyCall, sParam);
×
183

UNCOV
184
            dbContext.Set<TEntity>().ExecuteUpdate(setPropertyCallsExpr);
×
185
#endif
186
        }
187

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