< Summary - ZhEaIsNsAaBn.Libraries coverage (affected: 23 packages)

Information
Line coverage
97%
Covered lines: 131
Uncovered lines: 3
Coverable lines: 134
Total lines: 313
Line coverage: 97.7%
Branch coverage
83%
Covered branches: 92
Total branches: 110
Branch coverage: 83.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Bind(...)91.66%121292.3%
BindTable(...)75%282898%
MatchColumns(...)77.27%222293.33%
DataRows(...)95.45%2222100%
Keys(...)87.5%88100%
IsSkipped(...)100%44100%
Column(...)75%88100%
.ctor(...)100%11100%
.ctor(...)100%11100%
Convert(...)83.33%66100%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder/Binding/SeedMigrationBinder.cs

#LineLine coverage
 1namespace ZhEaIsNsAaBn.DbSeeder;
 2
 3/// <summary>
 4/// Binds a parsed migration to a context's EF model: finds each entity and table, resolves the match key and the manage
 5/// checks every row against the model and converts values to provider values, then orders the tables (§6.4).
 6/// </summary>
 7internal static class SeedMigrationBinder
 8{
 9    /// <returns>The tables in upsert order (principals first); otherwise <c>InvalidDataFile</c>, <c>UnknownProperty</c>
 10    /// <exception cref="InvalidOperationException">A configured <c>MatchBy</c> isn't the primary key or a unique index.
 11    public static LayerResponse<IReadOnlyList<SeedBoundTable>> Bind( IModel model, SeedConfiguration configuration, Seed
 12    {
 55713        var tables = new List<SeedBoundTable>();
 14
 15
 371516        foreach ( var (_, path, file) in migration.Tables )
 17        {
 132618            var bound = BindTable( model, configuration, path, file );
 19
 137120            if ( bound.IsFailure ) { return LayerResponse<IReadOnlyList<SeedBoundTable>>.Failure( bound ); }
 21
 127522            if ( tables.Any( t => t.EntityType == bound.Result!.EntityType ) )
 23            {
 024                return SeedErrors.InvalidDataFile( path, $"{file.Entity} has more than one table file in {migration.Id}.
 25            }
 26
 127527            tables.Add( bound.Result! );
 28        }
 29
 30
 31        // v1: a table matched by a business key can't have seed children in the same migration (no key remapping).
 103332        foreach ( var table in tables.Where( t => t.MatchesByBusinessKey ) )
 33        {
 1234            if ( tables.FirstOrDefault( t => t != table && t.EntityType.GetForeignKeys().Any( k => k.PrincipalEntityType
 35            {
 336                return SeedErrors.InvalidDataFile( table.File, $"{table.EntityType.Name} is matched by MatchBy but {chil
 37            }
 38        }
 39
 40
 50341        var (order, cycle) = SeedTableOrder.Sort( tables.Select( t => t.EntityType ).ToList() );
 42
 50343        return order is null ? SeedErrors.CycleDetected( migration.Id, cycle ) : order.Select( e => tables.Single( t => 
 5144    }
 45
 46
 47    internal static LayerResponse<SeedBoundTable> BindTable( IModel model, SeedConfiguration configuration, string path,
 48    {
 231049        if ( model.FindEntityType( file.Entity ) is not { } entityType ) { return SeedErrors.InvalidDataFile( path, $"{f
 50
 230451        if ( entityType.IsOwned() || entityType.HasSharedClrType || entityType.BaseType is not null || entityType.GetDer
 52        {
 053            return SeedErrors.InvalidDataFile( path, $"{file.Entity}: owned, shared-type and inheritance entities aren't
 54        }
 55
 230456        if ( entityType.GetTableName() is not { } tableName ) { return SeedErrors.InvalidDataFile( path, $"{file.Entity}
 57
 58
 230459        var store   = StoreObjectIdentifier.Table( tableName, entityType.GetSchema() );
 230460        var roles   = configuration.RolesFor( entityType.ClrType );
 230461        var managed = new Dictionary<SeedRole, SeedColumn>();
 62
 2067063        foreach ( var (role, name) in roles.Roles )
 64        {
 1606265            if ( entityType.FindProperty( name ) is { } property ) { managed[ role ] = Column( property, store ); }
 66        }
 67
 68
 230469        var match = MatchColumns( entityType, store, roles, file, path );
 70
 230771        if ( match.IsFailure ) { return LayerResponse<SeedBoundTable>.Failure( match ); }
 72
 73
 229574        var table = new SeedTable
 229575                        {
 229576                            EntityType     = entityType,
 229577                            Schema         = store.Schema,
 229578                            Name           = store.Name,
 229579                            MatchColumns   = match.Result!.Columns,
 229580                            SoftDeleteFlag = managed.GetValueOrDefault( SeedRole.SoftDeleteFlag )
 229581                        };
 82
 229583        var upsert = DataRows( entityType, store, roles, managed, table, file.Upsert, nameof( SeedTableFile.Upsert ), pa
 84
 232885        if ( upsert.IsFailure ) { return LayerResponse<SeedBoundTable>.Failure( upsert ); }
 86
 87
 226288        var deletes = Keys( table, file.Delete, nameof( SeedTableFile.Delete ), path );
 89
 226590        if ( deletes.IsFailure ) { return LayerResponse<SeedBoundTable>.Failure( deletes ); }
 91
 92
 225993        var softDeletes = Keys( table, file.SoftDelete, nameof( SeedTableFile.SoftDelete ), path );
 94
 225995        if ( softDeletes.IsFailure ) { return LayerResponse<SeedBoundTable>.Failure( softDeletes ); }
 96
 97
 225998        var restores = Keys( table, file.Restore, nameof( SeedTableFile.Restore ), path );
 99
 2259100        if ( restores.IsFailure ) { return LayerResponse<SeedBoundTable>.Failure( restores ); }
 101
 102
 2259103        var before = DataRows( entityType, store, roles, managed, table, file.Before, nameof( SeedTableFile.Before ), pa
 104
 2262105        if ( before.IsFailure ) { return LayerResponse<SeedBoundTable>.Failure( before ); }
 106
 107
 2256108        var (columns, rows) = upsert.Result!;
 2256109        var written         = columns.Select( c => c.PropertyName ).ToHashSet( StringComparer.Ordinal );
 110
 2256111        return new SeedBoundTable
 2256112                   {
 2256113                       File                 = path,
 2256114                       Table                = table,
 2256115                       Roles                = roles,
 2256116                       Managed              = managed,
 2256117                       UpsertColumns        = columns,
 2256118                       UpsertRows           = rows,
 2256119                       DeleteKeys           = deletes.Result!,
 2256120                       SoftDeleteKeys       = softDeletes.Result!,
 2256121                       RestoreKeys          = restores.Result!,
 2256122                       BeforeColumns        = before.Result!.Columns,
 2256123                       BeforeRows           = before.Result.Rows,
 2256124                       MatchesByBusinessKey = match.Result.IsBusinessKey,
 2256125                       SelfReferences = entityType.GetForeignKeys()
 2256126                                                  .Where( k => k.PrincipalEntityType == entityType
 2256127                                                            && k.Properties.Concat( k.PrincipalKey.Properties ).All( p =
 2256128                                                  .ToList()
 2256129                   };
 130    }
 131
 132    private static LayerResponse<MatchKey> MatchColumns(
 133            IEntityType entityType,
 134            StoreObjectIdentifier store,
 135            SeedEntityRoles roles,
 136            SeedTableFile file,
 137            string path
 138        )
 139    {
 2304140        var primaryKey = entityType.FindPrimaryKey();
 2304141        var names      = file.MatchBy.Count > 0 ? file.MatchBy : roles.MatchBy ?? primaryKey?.Properties.Select( p => p.
 142
 2304143        if ( names.Count == 0 ) { return SeedErrors.InvalidDataFile( path, $"{entityType.Name} has no primary key and no
 144
 145
 2304146        var properties = new List<IProperty>();
 147
 9216148        foreach ( var name in names )
 149        {
 2304150            if ( entityType.FindProperty( name ) is not { } property ) { return SeedErrors.UnknownProperty( path, entity
 151
 2304152            properties.Add( property );
 153        }
 154
 155
 2304156        var set           = properties.ToHashSet();
 2304157        var isPrimaryKey  = primaryKey is not null && primaryKey.Properties.ToHashSet().SetEquals( set );
 2304158        var isUniqueIndex = entityType.GetIndexes().Any( i => i.IsUnique && i.GetFilter() is null && i.Properties.ToHash
 159
 2304160        if ( !isPrimaryKey && !isUniqueIndex )
 161        {
 9162            var message = $"MatchBy ( {string.Join( ", ", names )} ) of {entityType.Name} is neither the primary key nor
 163
 164            // A data file can be wrong (error); a configuration that can never work is a programmer error.
 9165            return file.MatchBy.Count > 0 ? SeedErrors.InvalidDataFile( path, message ) : throw new InvalidOperationExce
 166        }
 167
 168
 2295169        return new MatchKey( properties.Select( p => Column( p, store ) ).ToList(), !isPrimaryKey );
 0170    }
 171
 172    /// Full data rows (<c>Upsert</c> or <c>Before</c>): same properties in every row, the match key included, no manage
 173    private static LayerResponse<UpsertData> DataRows(
 174            IEntityType entityType,
 175            StoreObjectIdentifier store,
 176            SeedEntityRoles roles,
 177            Dictionary<SeedRole, SeedColumn> managed,
 178            SeedTable table,
 179            IReadOnlyList<IReadOnlyDictionary<string, JsonElement>> source,
 180            string operation,
 181            string path
 182        )
 183    {
 7626184        if ( source.Count == 0 ) { return new UpsertData( [], [] ); }
 185
 186
 1482187        var names = source[ 0 ].Keys.ToHashSet( StringComparer.Ordinal );
 188
 1485189        if ( source.Any( r => !names.SetEquals( r.Keys ) ) ) { return SeedErrors.InvalidDataFile( path, $"Every {operati
 190
 1479191        if ( table.MatchColumns.FirstOrDefault( c => !names.Contains( c.PropertyName ) ) is { } missing )
 192        {
 3193            return SeedErrors.InvalidDataFile( path, $"{operation} rows must include the match key property {missing.Pro
 194        }
 195
 196
 1476197        var columns = new List<SeedColumn>();
 198
 12366199        foreach ( var property in entityType.GetProperties().Where( p => names.Contains( p.Name ) ) )
 200        {
 4716201            if ( managed.Values.Any( m => m.Property == property ) )
 202            {
 9203                return SeedErrors.InvalidDataFile( path, $"{property.Name} is an audit / soft-delete field, set by the r
 204            }
 205
 4707206            if ( IsSkipped( property, store, roles ) )
 207            {
 9208                return SeedErrors.InvalidDataFile( path, $"{property.Name} is ignored or generated by the database; it c
 209            }
 210
 4698211            columns.Add( Column( property, store ) );
 212        }
 213
 1464214        if ( names.FirstOrDefault( n => entityType.FindProperty( n ) is null ) is { } unknown ) { return SeedErrors.Unkn
 215
 216
 1452217        var rows = new List<IReadOnlyList<object?>>( source.Count );
 218
 8566219        foreach ( var row in source )
 220        {
 2834221            var values = new object?[ columns.Count ];
 222
 23688223            for ( var i = 0; i < columns.Count; i++ )
 224            {
 9016225                var value = Convert( row[ columns[ i ].PropertyName ], columns[ i ], path );
 226
 9022227                if ( value.IsFailure ) { return LayerResponse<UpsertData>.Failure( value ); }
 228
 9010229                values[ i ] = value.Result;
 230            }
 231
 2828232            rows.Add( values );
 233        }
 234
 235
 1446236        return new UpsertData( columns, rows );
 24237    }
 238
 239    private static LayerResponse<IReadOnlyList<IReadOnlyList<object?>>> Keys( SeedTable table, IReadOnlyList<IReadOnlyDi
 240    {
 6780241        var keys = new List<IReadOnlyList<object?>>( rows.Count );
 242
 243
 14205244        foreach ( var row in rows )
 245        {
 324246            var values = new object?[ table.MatchColumns.Count ];
 247
 1290248            for ( var i = 0; i < values.Length; i++ )
 249            {
 324250                var column = table.MatchColumns[ i ];
 251
 324252                if ( !row.TryGetValue( column.PropertyName, out var element ) )
 253                {
 3254                    return SeedErrors.InvalidDataFile( path, $"{operation} rows must include the match key property {col
 255                }
 256
 257
 321258                var value = Convert( element, column, path );
 259
 321260                if ( value.IsFailure ) { return LayerResponse<IReadOnlyList<IReadOnlyList<object?>>>.Failure( value ); }
 261
 321262                values[ i ] = value.Result;
 263            }
 264
 321265            keys.Add( values );
 266        }
 267
 268
 6777269        return keys;
 3270    }
 271
 272    /// Computed, row-version / temporal (generated on update), <c>[SeedIgnore]</c> and <c>Ignore(…)</c> properties are 
 273    internal static bool IsSkipped( IProperty property, StoreObjectIdentifier store, SeedEntityRoles roles )
 7887274        => property.GetComputedColumnSql( store ) is not null
 7887275        || property.ValueGenerated.HasFlag( ValueGenerated.OnUpdate )
 7887276        || roles.Ignored.Contains( property.Name );
 277
 278    internal static SeedColumn Column( IProperty property, StoreObjectIdentifier store )
 279    {
 18204280        var mapping = property.GetRelationalTypeMapping();
 281
 18204282        return new SeedColumn
 18204283                   {
 18204284                       Property        = property,
 18204285                       ColumnName      = property.GetColumnName( store ) ?? property.GetColumnName(),
 18204286                       ProviderClrType = mapping.Converter?.ProviderClrType ?? mapping.ClrType,
 18204287                       StoreType       = property.GetColumnType( store ) ?? mapping.StoreType
 18204288                   };
 289    }
 290
 291
 2295292    private sealed record MatchKey( IReadOnlyList<SeedColumn> Columns, bool IsBusinessKey );
 293
 4518294    private sealed record UpsertData( IReadOnlyList<SeedColumn> Columns, IReadOnlyList<IReadOnlyList<object?>> Rows );
 295
 296    /// A JSON value → the column's provider value. JSON <c>null</c> only for a nullable column.
 297    private static LayerResponse<object?> Convert( JsonElement element, SeedColumn column, string path )
 298    {
 9337299        if ( element.ValueKind == JsonValueKind.Null )
 300        {
 184301            return column.IsNullable ? LayerResponse<object?>.Success() : SeedErrors.InvalidDataFile( path, $"{column.Pr
 302        }
 303
 304
 9153305        var type = Nullable.GetUnderlyingType( column.ProviderClrType ) ?? column.ProviderClrType;
 306
 9153307        try { return LayerResponse<object?>.Success( element.Deserialize( type, SeedMigrationSource.JsonOptions ) ); }
 3308        catch ( Exception ex ) when ( ex is JsonException or NotSupportedException or FormatException or InvalidOperatio
 309        {
 3310            return SeedErrors.InvalidDataFile( path, $"{column.PropertyName}: '{element.GetRawText()}' isn't a {type.Nam
 311        }
 9153312    }
 313}

Methods/Properties

Bind(Microsoft.EntityFrameworkCore.Metadata.IModel,ZhEaIsNsAaBn.DbSeeder.SeedConfiguration,ZhEaIsNsAaBn.DbSeeder.SeedMigrationFiles)
BindTable(Microsoft.EntityFrameworkCore.Metadata.IModel,ZhEaIsNsAaBn.DbSeeder.SeedConfiguration,System.String,ZhEaIsNsAaBn.DbSeeder.SeedTableFile)
MatchColumns(Microsoft.EntityFrameworkCore.Metadata.IEntityType,Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier,ZhEaIsNsAaBn.DbSeeder.SeedEntityRoles,ZhEaIsNsAaBn.DbSeeder.SeedTableFile,System.String)
DataRows(Microsoft.EntityFrameworkCore.Metadata.IEntityType,Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier,ZhEaIsNsAaBn.DbSeeder.SeedEntityRoles,System.Collections.Generic.Dictionary`2<ZhEaIsNsAaBn.DbSeeder.SeedRole,ZhEaIsNsAaBn.DbSeeder.SeedColumn>,ZhEaIsNsAaBn.DbSeeder.SeedTable,System.Collections.Generic.IReadOnlyList`1<System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Text.Json.JsonElement>>,System.String,System.String)
Keys(ZhEaIsNsAaBn.DbSeeder.SeedTable,System.Collections.Generic.IReadOnlyList`1<System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.Text.Json.JsonElement>>,System.String,System.String)
IsSkipped(Microsoft.EntityFrameworkCore.Metadata.IProperty,Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier,ZhEaIsNsAaBn.DbSeeder.SeedEntityRoles)
Column(Microsoft.EntityFrameworkCore.Metadata.IProperty,Microsoft.EntityFrameworkCore.Metadata.StoreObjectIdentifier)
.ctor(System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedColumn>,System.Boolean)
.ctor(System.Collections.Generic.IReadOnlyList`1<ZhEaIsNsAaBn.DbSeeder.SeedColumn>,System.Collections.Generic.IReadOnlyList`1<System.Collections.Generic.IReadOnlyList`1<System.Object>>)
Convert(System.Text.Json.JsonElement,ZhEaIsNsAaBn.DbSeeder.SeedColumn,System.String)