| | | 1 | | namespace ZhEaIsNsAaBn.DbSeeder; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// The resolved seed behaviour of one CLR entity type, per field in this order: 1. seed configuration → 2. Domain attri |
| | | 5 | | /// 3. Domain interfaces → 4. nothing (no managed field; deletes are real, FK-checked). |
| | | 6 | | /// </summary> |
| | | 7 | | internal sealed class SeedEntityRoles |
| | | 8 | | { |
| | 10 | 9 | | private static readonly (Type Attribute, SeedRole Role)[] RoleAttributes = |
| | 10 | 10 | | [ |
| | 10 | 11 | | ( typeof( SoftDeleteFlagAttribute ), SeedRole.SoftDeleteFlag ), ( typeof( DeletedDateAttribute ), SeedRole.Delet |
| | 10 | 12 | | ( typeof( DeletedByIdAttribute ), SeedRole.DeletedById ), ( typeof( DeletedByUserAttribute ), SeedRole.DeletedBy |
| | 10 | 13 | | ( typeof( CreatedDateAttribute ), SeedRole.CreatedDate ), ( typeof( CreatedByIdAttribute ), SeedRole.CreatedById |
| | 10 | 14 | | ( typeof( CreatedByUserAttribute ), SeedRole.CreatedByUser ), ( typeof( UpdatedDateAttribute ), SeedRole.Updated |
| | 10 | 15 | | ( typeof( UpdatedByIdAttribute ), SeedRole.UpdatedById ), ( typeof( UpdatedByUserAttribute ), SeedRole.UpdatedBy |
| | 10 | 16 | | ]; |
| | | 17 | | |
| | | 18 | | |
| | 1933 | 19 | | private SeedEntityRoles( |
| | 1933 | 20 | | Type type, |
| | 1933 | 21 | | IReadOnlyDictionary<SeedRole, string> roles, |
| | 1933 | 22 | | IReadOnlySet<string> ignored, |
| | 1933 | 23 | | IReadOnlyList<string>? matchBy, |
| | 1933 | 24 | | SeedDeleteMode deleteMode, |
| | 1933 | 25 | | IReadOnlyList<string> warnings |
| | 1933 | 26 | | ) |
| | | 27 | | { |
| | | 28 | | Type = type; |
| | | 29 | | Roles = roles; |
| | | 30 | | Ignored = ignored; |
| | | 31 | | MatchBy = matchBy; |
| | | 32 | | DeleteMode = deleteMode; |
| | | 33 | | Warnings = warnings; |
| | 1933 | 34 | | } |
| | | 35 | | |
| | | 36 | | |
| | | 37 | | public Type Type { get; } |
| | | 38 | | |
| | | 39 | | /// Managed field → property name. |
| | | 40 | | public IReadOnlyDictionary<SeedRole, string> Roles { get; } |
| | | 41 | | |
| | | 42 | | /// <c>Ignore(…)</c> plus <c>[SeedIgnore]</c>. |
| | | 43 | | public IReadOnlySet<string> Ignored { get; } |
| | | 44 | | |
| | | 45 | | /// The configured business key; <see langword="null"/> for the primary key. |
| | | 46 | | public IReadOnlyList<string>? MatchBy { get; } |
| | | 47 | | |
| | | 48 | | public SeedDeleteMode DeleteMode { get; } |
| | | 49 | | |
| | | 50 | | /// "Set twice" warnings from the configuration (shown by the CLI). |
| | | 51 | | public IReadOnlyList<string> Warnings { get; } |
| | | 52 | | |
| | | 53 | | |
| | 21 | 54 | | public string? this[ SeedRole role ] => Roles.GetValueOrDefault( role ); |
| | | 55 | | |
| | | 56 | | |
| | | 57 | | /// <summary>Resolves the roles of <paramref name="type"/>.</summary> |
| | | 58 | | /// <exception cref="InvalidOperationException">Two properties claim one role, one property claims two roles, or a p |
| | | 59 | | public static SeedEntityRoles Resolve( Type type, SeedEntitySettings? settings ) |
| | | 60 | | { |
| | 1939 | 61 | | var properties = type.GetProperties( BindingFlags.Public | BindingFlags.Instance ); |
| | 1939 | 62 | | var attributed = new Dictionary<SeedRole, string>(); |
| | 1939 | 63 | | var ignored = new HashSet<string>( StringComparer.Ordinal ); |
| | | 64 | | |
| | | 65 | | |
| | | 66 | | // 2. Attributes. |
| | 29090 | 67 | | foreach ( var property in properties ) |
| | | 68 | | { |
| | 12615 | 69 | | if ( Attribute.IsDefined( property, typeof( SeedIgnoreAttribute ), inherit: true ) ) { ignored.Add( property |
| | | 70 | | |
| | 25248 | 71 | | foreach ( var (attribute, role) in RoleAttributes.Where( a => Attribute.IsDefined( property, a.Attribute, in |
| | | 72 | | { |
| | 18 | 73 | | if ( attributed.TryGetValue( role, out var other ) && other != property.Name ) |
| | | 74 | | { |
| | 0 | 75 | | throw new InvalidOperationException( $"{type.Name} has two {role} properties: {other} and {property. |
| | | 76 | | } |
| | | 77 | | |
| | 18 | 78 | | attributed[ role ] = property.Name; |
| | | 79 | | } |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | |
| | | 83 | | // 3. Interfaces. |
| | 1939 | 84 | | var interfaced = new Dictionary<SeedRole, string>(); |
| | | 85 | | |
| | 1939 | 86 | | if ( typeof( IBaseTrackableEntity ).IsAssignableFrom( type ) ) |
| | | 87 | | { |
| | 1011 | 88 | | interfaced[ SeedRole.CreatedDate ] = nameof( IBaseTrackableEntity.CreatedDate ); |
| | 1011 | 89 | | interfaced[ SeedRole.UpdatedDate ] = nameof( IBaseTrackableEntity.UpdatedDate ); |
| | | 90 | | } |
| | | 91 | | |
| | 1939 | 92 | | if ( Implements( type, typeof( IBaseTransactionEntity<> ) ) ) |
| | | 93 | | { |
| | 525 | 94 | | interfaced[ SeedRole.CreatedById ] = nameof( IBaseTransactionEntity<int>.CreatedById ); |
| | 525 | 95 | | interfaced[ SeedRole.CreatedByUser ] = nameof( IBaseTransactionEntity<int>.CreatedByUser ); |
| | 525 | 96 | | interfaced[ SeedRole.UpdatedById ] = nameof( IBaseTransactionEntity<int>.UpdatedById ); |
| | 525 | 97 | | interfaced[ SeedRole.UpdatedByUser ] = nameof( IBaseTransactionEntity<int>.UpdatedByUser ); |
| | | 98 | | } |
| | | 99 | | |
| | 1939 | 100 | | if ( Implements( type, typeof( IDeletableEntity<> ) ) ) |
| | | 101 | | { |
| | 525 | 102 | | interfaced[ SeedRole.SoftDeleteFlag ] = nameof( IDeletableEntity<int>.IsDeleted ); |
| | 525 | 103 | | interfaced[ SeedRole.DeletedDate ] = nameof( IDeletableEntity<int>.DeletedDate ); |
| | 525 | 104 | | interfaced[ SeedRole.DeletedById ] = nameof( IDeletableEntity<int>.DeletedById ); |
| | 525 | 105 | | interfaced[ SeedRole.DeletedByUser ] = nameof( IDeletableEntity<int>.DeletedByUser ); |
| | | 106 | | } |
| | | 107 | | |
| | | 108 | | |
| | | 109 | | // 1. Configuration wins, then attributes, then interfaces. |
| | 1939 | 110 | | var roles = new Dictionary<SeedRole, string>(); |
| | | 111 | | |
| | 42658 | 112 | | foreach ( var role in Enum.GetValues<SeedRole>() ) |
| | | 113 | | { |
| | 19390 | 114 | | var name = settings?.Roles.GetValueOrDefault( role ) ?? attributed.GetValueOrDefault( role ) ?? interfaced.G |
| | | 115 | | |
| | 25621 | 116 | | if ( name is not null ) { roles[ role ] = name; } |
| | | 117 | | } |
| | | 118 | | |
| | 1970 | 119 | | if ( settings is not null ) { ignored.UnionWith( settings.Ignored ); } |
| | | 120 | | |
| | | 121 | | |
| | 1939 | 122 | | Check( type, properties, roles ); |
| | | 123 | | |
| | 1933 | 124 | | return new SeedEntityRoles( type, roles, ignored, settings?.MatchBy, settings?.DeleteMode ?? SeedDeleteMode.Auto |
| | | 125 | | } |
| | | 126 | | |
| | | 127 | | |
| | | 128 | | private static bool Implements( Type type, Type openInterface ) |
| | 3878 | 129 | | => type.GetInterfaces().Any( i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface ); |
| | | 130 | | |
| | | 131 | | private static void Check( Type type, PropertyInfo[] properties, Dictionary<SeedRole, string> roles ) |
| | | 132 | | { |
| | 3881 | 133 | | foreach ( var group in roles.GroupBy( r => r.Value, StringComparer.Ordinal ).Where( g => g.Count() > 1 ) ) |
| | | 134 | | { |
| | 3 | 135 | | throw new InvalidOperationException( $"{type.Name}.{group.Key} plays more than one seed role: {string.Join( |
| | | 136 | | } |
| | | 137 | | |
| | | 138 | | |
| | 16313 | 139 | | foreach ( var (role, name) in roles ) |
| | | 140 | | { |
| | 6222 | 141 | | var propertyType = properties.FirstOrDefault( p => p.Name == name )?.PropertyType; |
| | | 142 | | |
| | 6222 | 143 | | if ( propertyType is null ) { continue; } // a shadow property; the EF model checks it |
| | | 144 | | |
| | | 145 | | |
| | 6222 | 146 | | var valid = role switch |
| | 6222 | 147 | | { |
| | 534 | 148 | | SeedRole.SoftDeleteFlag => propertyType |
| | 2538 | 149 | | SeedRole.CreatedDate or SeedRole.UpdatedDate or SeedRole.DeletedDate => propertyTy |
| | 1575 | 150 | | SeedRole.CreatedByUser or SeedRole.UpdatedByUser or SeedRole.DeletedByUser => propertyTy |
| | 1575 | 151 | | _ => ( Nullable.G |
| | 6222 | 152 | | }; |
| | | 153 | | |
| | 6225 | 154 | | if ( !valid ) { throw new InvalidOperationException( $"{type.Name}.{name} can't be the {role} field: its typ |
| | | 155 | | } |
| | 1933 | 156 | | } |
| | | 157 | | } |