< Summary

Information
Class: MyNet.Utilities.Execution.ScopedActionRunner<T1, T2>
Assembly: MyNet.Utilities
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Execution/ScopedActionRunner.cs
Tag: 323_28699572109
Line coverage
61%
Covered lines: 55
Uncovered lines: 34
Coverable lines: 89
Total lines: 417
Line coverage: 61.7%
Branch coverage
62%
Covered branches: 15
Total branches: 24
Branch coverage: 62.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
.ctor(...)100%11100%
.ctor(...)100%210%
get_LastElapsed()100%210%
RegisterScope(...)100%210%
RegisterOnStart(...)100%11100%
RegisterOnEnd(...)100%11100%
Unregister(...)100%210%
Run(...)62.5%8884.21%
InvokeHandlers(...)100%44100%
EndRun(...)50%2283.33%
CreateScopes()50%44100%
DisposeScopes()50%2275%
Dispose()50%2280%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Execution/ScopedActionRunner.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ScopedActionRunner.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Diagnostics;
 10using System.Linq;
 11
 12namespace MyNet.Utilities.Execution;
 13
 14/// <summary>
 15/// Runs an action with disposable scopes active for the duration, optional start/end hooks per subscriber,
 16/// and optional manual completion for work that outlives the initial call stack.
 17/// </summary>
 18public sealed class ScopedActionRunner : IDisposable
 19{
 20    private readonly Func<Action, bool> _run;
 21    private readonly Dictionary<object, List<Func<IDisposable>>> _scopeFactories = [];
 22    private readonly Dictionary<object, List<Action>> _startHandlers = [];
 23    private readonly Dictionary<object, List<Action>> _endHandlers = [];
 24    private readonly Stopwatch _stopwatch = new();
 25    private readonly bool _useStopwatch;
 26    private List<IDisposable> _activeScopes = [];
 27    private bool _disposed;
 28
 29    /// <summary>
 30    /// Initializes a new instance of the <see cref="ScopedActionRunner"/> class.
 31    /// </summary>
 32    public ScopedActionRunner(Action action, bool useStopwatch = false)
 33        : this(_ =>
 34        {
 35            action();
 36            return true;
 37        },
 38        useStopwatch)
 39    {
 40    }
 41
 42    /// <summary>
 43    /// Initializes a new instance of the <see cref="ScopedActionRunner"/> class.
 44    /// The run ends when the callback is invoked, or automatically when the delegate returns if it did not invoke the c
 45    /// </summary>
 46    public ScopedActionRunner(Action<Action> action, bool useStopwatch = false)
 47        : this(complete =>
 48        {
 49            var ended = false;
 50
 51            action(executeComplete);
 52            return !ended;
 53
 54            void executeComplete()
 55            {
 56                if (ended)
 57                    return;
 58
 59                ended = true;
 60                complete();
 61            }
 62        },
 63        useStopwatch)
 64    {
 65    }
 66
 67    /// <summary>
 68    /// Initializes a new instance of the <see cref="ScopedActionRunner"/> class with a custom run implementation.
 69    /// </summary>
 70    /// <param name="run">A function that executes the action and returns a boolean indicating whether the run should en
 71    /// <param name="useStopwatch">A value indicating whether to use a stopwatch to measure the elapsed time of the run.
 72    private ScopedActionRunner(Func<Action, bool> run, bool useStopwatch)
 73    {
 74        _run = run ?? throw new ArgumentNullException(nameof(run));
 75        _useStopwatch = useStopwatch;
 76    }
 77
 78    /// <summary>
 79    /// Gets a value indicating whether a run is in progress.
 80    /// </summary>
 81    public bool IsRunning { get; private set; }
 82
 83    /// <summary>
 84    /// Gets the elapsed time of the last completed run when stopwatch timing was enabled in the constructor.
 85    /// </summary>
 86    public TimeSpan LastElapsed => _stopwatch.Elapsed;
 87
 88    /// <summary>
 89    /// Registers a factory that creates a disposable scope for each run.
 90    /// </summary>
 91    public void RegisterScope(object subscriber, Func<IDisposable> createScope)
 92    {
 93        ArgumentNullException.ThrowIfNull(subscriber);
 94        ArgumentNullException.ThrowIfNull(createScope);
 95        SubscriberLists.Add(_scopeFactories, subscriber, createScope);
 96    }
 97
 98    /// <summary>
 99    /// Registers a handler invoked at the start of each run.
 100    /// </summary>
 101    public void RegisterOnStart(object subscriber, Action handler)
 102    {
 103        ArgumentNullException.ThrowIfNull(subscriber);
 104        ArgumentNullException.ThrowIfNull(handler);
 105        SubscriberLists.Add(_startHandlers, subscriber, handler);
 106    }
 107
 108    /// <summary>
 109    /// Registers a handler invoked at the end of each run.
 110    /// </summary>
 111    public void RegisterOnEnd(object subscriber, Action handler)
 112    {
 113        ArgumentNullException.ThrowIfNull(subscriber);
 114        ArgumentNullException.ThrowIfNull(handler);
 115        SubscriberLists.Add(_endHandlers, subscriber, handler);
 116    }
 117
 118    /// <summary>
 119    /// Removes all registrations for <paramref name="subscriber"/>.
 120    /// </summary>
 121    public void Unregister(object subscriber)
 122    {
 123        ArgumentNullException.ThrowIfNull(subscriber);
 124        _ = _scopeFactories.Remove(subscriber);
 125        _ = _startHandlers.Remove(subscriber);
 126        _ = _endHandlers.Remove(subscriber);
 127    }
 128
 129    /// <summary>
 130    /// Executes the bound action when no other run is active.
 131    /// </summary>
 132    /// <exception cref="InvalidOperationException">Thrown when a run is already in progress.</exception>
 133    public void Run()
 134    {
 135        ObjectDisposedException.ThrowIf(_disposed, this);
 136
 137        if (IsRunning)
 138            throw new InvalidOperationException("A run is already in progress.");
 139
 140        IsRunning = true;
 141        InvokeHandlers(_startHandlers);
 142
 143        _activeScopes = CreateScopes();
 144        var shouldEnd = true;
 145
 146        try
 147        {
 148            if (_useStopwatch)
 149                _stopwatch.Restart();
 150
 151            shouldEnd = _run(EndRun);
 152        }
 153        finally
 154        {
 155            if (_useStopwatch)
 156                _stopwatch.Stop();
 157
 158            if (shouldEnd)
 159                EndRun();
 160        }
 161    }
 162
 163    /// <summary>
 164    /// Invokes all handlers in the given dictionary, which may be either start or end handlers depending on the context
 165    /// </summary>
 166    /// <param name="handlers">The dictionary of handlers to invoke.</param>
 167    private static void InvokeHandlers(Dictionary<object, List<Action>> handlers)
 168    {
 169        foreach (var handler in handlers.Values.SelectMany(static list => list))
 170            handler.Invoke();
 171    }
 172
 173    /// <summary>
 174    /// Ends the current run, invoking end handlers and disposing scopes. Safe to call multiple times and from any conte
 175    /// </summary>
 176    private void EndRun()
 177    {
 178        if (!IsRunning)
 179            return;
 180
 181        IsRunning = false;
 182        InvokeHandlers(_endHandlers);
 183        DisposeScopes();
 184    }
 185
 186    /// <summary>
 187    /// Creates the disposable scopes for the current run by invoking all registered scope factories. The resulting list
 188    /// </summary>
 189    /// <returns>A list of disposable scopes for the current run.</returns>
 190    private List<IDisposable> CreateScopes() =>
 191        [.. _scopeFactories.Values.SelectMany(static factories => factories).Select(static factory => factory.Invoke())]
 192
 193    /// <summary>
 194    /// Disposes all active scopes for the current run, ensuring that any resources associated with the scopes are prope
 195    /// </summary>
 196    private void DisposeScopes()
 197    {
 198        foreach (var scope in _activeScopes)
 199            scope.Dispose();
 200
 201        _activeScopes = [];
 202    }
 203
 204    /// <inheritdoc />
 205    public void Dispose()
 206    {
 207        if (_disposed)
 208            return;
 209
 210        _disposed = true;
 211        DisposeScopes();
 212    }
 213}
 214
 215/// <summary>
 216/// Runs an action with input and result payloads, disposable scopes, and optional manual completion.
 217/// </summary>
 218public sealed class ScopedActionRunner<TIn, TOut> : IDisposable
 219{
 220    private readonly Func<TIn, Action<TOut>, bool> _run;
 3221    private readonly Dictionary<object, List<Func<IDisposable>>> _scopeFactories = [];
 3222    private readonly Dictionary<object, List<Action<TOut>>> _startHandlers = [];
 3223    private readonly Dictionary<object, List<Action<TOut>>> _endHandlers = [];
 3224    private readonly Stopwatch _stopwatch = new();
 225    private readonly bool _useStopwatch;
 3226    private List<IDisposable> _activeScopes = [];
 227    private bool _disposed;
 228
 229    /// <summary>
 230    /// Initializes a new instance of the <see cref="ScopedActionRunner{TIn, TOut}"/> class.
 231    /// </summary>
 232    public ScopedActionRunner(Action<TIn> action, bool useStopwatch = false)
 3233        : this((input, _) =>
 3234        {
 3235            action(input);
 3236            return true;
 3237        },
 3238        useStopwatch)
 239    {
 3240    }
 241
 242    /// <summary>
 243    /// Initializes a new instance of the <see cref="ScopedActionRunner{TIn, TOut}"/> class.
 244    /// </summary>
 245    public ScopedActionRunner(Action<TIn, Action<TOut>> action, bool useStopwatch = false)
 0246        : this((input, complete) =>
 0247        {
 0248            var ended = false;
 0249
 0250            action(input, executeComplete);
 0251            return !ended;
 0252
 0253            void executeComplete(TOut value)
 0254            {
 0255                if (ended)
 0256                    return;
 0257
 0258                ended = true;
 0259                complete(value);
 0260            }
 0261        },
 0262        useStopwatch)
 263    {
 0264    }
 265
 266    /// <summary>
 267    /// Initializes a new instance of the <see cref="ScopedActionRunner{TIn, TOut}"/> class with a custom run implementa
 268    /// </summary>
 269    /// <param name="run">The custom run implementation.</param>
 270    /// <param name="useStopwatch">Indicates whether to use a stopwatch to measure elapsed time.</param>
 271    /// <exception cref="ArgumentNullException">Thrown if <paramref name="run"/> is null.</exception>
 272    private ScopedActionRunner(Func<TIn, Action<TOut>, bool> run, bool useStopwatch)
 273    {
 3274        _run = run ?? throw new ArgumentNullException(nameof(run));
 3275        _useStopwatch = useStopwatch;
 3276    }
 277
 278    /// <summary>
 279    /// Gets a value indicating whether a run is in progress.
 280    /// </summary>
 281    public bool IsRunning { get; private set; }
 282
 283    /// <summary>
 284    /// Gets the elapsed time of the last completed run when stopwatch timing was enabled.
 285    /// </summary>
 0286    public TimeSpan LastElapsed => _stopwatch.Elapsed;
 287
 288    /// <summary>
 289    /// Registers a factory that creates a disposable scope for each run.
 290    /// </summary>
 291    public void RegisterScope(object subscriber, Func<IDisposable> createScope)
 292    {
 0293        ArgumentNullException.ThrowIfNull(subscriber);
 0294        ArgumentNullException.ThrowIfNull(createScope);
 0295        SubscriberLists.Add(_scopeFactories, subscriber, createScope);
 0296    }
 297
 298    /// <summary>
 299    /// Registers a handler invoked at the start of each run with the initial result value.
 300    /// </summary>
 301    public void RegisterOnStart(object subscriber, Action<TOut> handler)
 302    {
 3303        ArgumentNullException.ThrowIfNull(subscriber);
 3304        ArgumentNullException.ThrowIfNull(handler);
 3305        SubscriberLists.Add(_startHandlers, subscriber, handler);
 3306    }
 307
 308    /// <summary>
 309    /// Registers a handler invoked at the end of each run with the final result value.
 310    /// </summary>
 311    public void RegisterOnEnd(object subscriber, Action<TOut> handler)
 312    {
 3313        ArgumentNullException.ThrowIfNull(subscriber);
 3314        ArgumentNullException.ThrowIfNull(handler);
 3315        SubscriberLists.Add(_endHandlers, subscriber, handler);
 3316    }
 317
 318    /// <summary>
 319    /// Removes all registrations for <paramref name="subscriber"/>.
 320    /// </summary>
 321    public void Unregister(object subscriber)
 322    {
 0323        ArgumentNullException.ThrowIfNull(subscriber);
 0324        _ = _scopeFactories.Remove(subscriber);
 0325        _ = _startHandlers.Remove(subscriber);
 0326        _ = _endHandlers.Remove(subscriber);
 0327    }
 328
 329    /// <summary>
 330    /// Executes the bound action when no other run is active.
 331    /// </summary>
 332    public void Run(TIn input, Func<TOut> resultFactory)
 333    {
 3334        ObjectDisposedException.ThrowIf(_disposed, this);
 3335        ArgumentNullException.ThrowIfNull(resultFactory);
 336
 3337        if (IsRunning)
 0338            throw new InvalidOperationException("A run is already in progress.");
 339
 3340        var result = resultFactory();
 3341        IsRunning = true;
 3342        InvokeHandlers(_startHandlers, result);
 343
 3344        _activeScopes = CreateScopes();
 3345        var shouldEnd = true;
 346
 347        try
 348        {
 3349            if (_useStopwatch)
 0350                _stopwatch.Restart();
 351
 3352            shouldEnd = _run(input, EndRun);
 3353        }
 354        finally
 355        {
 3356            if (_useStopwatch)
 0357                _stopwatch.Stop();
 358
 3359            if (shouldEnd)
 3360                EndRun(result);
 3361        }
 3362    }
 363
 364    /// <summary>
 365    /// Invokes all handlers in the given dictionary with the provided value, which may be either start or end handlers 
 366    /// </summary>
 367    /// <param name="handlers">The dictionary of handlers to invoke.</param>
 368    /// <param name="value">The value to pass to each handler.</param>
 369    private static void InvokeHandlers(Dictionary<object, List<Action<TOut>>> handlers, TOut value)
 370    {
 24371        foreach (var handler in handlers.Values.SelectMany(static list => list))
 6372            handler.Invoke(value);
 6373    }
 374
 375    /// <summary>
 376    /// Ends the current run with the given result, invoking end handlers and disposing scopes. Safe to call multiple ti
 377    /// </summary>
 378    /// <param name="result">The result of the run.</param>
 379    private void EndRun(TOut result)
 380    {
 3381        if (!IsRunning)
 0382            return;
 383
 3384        IsRunning = false;
 3385        InvokeHandlers(_endHandlers, result);
 3386        DisposeScopes();
 3387    }
 388
 389    /// <summary>
 390    /// Creates the disposable scopes for the current run by invoking all registered scope factories. The resulting list
 391    /// </summary>
 392    /// <returns>The list of active scopes for the current run.</returns>
 393    private List<IDisposable> CreateScopes() =>
 3394        [.. _scopeFactories.Values.SelectMany(static factories => factories).Select(static factory => factory.Invoke())]
 395
 396    /// <summary>
 397    /// Disposes all active scopes for the current run, ensuring that any resources associated with the scopes are prope
 398    /// </summary>
 399    private void DisposeScopes()
 400    {
 12401        foreach (var scope in _activeScopes)
 0402            scope.Dispose();
 403
 6404        _activeScopes = [];
 6405    }
 406
 407    /// <inheritdoc />
 408    public void Dispose()
 409    {
 3410        if (_disposed)
 0411            return;
 412
 3413        _disposed = true;
 3414        DisposeScopes();
 3415    }
 416}
 417