| | | 1 | | using System.Reflection; |
| | | 2 | | using ZhEaIsNsAaBn.Infrastructure.Core; |
| | | 3 | | |
| | | 4 | | // ReSharper disable once CheckNamespace |
| | | 5 | | namespace Microsoft.EntityFrameworkCore; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Set-based updates and deletes that stamp the same audit fields as <see cref="AuditInterceptor{TId}"/> (<c>ExecuteUpd |
| | | 9 | | /// <c>ExecuteDelete</c> skip <c>SaveChanges</c>, so the interceptor never sees them). |
| | | 10 | | /// <para> |
| | | 11 | | /// Each method takes the <c>context</c> the query came from: an EF query doesn't expose its context, and the auditing s |
| | | 12 | | /// (user, clock, hard-delete types) and error classifier belong to it. The context must have auditing (<c>WithAuditing& |
| | | 13 | | /// otherwise <see cref="InvalidOperationException"/>. Database errors come back as <see cref="Error"/> (the <c>SafeExec |
| | | 14 | | /// </para> |
| | | 15 | | /// </summary> |
| | | 16 | | public static class AuditedBulkExtensions |
| | | 17 | | { |
| | | 18 | | /// <summary>Updates the matching rows with <paramref name="setters"/> and stamps <c>UpdatedDate</c> / <c>UpdatedByI |
| | | 19 | | /// <typeparam name="TEntity">The entity type.</typeparam> |
| | | 20 | | /// <param name="query">The rows, e.g. <c>db.Orders.Where( o => o.Expired )</c>.</param> |
| | | 21 | | /// <param name="context">The context the query came from.</param> |
| | | 22 | | /// <param name="setters">The columns to set, e.g. <c>s => s.SetProperty( o => o.Status, "Closed" )</c>.</para |
| | | 23 | | /// <returns>The number of rows updated, or the classified error.</returns> |
| | | 24 | | /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception> |
| | | 25 | | /// <exception cref="InvalidOperationException">The context has no auditing.</exception> |
| | | 26 | | public static LayerResponse<int> AuditedExecuteUpdate< TEntity >( this IQueryable<TEntity> query, DbContext context, |
| | | 27 | | where TEntity : class |
| | | 28 | | { |
| | 8 | 29 | | var all = Update( query, context, setters ); |
| | | 30 | | |
| | 4 | 31 | | return SafeExecution.ExecuteSafe( () => all.Execute( query ), context.GetDbErrorClassifier() ); |
| | | 32 | | } |
| | | 33 | | |
| | | 34 | | /// <inheritdoc cref="AuditedExecuteUpdate{TEntity}" /> |
| | | 35 | | /// <param name="query">The rows.</param> |
| | | 36 | | /// <param name="context">The context the query came from.</param> |
| | | 37 | | /// <param name="setters">The columns to set.</param> |
| | | 38 | | /// <param name="cancellationToken">Token to cancel the command.</param> |
| | | 39 | | public static Task<LayerResponse<int>> AuditedExecuteUpdateAsync< TEntity >( |
| | | 40 | | this IQueryable<TEntity> query, DbContext context, Action<AuditedUpdateSetters<TEntity>> setters, Cancellati |
| | | 41 | | ) |
| | | 42 | | where TEntity : class |
| | | 43 | | { |
| | 8 | 44 | | var all = Update( query, context, setters ); |
| | | 45 | | |
| | 8 | 46 | | return SafeExecution.ExecuteSafeAsync( () => all.ExecuteAsync( query, cancellationToken ), context.GetDbErrorCla |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Deletes the matching rows: a soft delete (flag, <c>DeletedDate</c>, <c>DeletedById</c>, <c>DeletedByUser</c>; ro |
| | | 51 | | /// are left as they are) when <typeparamref name="TEntity"/> has a soft-delete flag and isn't <c>HardDelete<T> |
| | | 52 | | /// <c>ExecuteDelete</c>. |
| | | 53 | | /// </summary> |
| | | 54 | | /// <typeparam name="TEntity">The entity type.</typeparam> |
| | | 55 | | /// <param name="query">The rows.</param> |
| | | 56 | | /// <param name="context">The context the query came from.</param> |
| | | 57 | | /// <returns>The number of rows deleted (or flagged), or the classified error.</returns> |
| | | 58 | | /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception> |
| | | 59 | | /// <exception cref="InvalidOperationException">The context has no auditing.</exception> |
| | | 60 | | public static LayerResponse<int> AuditedExecuteDelete< TEntity >( this IQueryable<TEntity> query, DbContext context |
| | | 61 | | where TEntity : class |
| | | 62 | | { |
| | 4 | 63 | | var plan = Delete( query, context ); |
| | | 64 | | |
| | 4 | 65 | | return SafeExecution.ExecuteSafe( () => plan.Setters is null ? query.ExecuteDelete() : plan.Setters.Execute( pla |
| | | 66 | | } |
| | | 67 | | |
| | | 68 | | /// <inheritdoc cref="AuditedExecuteDelete{TEntity}" /> |
| | | 69 | | /// <param name="query">The rows.</param> |
| | | 70 | | /// <param name="context">The context the query came from.</param> |
| | | 71 | | /// <param name="cancellationToken">Token to cancel the command.</param> |
| | | 72 | | public static Task<LayerResponse<int>> AuditedExecuteDeleteAsync< TEntity >( this IQueryable<TEntity> query, DbConte |
| | | 73 | | where TEntity : class |
| | | 74 | | { |
| | 12 | 75 | | var plan = Delete( query, context ); |
| | | 76 | | |
| | 8 | 77 | | return SafeExecution.ExecuteSafeAsync( () => plan.Setters is null ? query.ExecuteDeleteAsync( cancellationToken |
| | 8 | 78 | | context.GetDbErrorClassifier() ); |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | /// <summary> |
| | | 82 | | /// Brings soft-deleted rows back: clears the flag and the deleted fields and stamps the updated fields. Only flagge |
| | | 83 | | /// <b>The soft-delete query filter hides them</b>, so pass a query without it, e.g. <c>db.Orders.IgnoreQueryFilters |
| | | 84 | | /// </summary> |
| | | 85 | | /// <typeparam name="TEntity">The entity type.</typeparam> |
| | | 86 | | /// <param name="query">The rows.</param> |
| | | 87 | | /// <param name="context">The context the query came from.</param> |
| | | 88 | | /// <returns>The number of rows restored, or the classified error.</returns> |
| | | 89 | | /// <exception cref="ArgumentNullException">An argument is <see langword="null"/>.</exception> |
| | | 90 | | /// <exception cref="InvalidOperationException">The context has no auditing, or <typeparamref name="TEntity"/> has n |
| | | 91 | | public static LayerResponse<int> Restore< TEntity >( this IQueryable<TEntity> query, DbContext context ) |
| | | 92 | | where TEntity : class |
| | | 93 | | { |
| | 8 | 94 | | var (flagged, setters) = RestorePlan( query, context ); |
| | | 95 | | |
| | 4 | 96 | | return SafeExecution.ExecuteSafe( () => setters.Execute( flagged ), context.GetDbErrorClassifier() ); |
| | | 97 | | } |
| | | 98 | | |
| | | 99 | | /// <inheritdoc cref="Restore{TEntity}" /> |
| | | 100 | | /// <param name="query">The rows.</param> |
| | | 101 | | /// <param name="context">The context the query came from.</param> |
| | | 102 | | /// <param name="cancellationToken">Token to cancel the command.</param> |
| | | 103 | | public static Task<LayerResponse<int>> RestoreAsync< TEntity >( this IQueryable<TEntity> query, DbContext context, C |
| | | 104 | | where TEntity : class |
| | | 105 | | { |
| | 4 | 106 | | var (flagged, setters) = RestorePlan( query, context ); |
| | | 107 | | |
| | 4 | 108 | | return SafeExecution.ExecuteSafeAsync( () => setters.ExecuteAsync( flagged, cancellationToken ), context.GetDbEr |
| | | 109 | | } |
| | | 110 | | |
| | | 111 | | |
| | | 112 | | private static AuditedUpdateSetters<TEntity> Update< TEntity >( IQueryable<TEntity> query, DbContext context, Action |
| | | 113 | | where TEntity : class |
| | | 114 | | { |
| | 16 | 115 | | ArgumentNullException.ThrowIfNull( query ); |
| | 16 | 116 | | ArgumentNullException.ThrowIfNull( context ); |
| | 16 | 117 | | ArgumentNullException.ThrowIfNull( setters ); |
| | | 118 | | |
| | 16 | 119 | | var stamper = context.RequireAuditStamper(); |
| | 12 | 120 | | var all = new AuditedUpdateSetters<TEntity>(); |
| | | 121 | | |
| | | 122 | | |
| | 12 | 123 | | setters( all ); |
| | 12 | 124 | | StampUpdated( all, stamper, AuditMap.For( typeof( TEntity ) ), stamper.Stamp() ); |
| | | 125 | | |
| | 12 | 126 | | return all; |
| | | 127 | | } |
| | | 128 | | |
| | | 129 | | private static (IQueryable<TEntity> Query, AuditedUpdateSetters<TEntity>? Setters) Delete< TEntity >( IQueryable<TEn |
| | | 130 | | where TEntity : class |
| | | 131 | | { |
| | 16 | 132 | | ArgumentNullException.ThrowIfNull( query ); |
| | 16 | 133 | | ArgumentNullException.ThrowIfNull( context ); |
| | | 134 | | |
| | 16 | 135 | | var stamper = context.RequireAuditStamper(); |
| | 12 | 136 | | var map = AuditMap.For( typeof( TEntity ) ); |
| | | 137 | | |
| | 20 | 138 | | if ( map.SoftDeleteFlag is not { } flag || stamper.IsHardDelete( typeof( TEntity ) ) ) { return ( query, null ); |
| | | 139 | | |
| | | 140 | | |
| | 4 | 141 | | var stamp = stamper.Stamp(); |
| | 4 | 142 | | var setters = new AuditedUpdateSetters<TEntity>(); |
| | | 143 | | |
| | 4 | 144 | | setters.SetAuditValue( flag, true ); |
| | 4 | 145 | | Stamp( setters, stamper, map, AuditRole.DeletedDate, stamp.Now ); |
| | 4 | 146 | | Stamp( setters, stamper, map, AuditRole.DeletedById, stamp.UserId ); |
| | 4 | 147 | | Stamp( setters, stamper, map, AuditRole.DeletedByUser, stamp.UserName ); |
| | | 148 | | |
| | | 149 | | |
| | 4 | 150 | | return ( query.Where( Flag<TEntity>( flag, isSet: false ) ), setters ); |
| | | 151 | | } |
| | | 152 | | |
| | | 153 | | private static (IQueryable<TEntity> Query, AuditedUpdateSetters<TEntity> Setters) RestorePlan< TEntity >( IQueryable |
| | | 154 | | where TEntity : class |
| | | 155 | | { |
| | 12 | 156 | | ArgumentNullException.ThrowIfNull( query ); |
| | 12 | 157 | | ArgumentNullException.ThrowIfNull( context ); |
| | | 158 | | |
| | 12 | 159 | | var stamper = context.RequireAuditStamper(); |
| | 12 | 160 | | var map = AuditMap.For( typeof( TEntity ) ); |
| | 12 | 161 | | var flag = map.SoftDeleteFlag ?? throw new InvalidOperationException( $"{typeof( TEntity ).Name} has no soft- |
| | 8 | 162 | | var setters = new AuditedUpdateSetters<TEntity>(); |
| | | 163 | | |
| | 8 | 164 | | setters.SetAuditValue( flag, false ); |
| | | 165 | | |
| | 64 | 166 | | foreach ( var role in new[] { AuditRole.DeletedDate, AuditRole.DeletedById, AuditRole.DeletedByUser } ) |
| | | 167 | | { |
| | | 168 | | // Back to null where the column allows it. |
| | 24 | 169 | | if ( map[ role ] is { } property && ( !property.PropertyType.IsValueType || Nullable.GetUnderlyingType( prop |
| | | 170 | | { |
| | 24 | 171 | | setters.SetAuditValue( property, null ); |
| | | 172 | | } |
| | | 173 | | } |
| | | 174 | | |
| | 8 | 175 | | StampUpdated( setters, stamper, map, stamper.Stamp() ); |
| | | 176 | | |
| | | 177 | | |
| | 8 | 178 | | return ( query.Where( Flag<TEntity>( flag, isSet: true ) ), setters ); |
| | | 179 | | } |
| | | 180 | | |
| | | 181 | | private static void StampUpdated< TEntity >( AuditedUpdateSetters<TEntity> setters, IAuditStamper stamper, AuditMap |
| | | 182 | | where TEntity : class |
| | | 183 | | { |
| | 20 | 184 | | Stamp( setters, stamper, map, AuditRole.UpdatedDate, stamp.Now ); |
| | 20 | 185 | | Stamp( setters, stamper, map, AuditRole.UpdatedById, stamp.UserId ); |
| | 20 | 186 | | Stamp( setters, stamper, map, AuditRole.UpdatedByUser, stamp.UserName ); |
| | 20 | 187 | | } |
| | | 188 | | |
| | | 189 | | private static void Stamp< TEntity >( AuditedUpdateSetters<TEntity> setters, IAuditStamper stamper, AuditMap map, Au |
| | | 190 | | where TEntity : class |
| | | 191 | | { |
| | 72 | 192 | | if ( map[ role ] is not { } property ) { return; } |
| | | 193 | | |
| | 96 | 194 | | if ( role is AuditRole.UpdatedById or AuditRole.DeletedById ) { stamper.CheckIdType( typeof( TEntity ), property |
| | | 195 | | |
| | | 196 | | |
| | 72 | 197 | | setters.SetAuditValue( property, value ); |
| | 72 | 198 | | } |
| | | 199 | | |
| | | 200 | | private static Expression<Func<TEntity, bool>> Flag< TEntity >( PropertyInfo flag, bool isSet ) |
| | | 201 | | { |
| | 12 | 202 | | var row = Expression.Parameter( typeof( TEntity ), "e" ); |
| | 12 | 203 | | Expression value = Expression.Property( row, flag ); |
| | | 204 | | |
| | 12 | 205 | | return Expression.Lambda<Func<TEntity, bool>>( isSet ? value : Expression.Not( value ), row ); |
| | | 206 | | } |
| | | 207 | | } |