< Summary

Information
Class: MyNet.Utilities.Execution.ScopedActionRunner
Assembly: MyNet.Utilities
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Execution/ScopedActionRunner.cs
Tag: 323_28699572109
Line coverage
88%
Covered lines: 77
Uncovered lines: 10
Coverable lines: 87
Total lines: 417
Line coverage: 88.5%
Branch coverage
72%
Covered branches: 13
Total branches: 18
Branch coverage: 72.2%
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%11100%
get_LastElapsed()100%210%
RegisterScope(...)100%11100%
RegisterOnStart(...)100%11100%
RegisterOnEnd(...)100%11100%
Unregister(...)100%210%
Run()75%8888.23%
InvokeHandlers(...)100%22100%
EndRun()50%2283.33%
CreateScopes()100%11100%
DisposeScopes()100%22100%
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;
 1221    private readonly Dictionary<object, List<Func<IDisposable>>> _scopeFactories = [];
 1222    private readonly Dictionary<object, List<Action>> _startHandlers = [];
 1223    private readonly Dictionary<object, List<Action>> _endHandlers = [];
 1224    private readonly Stopwatch _stopwatch = new();
 25    private readonly bool _useStopwatch;
 1226    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)
 633        : this(_ =>
 634        {
 635            action();
 636            return true;
 637        },
 638        useStopwatch)
 39    {
 640    }
 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)
 647        : this(complete =>
 648        {
 649            var ended = false;
 650
 651            action(executeComplete);
 652            return !ended;
 653
 654            void executeComplete()
 655            {
 656                if (ended)
 657                    return;
 658
 659                ended = true;
 660                complete();
 661            }
 662        },
 663        useStopwatch)
 64    {
 665    }
 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    {
 1274        _run = run ?? throw new ArgumentNullException(nameof(run));
 1275        _useStopwatch = useStopwatch;
 1276    }
 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>
 086    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    {
 393        ArgumentNullException.ThrowIfNull(subscriber);
 394        ArgumentNullException.ThrowIfNull(createScope);
 395        SubscriberLists.Add(_scopeFactories, subscriber, createScope);
 396    }
 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    {
 3103        ArgumentNullException.ThrowIfNull(subscriber);
 3104        ArgumentNullException.ThrowIfNull(handler);
 3105        SubscriberLists.Add(_startHandlers, subscriber, handler);
 3106    }
 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    {
 3113        ArgumentNullException.ThrowIfNull(subscriber);
 3114        ArgumentNullException.ThrowIfNull(handler);
 3115        SubscriberLists.Add(_endHandlers, subscriber, handler);
 3116    }
 117
 118    /// <summary>
 119    /// Removes all registrations for <paramref name="subscriber"/>.
 120    /// </summary>
 121    public void Unregister(object subscriber)
 122    {
 0123        ArgumentNullException.ThrowIfNull(subscriber);
 0124        _ = _scopeFactories.Remove(subscriber);
 0125        _ = _startHandlers.Remove(subscriber);
 0126        _ = _endHandlers.Remove(subscriber);
 0127    }
 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    {
 15135        ObjectDisposedException.ThrowIf(_disposed, this);
 136
 15137        if (IsRunning)
 3138            throw new InvalidOperationException("A run is already in progress.");
 139
 12140        IsRunning = true;
 12141        InvokeHandlers(_startHandlers);
 142
 12143        _activeScopes = CreateScopes();
 12144        var shouldEnd = true;
 145
 146        try
 147        {
 12148            if (_useStopwatch)
 0149                _stopwatch.Restart();
 150
 12151            shouldEnd = _run(EndRun);
 12152        }
 153        finally
 154        {
 12155            if (_useStopwatch)
 0156                _stopwatch.Stop();
 157
 12158            if (shouldEnd)
 6159                EndRun();
 12160        }
 12161    }
 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    {
 60169        foreach (var handler in handlers.Values.SelectMany(static list => list))
 6170            handler.Invoke();
 24171    }
 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    {
 12178        if (!IsRunning)
 0179            return;
 180
 12181        IsRunning = false;
 12182        InvokeHandlers(_endHandlers);
 12183        DisposeScopes();
 12184    }
 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() =>
 12191        [.. _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    {
 48198        foreach (var scope in _activeScopes)
 3199            scope.Dispose();
 200
 21201        _activeScopes = [];
 21202    }
 203
 204    /// <inheritdoc />
 205    public void Dispose()
 206    {
 9207        if (_disposed)
 0208            return;
 209
 9210        _disposed = true;
 9211        DisposeScopes();
 9212    }
 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;
 221    private readonly Dictionary<object, List<Func<IDisposable>>> _scopeFactories = [];
 222    private readonly Dictionary<object, List<Action<TOut>>> _startHandlers = [];
 223    private readonly Dictionary<object, List<Action<TOut>>> _endHandlers = [];
 224    private readonly Stopwatch _stopwatch = new();
 225    private readonly bool _useStopwatch;
 226    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)
 233        : this((input, _) =>
 234        {
 235            action(input);
 236            return true;
 237        },
 238        useStopwatch)
 239    {
 240    }
 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)
 246        : this((input, complete) =>
 247        {
 248            var ended = false;
 249
 250            action(input, executeComplete);
 251            return !ended;
 252
 253            void executeComplete(TOut value)
 254            {
 255                if (ended)
 256                    return;
 257
 258                ended = true;
 259                complete(value);
 260            }
 261        },
 262        useStopwatch)
 263    {
 264    }
 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    {
 274        _run = run ?? throw new ArgumentNullException(nameof(run));
 275        _useStopwatch = useStopwatch;
 276    }
 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>
 286    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    {
 293        ArgumentNullException.ThrowIfNull(subscriber);
 294        ArgumentNullException.ThrowIfNull(createScope);
 295        SubscriberLists.Add(_scopeFactories, subscriber, createScope);
 296    }
 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    {
 303        ArgumentNullException.ThrowIfNull(subscriber);
 304        ArgumentNullException.ThrowIfNull(handler);
 305        SubscriberLists.Add(_startHandlers, subscriber, handler);
 306    }
 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    {
 313        ArgumentNullException.ThrowIfNull(subscriber);
 314        ArgumentNullException.ThrowIfNull(handler);
 315        SubscriberLists.Add(_endHandlers, subscriber, handler);
 316    }
 317
 318    /// <summary>
 319    /// Removes all registrations for <paramref name="subscriber"/>.
 320    /// </summary>
 321    public void Unregister(object subscriber)
 322    {
 323        ArgumentNullException.ThrowIfNull(subscriber);
 324        _ = _scopeFactories.Remove(subscriber);
 325        _ = _startHandlers.Remove(subscriber);
 326        _ = _endHandlers.Remove(subscriber);
 327    }
 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    {
 334        ObjectDisposedException.ThrowIf(_disposed, this);
 335        ArgumentNullException.ThrowIfNull(resultFactory);
 336
 337        if (IsRunning)
 338            throw new InvalidOperationException("A run is already in progress.");
 339
 340        var result = resultFactory();
 341        IsRunning = true;
 342        InvokeHandlers(_startHandlers, result);
 343
 344        _activeScopes = CreateScopes();
 345        var shouldEnd = true;
 346
 347        try
 348        {
 349            if (_useStopwatch)
 350                _stopwatch.Restart();
 351
 352            shouldEnd = _run(input, EndRun);
 353        }
 354        finally
 355        {
 356            if (_useStopwatch)
 357                _stopwatch.Stop();
 358
 359            if (shouldEnd)
 360                EndRun(result);
 361        }
 362    }
 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    {
 371        foreach (var handler in handlers.Values.SelectMany(static list => list))
 372            handler.Invoke(value);
 373    }
 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    {
 381        if (!IsRunning)
 382            return;
 383
 384        IsRunning = false;
 385        InvokeHandlers(_endHandlers, result);
 386        DisposeScopes();
 387    }
 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() =>
 394        [.. _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    {
 401        foreach (var scope in _activeScopes)
 402            scope.Dispose();
 403
 404        _activeScopes = [];
 405    }
 406
 407    /// <inheritdoc />
 408    public void Dispose()
 409    {
 410        if (_disposed)
 411            return;
 412
 413        _disposed = true;
 414        DisposeScopes();
 415    }
 416}
 417