< Summary

Information
Class: MyNet.Observable.Behaviors.BehaviorRegistry
Assembly: MyNet.Observable
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Behaviors/BehaviorRegistry.cs
Tag: 323_28699572109
Line coverage
91%
Covered lines: 86
Uncovered lines: 8
Coverable lines: 94
Total lines: 304
Line coverage: 91.4%
Branch coverage
75%
Covered branches: 33
Total branches: 44
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
DisposeBehavior(...)50%22100%
get_All()100%210%
Register(...)100%66100%
Unregister(...)50%2288.88%
TryGet(...)100%66100%
GetAll(...)100%11100%
Get(...)16.66%66100%
Has(...)100%11100%
GetOrDefault(...)100%11100%
TryExecute(...)50%2280%
Execute(...)100%11100%
TryEvaluate(...)50%2266.66%
Evaluate(...)50%22100%
RebuildPipeline()100%66100%
CollectMatches(...)100%66100%
Dispose()75%4490%
ThrowIfDisposed()100%11100%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Behaviors/BehaviorRegistry.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="BehaviorRegistry.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.CodeAnalysis;
 10using System.Linq;
 11using System.Threading;
 12
 13namespace MyNet.Observable.Behaviors;
 14
 15/// <summary>
 16/// Registry for behaviors attached to an <see cref="ObservableObject"/>.
 17/// Each entry is keyed by <see cref="BehaviorKey"/>; registration order defines pipeline execution order.
 18/// </summary>
 19internal sealed class BehaviorRegistry : IDisposable
 20{
 193521    private readonly Dictionary<BehaviorKey, IObservableBehavior> _behaviors = [];
 193522    private readonly List<BehaviorKey> _registrationOrder = [];
 193523    private readonly Lock _gate = new();
 24    private bool _disposed;
 25
 26    private static void DisposeBehavior(IObservableBehavior behavior)
 27    {
 3028        if (behavior is IDisposable disposable)
 3029            disposable.Dispose();
 3030    }
 31
 32    /// <summary>
 33    /// Gets property-changing behaviors in registration order.
 34    /// </summary>
 35    public IPropertyChangingBehavior[] Changing { get; private set; } = [];
 36
 37    /// <summary>
 38    /// Gets property-changed behaviors in registration order.
 39    /// </summary>
 40    public IPropertyChangedBehavior[] Changed { get; private set; } = [];
 41
 42    /// <summary>
 43    /// Gets all registered behavior instances in registration order.
 44    /// </summary>
 45    public IObservableBehavior[] All
 46    {
 47        get
 048        {
 49            lock (_gate)
 50            {
 051                return [.. _registrationOrder.Select(k => _behaviors[k])];
 52            }
 053        }
 54    }
 55
 56    #region Register / unregister
 57
 58    /// <summary>
 59    /// Registers or replaces a behavior for the computed key. Previous instances are disposed when <see cref="IDisposab
 60    /// </summary>
 61    public void Register(IObservableBehavior behavior, string? propertyName = null, string? scope = null)
 62    {
 42663        ArgumentNullException.ThrowIfNull(behavior);
 42664        ThrowIfDisposed();
 65
 66        lock (_gate)
 67        {
 42668            var key = BehaviorKey.Create(behavior.GetType(), scope, propertyName);
 69
 42670            if (_behaviors.TryGetValue(key, out var existing) && !ReferenceEquals(existing, behavior))
 2771                DisposeBehavior(existing);
 72
 42673            if (!_behaviors.ContainsKey(key))
 39974                _registrationOrder.Add(key);
 75
 42676            _behaviors[key] = behavior;
 42677            RebuildPipeline();
 42678        }
 42679    }
 80
 81    /// <summary>
 82    /// Removes a behavior for the specified key and disposes it when <see cref="IDisposable"/>.
 83    /// </summary>
 84    public bool Unregister<T>(string? propertyName = null, string? scope = null)
 85        where T : class, IObservableBehavior
 86    {
 387        ThrowIfDisposed();
 88
 89        lock (_gate)
 90        {
 391            var key = BehaviorKey.Create(typeof(T), scope, propertyName);
 92
 393            if (!_behaviors.Remove(key, out var existing))
 094                return false;
 95
 396            _registrationOrder.Remove(key);
 397            DisposeBehavior(existing);
 398            RebuildPipeline();
 399            return true;
 100        }
 3101    }
 102
 103    #endregion
 104
 105    #region Lookup
 106
 107    /// <summary>
 108    /// Tries to get a behavior using an exact or filtered key match. Returns false when zero or more than one instance 
 109    /// </summary>
 110    public bool TryGet<T>([NotNullWhen(true)] out T? behavior, string? propertyName = null, string? scope = null)
 111        where T : class, IObservableBehavior
 225112    {
 113        lock (_gate)
 114        {
 225115            var key = BehaviorKey.Create(typeof(T), scope, propertyName);
 116
 225117            if (_behaviors.TryGetValue(key, out var exact) && exact is T typed)
 118            {
 96119                behavior = typed;
 96120                return true;
 121            }
 122
 129123            var matches = CollectMatches<T>(propertyName, scope);
 124
 129125            if (matches.Count == 1)
 126            {
 39127                behavior = matches[0];
 39128                return true;
 129            }
 130
 90131            behavior = null;
 90132            return false;
 133        }
 225134    }
 135
 136    /// <summary>
 137    /// Gets all registered behaviors assignable to <typeparamref name="T"/> that match the optional scope and property 
 138    /// </summary>
 139    public T[] GetAll<T>(string? propertyName = null, string? scope = null)
 140        where T : class, IObservableBehavior
 3141    {
 142        lock (_gate)
 143        {
 3144            return [.. CollectMatches<T>(propertyName, scope)];
 145        }
 3146    }
 147
 148    public T Get<T>(string? propertyName = null, string? scope = null)
 149        where T : class, IObservableBehavior
 30150        => TryGet<T>(out var b, propertyName, scope)
 30151            ? b
 30152            : throw new InvalidOperationException(
 30153                $"Behavior {typeof(T).Name} not found for propertyName '{propertyName ?? "<null>"}' and scope '{scope ??
 154
 155    public bool Has<T>(string? propertyName = null, string? scope = null)
 156        where T : class, IObservableBehavior
 15157        => TryGet<T>(out _, propertyName, scope);
 158
 159    public T? GetOrDefault<T>(string? propertyName = null, string? scope = null)
 160        where T : class, IObservableBehavior
 161    {
 33162        TryGet<T>(out var behavior, propertyName, scope);
 33163        return behavior;
 164    }
 165
 166    #endregion
 167
 168    #region Api
 169
 170    public bool TryExecute<T>(Action<T> action, string? propertyName = null, string? scope = null)
 171        where T : class, IObservableBehavior
 172    {
 3173        ArgumentNullException.ThrowIfNull(action);
 174
 3175        if (!TryGet<T>(out var behavior, propertyName, scope))
 0176            return false;
 177
 3178        action(behavior);
 3179        return true;
 180    }
 181
 182    public void Execute<T>(Action<T> action, string? propertyName = null, string? scope = null)
 183        where T : class, IObservableBehavior
 184    {
 3185        ArgumentNullException.ThrowIfNull(action);
 3186        action(Get<T>(propertyName, scope));
 3187    }
 188
 189    public bool TryEvaluate<TBehavior, TResult>(Func<TBehavior, TResult> selector, [MaybeNullWhen(false)] out TResult re
 190        where TBehavior : class, IObservableBehavior
 191    {
 3192        ArgumentNullException.ThrowIfNull(selector);
 193
 3194        if (!TryGet<TBehavior>(out var behavior, propertyName, scope))
 195        {
 0196            result = default;
 0197            return false;
 198        }
 199
 3200        result = selector(behavior);
 3201        return true;
 202    }
 203
 204    public TResult Evaluate<TBehavior, TResult>(Func<TBehavior, TResult> selector, TResult defaultValue = default!, stri
 205        where TBehavior : class, IObservableBehavior
 206    {
 3207        ArgumentNullException.ThrowIfNull(selector);
 3208        return TryGet<TBehavior>(out var behavior, propertyName, scope) ? selector(behavior) : defaultValue;
 209    }
 210
 211    #endregion
 212
 213    #region Pipeline
 214
 215    private void RebuildPipeline()
 216    {
 732217        var changing = new List<IPropertyChangingBehavior>();
 732218        var changed = new List<IPropertyChangedBehavior>();
 219
 2442220        foreach (var behavior in _registrationOrder.Select(key => _behaviors[key]))
 221        {
 489222            if (behavior is IPropertyChangingBehavior changingBehavior)
 21223                changing.Add(changingBehavior);
 224
 489225            if (behavior is IPropertyChangedBehavior changedBehavior)
 291226                changed.Add(changedBehavior);
 227        }
 228
 732229        Changing = [.. changing];
 732230        Changed = [.. changed];
 732231    }
 232
 233    private List<T> CollectMatches<T>(string? propertyName, string? scope)
 234        where T : class, IObservableBehavior
 235    {
 132236        var matches = new List<T>();
 237
 420238        foreach (var entry in _behaviors)
 239        {
 78240            if (entry.Value is not T typed)
 241                continue;
 242
 51243            if (!entry.Key.Matches(scope, propertyName))
 244                continue;
 245
 45246            matches.Add(typed);
 247        }
 248
 132249        return matches;
 250    }
 251
 252    #endregion
 253
 254    public void Dispose()
 255    {
 303256        if (_disposed)
 0257            return;
 258
 303259        _disposed = true;
 260
 261        lock (_gate)
 262        {
 624263            foreach (var behavior in _behaviors.Values.OfType<IDisposable>())
 9264                behavior.Dispose();
 265
 303266            _behaviors.Clear();
 303267            _registrationOrder.Clear();
 303268            RebuildPipeline();
 303269        }
 303270    }
 271
 429272    private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, nameof(BehaviorRegistry));
 273}
 274
 275/// <summary>
 276/// Identifies a behavior instance in <see cref="BehaviorRegistry"/>.
 277/// </summary>
 278/// <param name="BehaviorType">Concrete behavior type used at registration.</param>
 279/// <param name="Scope">Optional scope discriminator (for example behavior kind name).</param>
 280/// <param name="PropertyName">Optional property name the behavior is bound to.</param>
 281[SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Global", Justification = "Properties are used for equality 
 282public readonly record struct BehaviorKey(Type BehaviorType, string? Scope = null, string? PropertyName = null)
 283{
 284    private static string? Normalize(string? value) => string.IsNullOrEmpty(value) ? null : value;
 285
 286    /// <summary>
 287    /// Creates a normalized key for the given behavior type and optional scope/property identifiers.
 288    /// </summary>
 289    public static BehaviorKey Create(Type behaviorType, string? scope = null, string? propertyName = null)
 290        => new(behaviorType, Normalize(scope), Normalize(propertyName));
 291
 292    /// <summary>
 293    /// Returns whether this key matches the optional scope and property filters.
 294    /// When both filters are null, only keys without scope and property name match.
 295    /// </summary>
 296    public bool Matches(string? scope, string? propertyName)
 297    {
 298        var normalizedScope = Normalize(scope);
 299        var normalizedProperty = Normalize(propertyName);
 300
 301        return normalizedScope is null && normalizedProperty is null ? Scope is null && PropertyName is null : (normaliz
 302    }
 303}
 304