< Summary

Information
Class: MyNet.Observable.Behaviors.BehaviorKey
Assembly: MyNet.Observable
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Observable/Behaviors/BehaviorRegistry.cs
Tag: 323_28699572109
Line coverage
100%
Covered lines: 5
Uncovered lines: 0
Coverable lines: 5
Total lines: 304
Line coverage: 100%
Branch coverage
71%
Covered branches: 10
Total branches: 14
Branch coverage: 71.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Normalize(...)100%22100%
Create(...)100%11100%
Matches(...)66.66%1212100%

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{
 21    private readonly Dictionary<BehaviorKey, IObservableBehavior> _behaviors = [];
 22    private readonly List<BehaviorKey> _registrationOrder = [];
 23    private readonly Lock _gate = new();
 24    private bool _disposed;
 25
 26    private static void DisposeBehavior(IObservableBehavior behavior)
 27    {
 28        if (behavior is IDisposable disposable)
 29            disposable.Dispose();
 30    }
 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
 48        {
 49            lock (_gate)
 50            {
 51                return [.. _registrationOrder.Select(k => _behaviors[k])];
 52            }
 53        }
 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    {
 63        ArgumentNullException.ThrowIfNull(behavior);
 64        ThrowIfDisposed();
 65
 66        lock (_gate)
 67        {
 68            var key = BehaviorKey.Create(behavior.GetType(), scope, propertyName);
 69
 70            if (_behaviors.TryGetValue(key, out var existing) && !ReferenceEquals(existing, behavior))
 71                DisposeBehavior(existing);
 72
 73            if (!_behaviors.ContainsKey(key))
 74                _registrationOrder.Add(key);
 75
 76            _behaviors[key] = behavior;
 77            RebuildPipeline();
 78        }
 79    }
 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    {
 87        ThrowIfDisposed();
 88
 89        lock (_gate)
 90        {
 91            var key = BehaviorKey.Create(typeof(T), scope, propertyName);
 92
 93            if (!_behaviors.Remove(key, out var existing))
 94                return false;
 95
 96            _registrationOrder.Remove(key);
 97            DisposeBehavior(existing);
 98            RebuildPipeline();
 99            return true;
 100        }
 101    }
 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
 112    {
 113        lock (_gate)
 114        {
 115            var key = BehaviorKey.Create(typeof(T), scope, propertyName);
 116
 117            if (_behaviors.TryGetValue(key, out var exact) && exact is T typed)
 118            {
 119                behavior = typed;
 120                return true;
 121            }
 122
 123            var matches = CollectMatches<T>(propertyName, scope);
 124
 125            if (matches.Count == 1)
 126            {
 127                behavior = matches[0];
 128                return true;
 129            }
 130
 131            behavior = null;
 132            return false;
 133        }
 134    }
 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
 141    {
 142        lock (_gate)
 143        {
 144            return [.. CollectMatches<T>(propertyName, scope)];
 145        }
 146    }
 147
 148    public T Get<T>(string? propertyName = null, string? scope = null)
 149        where T : class, IObservableBehavior
 150        => TryGet<T>(out var b, propertyName, scope)
 151            ? b
 152            : throw new InvalidOperationException(
 153                $"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
 157        => TryGet<T>(out _, propertyName, scope);
 158
 159    public T? GetOrDefault<T>(string? propertyName = null, string? scope = null)
 160        where T : class, IObservableBehavior
 161    {
 162        TryGet<T>(out var behavior, propertyName, scope);
 163        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    {
 173        ArgumentNullException.ThrowIfNull(action);
 174
 175        if (!TryGet<T>(out var behavior, propertyName, scope))
 176            return false;
 177
 178        action(behavior);
 179        return true;
 180    }
 181
 182    public void Execute<T>(Action<T> action, string? propertyName = null, string? scope = null)
 183        where T : class, IObservableBehavior
 184    {
 185        ArgumentNullException.ThrowIfNull(action);
 186        action(Get<T>(propertyName, scope));
 187    }
 188
 189    public bool TryEvaluate<TBehavior, TResult>(Func<TBehavior, TResult> selector, [MaybeNullWhen(false)] out TResult re
 190        where TBehavior : class, IObservableBehavior
 191    {
 192        ArgumentNullException.ThrowIfNull(selector);
 193
 194        if (!TryGet<TBehavior>(out var behavior, propertyName, scope))
 195        {
 196            result = default;
 197            return false;
 198        }
 199
 200        result = selector(behavior);
 201        return true;
 202    }
 203
 204    public TResult Evaluate<TBehavior, TResult>(Func<TBehavior, TResult> selector, TResult defaultValue = default!, stri
 205        where TBehavior : class, IObservableBehavior
 206    {
 207        ArgumentNullException.ThrowIfNull(selector);
 208        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    {
 217        var changing = new List<IPropertyChangingBehavior>();
 218        var changed = new List<IPropertyChangedBehavior>();
 219
 220        foreach (var behavior in _registrationOrder.Select(key => _behaviors[key]))
 221        {
 222            if (behavior is IPropertyChangingBehavior changingBehavior)
 223                changing.Add(changingBehavior);
 224
 225            if (behavior is IPropertyChangedBehavior changedBehavior)
 226                changed.Add(changedBehavior);
 227        }
 228
 229        Changing = [.. changing];
 230        Changed = [.. changed];
 231    }
 232
 233    private List<T> CollectMatches<T>(string? propertyName, string? scope)
 234        where T : class, IObservableBehavior
 235    {
 236        var matches = new List<T>();
 237
 238        foreach (var entry in _behaviors)
 239        {
 240            if (entry.Value is not T typed)
 241                continue;
 242
 243            if (!entry.Key.Matches(scope, propertyName))
 244                continue;
 245
 246            matches.Add(typed);
 247        }
 248
 249        return matches;
 250    }
 251
 252    #endregion
 253
 254    public void Dispose()
 255    {
 256        if (_disposed)
 257            return;
 258
 259        _disposed = true;
 260
 261        lock (_gate)
 262        {
 263            foreach (var behavior in _behaviors.Values.OfType<IDisposable>())
 264                behavior.Dispose();
 265
 266            _behaviors.Clear();
 267            _registrationOrder.Clear();
 268            RebuildPipeline();
 269        }
 270    }
 271
 272    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{
 1410284    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)
 654290        => 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    {
 51298        var normalizedScope = Normalize(scope);
 51299        var normalizedProperty = Normalize(propertyName);
 300
 51301        return normalizedScope is null && normalizedProperty is null ? Scope is null && PropertyName is null : (normaliz
 302    }
 303}
 304