11using System ;
2- using System . Linq ;
2+ using System . Collections . Generic ;
33using System . Threading ;
44using System . Threading . Tasks ;
55using Microsoft . EntityFrameworkCore ;
@@ -24,43 +24,94 @@ public InpBsnBackfillService(IServiceProvider serviceProvider, ILogger<InpBsnBac
2424
2525 protected override async Task ExecuteAsync ( CancellationToken stoppingToken )
2626 {
27+ // Check whether the legacy inpbsn column still exists. After migration
28+ // 20260413000000_remove_inpbsn_plain_column_from_zaakrollen has run, there
29+ // is nothing left to backfill and this service exits immediately.
30+ using var scope = _serviceProvider . CreateScope ( ) ;
31+ await using var db = scope . ServiceProvider . GetRequiredService < ZrcDbContext > ( ) ;
32+
33+ var columnExists = await db . Database
34+ . SqlQuery < bool > ( $ """
35+ SELECT EXISTS (
36+ SELECT 1
37+ FROM information_schema.columns
38+ WHERE table_name = 'zaakrollen_natuurlijk_personen'
39+ AND column_name = 'inpbsn'
40+ )
41+ """ )
42+ . SingleAsync ( stoppingToken ) ;
43+
44+ if ( ! columnExists )
45+ {
46+ _logger . LogInformation ( "InpBsnBackfillService: inpbsn column no longer exists, nothing to backfill." ) ;
47+ return ;
48+ }
49+
2750 const int batchSize = 5_000 ;
2851 int totalMigrated = 0 ;
2952 int batchCount ;
3053
3154 do
3255 {
33- using var scope = _serviceProvider . CreateScope ( ) ;
34- await using var db = scope . ServiceProvider . GetRequiredService < ZrcDbContext > ( ) ;
56+ using var batchScope = _serviceProvider . CreateScope ( ) ;
57+ await using var batchDb = batchScope . ServiceProvider . GetRequiredService < ZrcDbContext > ( ) ;
3558
36- var batch = await db . Set < NatuurlijkPersoonZaakRol > ( )
37- . Where ( r => r . InpBsn != null && r . InpBsnHash == null )
38- . Take ( batchSize )
59+ // Read raw inpbsn values via SQL because the entity property has been removed.
60+ var rawRecords = await batchDb . Database
61+ . SqlQuery < InpBsnRawRecord > ( $ """
62+ SELECT id, inpbsn AS bsn
63+ FROM zaakrollen_natuurlijk_personen
64+ WHERE inpbsn IS NOT NULL
65+ AND inpbsn_hash IS NULL
66+ LIMIT { batchSize }
67+ """ )
3968 . ToListAsync ( stoppingToken ) ;
4069
41- batchCount = batch . Count ;
70+ batchCount = rawRecords . Count ;
4271
43- foreach ( var record in batch )
72+ if ( batchCount > 0 )
4473 {
45- record . InpBsnHash = record . InpBsn ;
46- record . InpBsnEncrypted = record . InpBsn ;
47- }
74+ var ids = new List < Guid > ( batchCount ) ;
75+ var bsnById = new Dictionary < Guid , string > ( batchCount ) ;
76+ foreach ( var r in rawRecords )
77+ {
78+ ids . Add ( r . Id ) ;
79+ bsnById [ r . Id ] = r . Bsn ;
80+ }
4881
49- await db . SaveChangesAsync ( stoppingToken ) ;
50- db . ChangeTracker . Clear ( ) ;
82+ var entities = await batchDb . Set < NatuurlijkPersoonZaakRol > ( )
83+ . Where ( e => ids . Contains ( e . Id ) )
84+ . ToListAsync ( stoppingToken ) ;
5185
52- totalMigrated += batchCount ;
86+ foreach ( var entity in entities )
87+ {
88+ var bsn = bsnById [ entity . Id ] ;
89+ entity . InpBsnHash = bsn ;
90+ entity . InpBsnEncrypted = bsn ;
91+ }
5392
54- _logger . LogInformation ( "Migrated {BatchCount} InpBsn records to Hash and Encrypted (total: {TotalMigrated})" , batchCount , totalMigrated ) ;
93+ await batchDb . SaveChangesAsync ( stoppingToken ) ;
94+ batchDb . ChangeTracker . Clear ( ) ;
95+
96+ totalMigrated += batchCount ;
97+
98+ _logger . LogInformation (
99+ "Migrated {BatchCount} InpBsn records to Hash and Encrypted (total: {TotalMigrated})" ,
100+ batchCount ,
101+ totalMigrated ) ;
55102
56- if ( batchCount > 0 )
57- {
58103 await Task . Delay ( 50 , stoppingToken ) ;
59104 }
60- } while ( batchCount > 0 ) ;
105+ }
106+ while ( batchCount > 0 ) ;
61107
62- _logger . LogInformation ( "Completed InpBsn migration . Total records migrated: {TotalMigrated}" , totalMigrated ) ;
108+ _logger . LogInformation ( "Completed InpBsn backfill . Total records migrated: {TotalMigrated}" , totalMigrated ) ;
63109 }
64110
65- public Task StopAsync ( CancellationToken cancellationToken ) => Task . CompletedTask ;
111+ // Keyless projection type for EF8 SqlQuery<T>
112+ private sealed class InpBsnRawRecord
113+ {
114+ public Guid Id { get ; init ; }
115+ public string Bsn { get ; init ; }
116+ }
66117}
0 commit comments