| | | 1 | | using System.Collections.Concurrent; |
| | | 2 | | using System.Reflection; |
| | | 3 | | |
| | | 4 | | namespace ZhEaIsNsAaBn.Infrastructure.Core; |
| | | 5 | | |
| | | 6 | | /// <summary>An audit field an entity can have.</summary> |
| | | 7 | | internal enum AuditRole |
| | | 8 | | { |
| | | 9 | | SoftDeleteFlag, |
| | | 10 | | DeletedDate, |
| | | 11 | | DeletedById, |
| | | 12 | | DeletedByUser, |
| | | 13 | | CreatedDate, |
| | | 14 | | CreatedById, |
| | | 15 | | CreatedByUser, |
| | | 16 | | UpdatedDate, |
| | | 17 | | UpdatedById, |
| | | 18 | | UpdatedByUser |
| | | 19 | | } |
| | | 20 | | |
| | | 21 | | /// <summary> |
| | | 22 | | /// Which property of an entity type plays which audit role: from the Domain attributes (<c>[CreatedDate]</c>, <c>[SoftD |
| | | 23 | | /// first, then from the Domain interfaces (<see cref="IBaseTrackableEntity"/>, <see cref="IBaseTransactionEntity{TId}"/ |
| | | 24 | | /// <see cref="IDeletableEntity{TId}"/>). Built once per CLR type. |
| | | 25 | | /// </summary> |
| | | 26 | | internal sealed class AuditMap |
| | | 27 | | { |
| | 4 | 28 | | private static readonly ConcurrentDictionary<Type, AuditMap> Cache = new(); |
| | | 29 | | |
| | 4 | 30 | | private static readonly (Type Attribute, AuditRole Role)[] Attributes = |
| | 4 | 31 | | [ |
| | 4 | 32 | | ( typeof( SoftDeleteFlagAttribute ), AuditRole.SoftDeleteFlag ), ( typeof( DeletedDateAttribute ), AuditRole.Del |
| | 4 | 33 | | ( typeof( DeletedByIdAttribute ), AuditRole.DeletedById ), ( typeof( DeletedByUserAttribute ), AuditRole.Deleted |
| | 4 | 34 | | ( typeof( CreatedDateAttribute ), AuditRole.CreatedDate ), ( typeof( CreatedByIdAttribute ), AuditRole.CreatedBy |
| | 4 | 35 | | ( typeof( CreatedByUserAttribute ), AuditRole.CreatedByUser ), ( typeof( UpdatedDateAttribute ), AuditRole.Updat |
| | 4 | 36 | | ( typeof( UpdatedByIdAttribute ), AuditRole.UpdatedById ), ( typeof( UpdatedByUserAttribute ), AuditRole.Updated |
| | 4 | 37 | | ]; |
| | | 38 | | |
| | | 39 | | private readonly PropertyInfo?[] _properties; |
| | | 40 | | |
| | | 41 | | |
| | 32 | 42 | | private AuditMap( Type type, PropertyInfo?[] properties ) |
| | | 43 | | { |
| | | 44 | | Type = type; |
| | 32 | 45 | | _properties = properties; |
| | 32 | 46 | | } |
| | | 47 | | |
| | | 48 | | |
| | | 49 | | public Type Type { get; } |
| | | 50 | | |
| | 196 | 51 | | public bool IsEmpty => _properties.All( p => p is null ); |
| | | 52 | | |
| | 132 | 53 | | public PropertyInfo? SoftDeleteFlag => this[ AuditRole.SoftDeleteFlag ]; |
| | | 54 | | |
| | 1260 | 55 | | public PropertyInfo? this[ AuditRole role ] => _properties[ (int) role ]; |
| | | 56 | | |
| | | 57 | | |
| | 256 | 58 | | public static AuditMap For( Type type ) => Cache.GetOrAdd( type, Build ); |
| | | 59 | | |
| | | 60 | | |
| | | 61 | | private static AuditMap Build( Type type ) |
| | | 62 | | { |
| | 32 | 63 | | var properties = new PropertyInfo?[ Enum.GetValues<AuditRole>().Length ]; |
| | 32 | 64 | | var candidates = type.GetProperties( BindingFlags.Public | BindingFlags.Instance ); |
| | | 65 | | |
| | | 66 | | |
| | 504 | 67 | | foreach ( var property in candidates ) |
| | | 68 | | { |
| | 4840 | 69 | | foreach ( var (attribute, role) in Attributes ) |
| | | 70 | | { |
| | 2200 | 71 | | if ( !Attribute.IsDefined( property, attribute, inherit: true ) ) { continue; } |
| | | 72 | | |
| | 40 | 73 | | if ( properties[ (int) role ] is { } other && other.Name != property.Name ) |
| | | 74 | | { |
| | 0 | 75 | | throw new InvalidOperationException( $"{type.Name} has two {role} properties: {other.Name} and {prop |
| | | 76 | | } |
| | | 77 | | |
| | 40 | 78 | | properties[ (int) role ] = property; |
| | | 79 | | } |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | |
| | 32 | 83 | | if ( typeof( IBaseTrackableEntity ).IsAssignableFrom( type ) ) |
| | | 84 | | { |
| | 16 | 85 | | Fill( AuditRole.CreatedDate, nameof( IBaseTrackableEntity.CreatedDate ) ); |
| | 16 | 86 | | Fill( AuditRole.UpdatedDate, nameof( IBaseTrackableEntity.UpdatedDate ) ); |
| | | 87 | | } |
| | | 88 | | |
| | 32 | 89 | | if ( Implements( type, typeof( IBaseTransactionEntity<> ) ) ) |
| | | 90 | | { |
| | 4 | 91 | | Fill( AuditRole.CreatedById, nameof( IBaseTransactionEntity<int>.CreatedById ) ); |
| | 4 | 92 | | Fill( AuditRole.CreatedByUser, nameof( IBaseTransactionEntity<int>.CreatedByUser ) ); |
| | 4 | 93 | | Fill( AuditRole.UpdatedById, nameof( IBaseTransactionEntity<int>.UpdatedById ) ); |
| | 4 | 94 | | Fill( AuditRole.UpdatedByUser, nameof( IBaseTransactionEntity<int>.UpdatedByUser ) ); |
| | | 95 | | } |
| | | 96 | | |
| | 32 | 97 | | if ( Implements( type, typeof( IDeletableEntity<> ) ) ) |
| | | 98 | | { |
| | 12 | 99 | | Fill( AuditRole.SoftDeleteFlag, nameof( IDeletableEntity<int>.IsDeleted ) ); |
| | 12 | 100 | | Fill( AuditRole.DeletedDate, nameof( IDeletableEntity<int>.DeletedDate ) ); |
| | 12 | 101 | | Fill( AuditRole.DeletedById, nameof( IDeletableEntity<int>.DeletedById ) ); |
| | 12 | 102 | | Fill( AuditRole.DeletedByUser, nameof( IDeletableEntity<int>.DeletedByUser ) ); |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | |
| | 32 | 106 | | var map = new AuditMap( type, properties ); |
| | 32 | 107 | | map.Check(); |
| | | 108 | | |
| | 32 | 109 | | return map; |
| | | 110 | | |
| | | 111 | | |
| | | 112 | | // An attribute wins over the interface property of the same role. |
| | | 113 | | void Fill( AuditRole role, string name ) => properties[ (int) role ] ??= candidates.FirstOrDefault( p => p.Name |
| | | 114 | | } |
| | | 115 | | |
| | | 116 | | private static bool Implements( Type type, Type openInterface ) |
| | 64 | 117 | | => type.GetInterfaces().Any( i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface ); |
| | | 118 | | |
| | | 119 | | private void Check() |
| | | 120 | | { |
| | 32 | 121 | | Require( AuditRole.SoftDeleteFlag, t => t == typeof( bool ), "bool" ); |
| | | 122 | | |
| | 256 | 123 | | foreach ( var role in new[] { AuditRole.CreatedDate, AuditRole.UpdatedDate, AuditRole.DeletedDate } ) |
| | | 124 | | { |
| | 96 | 125 | | Require( role, t => t == typeof( DateTime ) || t == typeof( DateTime? ), "DateTime or DateTime?" ); |
| | | 126 | | } |
| | | 127 | | |
| | 256 | 128 | | foreach ( var role in new[] { AuditRole.CreatedByUser, AuditRole.UpdatedByUser, AuditRole.DeletedByUser } ) |
| | | 129 | | { |
| | 96 | 130 | | Require( role, t => t == typeof( string ), "string" ); |
| | | 131 | | } |
| | | 132 | | |
| | | 133 | | |
| | | 134 | | void Require( AuditRole role, Func<Type, bool> valid, string expected ) |
| | | 135 | | { |
| | | 136 | | if ( this[ role ] is { } property && !valid( property.PropertyType ) ) |
| | | 137 | | { |
| | | 138 | | throw new InvalidOperationException( $"{Type.Name}.{property.Name} ({role}) must be {expected}, not {pro |
| | | 139 | | } |
| | | 140 | | } |
| | 32 | 141 | | } |
| | | 142 | | } |