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

lucaslorentz / auto-compute / 23084474323

14 Mar 2026 08:40AM UTC coverage: 59.695%. First build
23084474323

Pull #19

github

lucaslorentz
Add automatic backfill SQL generation in migrations for computed properties

When a computed property is added or its expression changes, `dotnet ef migrations add`
now automatically generates `migrationBuilder.Sql("UPDATE ...")` to update existing records.

- SHA256 hash of expanded expression stored as permanent annotation for change detection
- Uses ExecuteUpdate + SqlCaptureInterceptor for provider-agnostic SQL generation
- Configurable via `EnableBackfillInMigrations` (enabled by default)
- Untranslatable expressions generate a friendly SQL comment
- Existing properties without hashes are skipped (assumes consistency)
- Removes code coverage threshold from CI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pull Request #19: Automatic backfill SQL generation in migrations

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

1995 of 3342 relevant lines covered (59.69%)

663.56 hits per line

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

16.22
/src/LLL.AutoCompute.EFCore/Metadata/Conventions/RemoveComputedAnnotationsConvention.cs
1
using System.Linq.Expressions;
2
using System.Security.Cryptography;
3
using System.Text;
4
using LLL.AutoCompute.EFCore.Metadata.Internal;
5
using Microsoft.EntityFrameworkCore;
6
using Microsoft.EntityFrameworkCore.Metadata;
7
using Microsoft.EntityFrameworkCore.Metadata.Builders;
8
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
9

10
namespace LLL.AutoCompute.EFCore.Metadata.Conventions;
11

12
class DesignTimeComputedConvention(
45✔
13
    Func<IModel, IComputedExpressionAnalyzer> analyzerFactory,
45✔
14
    bool enableBackfillInMigrations
45✔
15
) : IModelFinalizingConvention
45✔
16
{
17
    public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
18
    {
19
        if (!EF.IsDesignTime)
45✔
20
            return;
45✔
21

NEW
22
        var model = modelBuilder.Metadata;
×
23

NEW
24
        if (enableBackfillInMigrations)
×
25
        {
26
            // 1. Collect computed factories BEFORE removing annotations
NEW
27
            var factories = new List<(IProperty, ComputedMemberFactory<IProperty>)>();
×
NEW
28
            foreach (var entityType in model.GetEntityTypes())
×
29
            {
NEW
30
                foreach (var property in entityType.GetDeclaredProperties())
×
31
                {
NEW
32
                    var factory = ((IReadOnlyProperty)property).GetComputedFactory();
×
NEW
33
                    if (factory is not null)
×
NEW
34
                        factories.Add(((IProperty)property, factory));
×
35
                }
36
            }
37

NEW
38
            DesignTimeComputedStore.Factories = factories;
×
39

40
            // 2. Compute hashes from expanded expressions (after expression modifiers)
NEW
41
            var analyzer = analyzerFactory((IModel)model);
×
NEW
42
            foreach (var entityType in model.GetEntityTypes())
×
43
            {
NEW
44
                foreach (var property in entityType.GetDeclaredProperties())
×
45
                {
NEW
46
                    var rawExpr = property.FindAnnotation(AutoComputeAnnotationNames.RawExpression)?.Value as LambdaExpression;
×
NEW
47
                    if (rawExpr is null)
×
48
                        continue;
49

NEW
50
                    var expanded = analyzer.RunExpressionModifiers(rawExpr);
×
NEW
51
                    var hash = ComputeHash(expanded.ToString());
×
NEW
52
                    property.SetAnnotation(AutoComputeAnnotationNames.Hash, hash);
×
53
                }
54
            }
55
        }
56

57
        // 3. Remove non-serializable annotations (original behavior)
NEW
58
        var annotatables = model.GetEntityTypes()
×
59
            .SelectMany(e => new IConventionAnnotatable[] { e }.Concat(e.GetMembers().OfType<IConventionAnnotatable>()))
×
NEW
60
            .Concat([model])
×
61
            .ToArray();
×
62

63
        foreach (var annotatable in annotatables)
×
64
        {
65
            var computedAnnotations = annotatable
×
66
                .GetAnnotations()
×
67
                .Where(a => a.Name.StartsWith(AutoComputeAnnotationNames.TempPrefix))
×
68
                .ToArray();
×
69

70
            if (computedAnnotations.Length > 0)
×
71
            {
72
                foreach (var annotation in computedAnnotations)
×
73
                    annotatable.RemoveAnnotation(annotation.Name);
×
74
            }
75
        }
76
    }
77

78
    private static string ComputeHash(string input)
79
    {
NEW
80
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
×
NEW
81
        return Convert.ToHexString(bytes);
×
82
    }
83
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc