< Summary

Information
Class: AmbientServices.InitializationErrorEventArgs
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/AmbientService.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 2
Uncovered lines: 0
Coverable lines: 2
Total lines: 525
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/AmbientService.cs

#LineLine coverage
 1using System;
 2using System.Reflection;
 3using System.Threading;
 4#if NETCOREAPP3_0_OR_GREATER
 5using System.Diagnostics.CodeAnalysis;
 6#endif
 7
 8namespace AmbientServices;
 9
 10/// <summary>
 11/// A class that contains information about an initialization error.
 12/// </summary>
 13/// <param name="exception">The <see cref="Exception"/> that caused the initialization error.</param>
 214public class InitializationErrorEventArgs(Exception exception) : EventArgs
 15{
 16    /// <summary>
 17    /// The <see cref="Exception"/> that caused the initialization error.
 18    /// </summary>
 219    public Exception Exception { get; } = exception;
 20}
 21/// <summary>
 22/// A static class that provides access to <see cref="AmbientService{T}"/>s.
 23/// </summary>
 24/// <remarks>
 25/// <pitch>The front door to the library: ask it for the <see cref="AmbientService{T}"/> accessor for any service interf
 26/// <pledge>
 27/// For a given interface there is exactly one <see cref="AmbientService{T}"/> per loaded copy of this assembly, and <se
 28/// <see cref="InitializationError"/> is raised (on an arbitrary thread) when constructing a discovered default implemen
 29/// </pledge>
 30/// </remarks>
 31public static class Ambient
 32{
 33    /// <summary>
 34    /// Gets the <see cref="AmbientService{T}"/> for the indicated type.
 35    /// </summary>
 36    /// <typeparam name="T">The type of service that is needed.</typeparam>
 37    /// <returns>The <see cref="AmbientService{T}"/> instance.  This should never be null.</returns>
 38    public static AmbientService<T> GetService<
 39#if NETCOREAPP3_0_OR_GREATER
 40        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.N
 41#endif
 42        T>() where T : class
 43    {
 44        return AmbientService<T>.Instance;
 45    }
 46    /// <summary>
 47    /// Gets the <see cref="AmbientService{T}"/> for the indicated type.
 48    /// </summary>
 49    /// <typeparam name="T">The type of service that is needed.</typeparam>
 50    /// <param name="service">[OUT] Receives the ambient service.</param>
 51    /// <returns>The <see cref="AmbientService{T}"/> instance.  This should never be null.</returns>
 52    public static AmbientService<T> GetService<
 53#if NETCOREAPP3_0_OR_GREATER
 54        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.N
 55#endif
 56        T>(out AmbientService<T> service) where T : class
 57    {
 58        service = AmbientService<T>.Instance;
 59        return service;
 60    }
 61    /// <summary>
 62    /// An event that will notify subscribers when a service initialization error occurs.
 63    /// The notification may happen on any arbitrary thread.
 64    /// Thread-safe.
 65    /// </summary>
 66    public static event EventHandler<InitializationErrorEventArgs>? InitializationError;
 67
 68    internal static void NotifyInitializationError(Exception ex)
 69    {
 70        InitializationError?.Invoke(null, new InitializationErrorEventArgs(ex));
 71    }
 72}
 73/// <summary>
 74/// A generic class that provides access to an ambient service implementation.
 75/// Must be accessed through <see cref="Ambient.GetService{T}()"/> or <see cref="Ambient.GetService{T}(out AmbientServic
 76/// </summary>
 77/// <remarks>
 78/// <pitch>
 79/// The access point for one ambient service: read whichever implementation currently applies, replace it process-wide, 
 80/// It exists so libraries can consume ubiquitous-but-optional services (logging, caching, clock, settings, and the like
 81/// </pitch>
 82/// <pledge>
 83/// <see cref="Local"/> resolves to the call-context override when one exists (including the suppressed state, which mak
 84/// Call-context overrides flow with <see cref="ExecutionContext"/> into awaited continuations and forked work but never
 85/// <see cref="GlobalChanged"/> notifications may arrive on arbitrary threads and out of order; subscribers must re-quer
 86/// The default implementation (a <see cref="DefaultAmbientServiceAttribute"/>-discovered class) is constructed lazily o
 87/// </pledge>
 88/// <plan>
 89/// One singleton per closed generic type, reached through <see cref="Ambient.GetService{T}()"/>.  The global side lives
 90/// The local side is an <see cref="AsyncLocal{T}"/> slot, detailed in the paragraphs below.
 91/// </plan>
 92/// <priority>
 93/// 1. Call-context isolation over cross-context visibility: an override is confined to the logical call flow that insta
 94/// 2. Reporting a failed default construction over throwing it: a <see cref="DefaultAmbientServiceAttribute"/> class wh
 95/// 3. Cheap steady-state resolution over eager initialization: every read is an <see cref="AsyncLocal{T}"/> read plus a
 96/// </priority>
 97/// <para>Ambient state lives in static fields on <see cref="AmbientService{T}"/> in each loaded copy of this assembly. 
 98/// (or a single default assembly load context on .NET Core+), the library must be loaded only once; additional custom a
 99/// that need to share overrides with the host should resolve this assembly from the default context (or another agreed 
 100/// <para>Resolving an ambient implementation usually checks the call-context local slot first, then the global implemen
 101/// <para>The local slot is an <see cref="System.Threading.AsyncLocal{T}"/> holding <c>object?</c>: <see langword="null"
 102/// Assignments to the local slot participate in <see cref="System.Threading.ExecutionContext"/> copy-on-write, so neste
 103/// </remarks>
 104/// <typeparam name="T">The interface for the service.</typeparam>
 105public class AmbientService<
 106#if NETCOREAPP3_0_OR_GREATER
 107    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPu
 108#endif
 109    T> where T : class
 110{
 111    /// <summary>
 112    /// Gets the <see cref="AmbientService{T}"/> for the service indicated by the type.
 113    /// </summary>
 114    internal static AmbientService<T> Instance { get; } = new();
 115    /// <summary>
 116    /// The singleton call-context-local service reference (non-singleton AmbientService&lt;T&gt; can be used for unit t
 117    /// </summary>
 118    private readonly AsyncLocal<object?> _localReference = new();
 119
 120    /// <summary>
 121    /// An object whose instance is used to indicate that the global implementation has been not overridden with a local
 122    /// </summary>
 123    internal static readonly object SuppressedImplementation = new();
 124
 125    /// <summary>
 126    /// Gets the raw local reference, which may be <see cref="SuppressedImplementation"/>.
 127    /// </summary>
 128    internal object? RawLocalOverride => _localReference.Value;
 129    /// <summary>
 130    /// Sets the raw local override implementation.
 131    /// Thread-safe without caller synchronization: values are stored in <see cref="AsyncLocal{T}"/> and follow <see cre
 132    /// </summary>
 133    /// <param name="override">The new local service implementation to use, <see cref="SuppressedImplementation"/> to su
 134    internal void SetRawLocalOverride(object? @override)
 135    {
 136        _localReference.Value = @override;
 137    }
 138#if NEEDED
 139    /// <summary>
 140    /// Sets the context-local service  implementation.
 141    /// Thread-safe without caller synchronization: values are stored in <see cref="AsyncLocal{T}"/> and follow <see cre
 142    /// </summary>
 143    /// <param name="newLocalService">The new local service implementation to use.</param>
 144    internal void SetLocalOverride(T newLocalService)
 145    {
 146        _localReference.Value = newLocalService;
 147    }
 148#endif
 149    /// <summary>
 150    /// Clears the local override of the global implementation.
 151    /// Thread-safe without caller synchronization: values are stored in <see cref="AsyncLocal{T}"/> and follow <see cre
 152    /// </summary>
 153    internal void ClearLocalOverride()
 154    {
 155        _localReference.Value = null;
 156    }
 157    /// <summary>
 158    /// Sets the local override of the global implementation in such a way that the global implementation is suppressed 
 159    /// Thread-safe without caller synchronization: values are stored in <see cref="AsyncLocal{T}"/> and follow <see cre
 160    /// </summary>
 161    internal void SuppressGlobalUsingLocalOverride()
 162    {
 163        _localReference.Value = SuppressedImplementation;
 164    }
 165
 166    // this is only internal instead of private so that we can diagnose issues in test cases
 167    internal GlobalServiceReference<T> GlobalReference { get; } = new();
 168
 169    /// <summary>
 170    /// Overrides the service implementation locally and temporarily.
 171    /// </summary>
 172    /// <remarks>
 173    /// <para>This override applies to the <see cref="AmbientService{T}"/> singleton for the loaded copy of this assembl
 174    /// </remarks>
 175    /// <param name="newLocalServiceImplementation">The new local service implementation to use until the returned objec
 176    /// <returns>An <see cref="IDisposable"/> instance that, when disposed, will return the local service implementation
 177    public IDisposable ScopedLocalOverride(T? newLocalServiceImplementation)
 178    {
 179        return new ScopedLocalServiceOverride<T>(newLocalServiceImplementation);
 180    }
 181
 182    /// <summary>
 183    /// Suppresses both the global and local implementations temporarily, optionally replacing everything with a specifi
 184    /// This can be useful in cases where you're calling across assembly load contexts into a partially-trusted assembly
 185    /// but that you want to prevent from accessing specific ambient services.
 186    /// </summary>
 187    /// <param name="temporaryGlobalServiceImplementation">An optional implementation to use as the global implementatio
 188    /// <returns>An <see cref="IDisposable"/> instance that, when disposed, will return the global and local service imp
 189    public IDisposable ScopedGlobalOverride(T? temporaryGlobalServiceImplementation = null)
 190    {
 191        return new ScopedGlobalServiceOverride<T>(temporaryGlobalServiceImplementation);
 192    }
 193
 194    internal AmbientService()
 195    {
 196    }
 197    /// <summary>
 198    /// Gets or sets the global service implementation, or null if there is no implementation or it has been suppressed.
 199    /// If set to null, suppresses the global service.
 200    /// When setting the service, overwrites any previous implementation and raises the <see cref="GlobalChanged"/> even
 201    /// Thread-safe.
 202    /// </summary>
 203    public T? Global
 204    {
 205        get
 206        {
 207            return GlobalReference.Service;
 208        }
 209        set
 210        {
 211            GlobalReference.Service = value;
 212        }
 213    }
 214    /// <summary>
 215    /// Gets or sets the call-context-local override implementation for the service, or null if there is no override imp
 216    /// If set to null, reverts to the global service implementation and begins watching changes to that.
 217    /// Otherwise sets to the specified implementation.
 218    /// Thread-safe without caller synchronization: local values use <see cref="AsyncLocal{T}"/> and <see cref="Executio
 219    /// </summary>
 220    public T? Override
 221    {
 222        get
 223        {
 224            return _localReference.Value as T;
 225        }
 226        set
 227        {
 228            if (value == null) ClearLocalOverride();
 229            else _localReference.Value = value;
 230        }
 231    }
 232    /// <summary>
 233    /// Gets or sets the call-context-local service implementation.
 234    /// If set to null, suppresses any local or global service (and begins ignoring changes to the global service).
 235    /// Otherwise sets the local override to the specified implementation.
 236    /// Thread-safe without caller synchronization: local values use <see cref="AsyncLocal{T}"/> and <see cref="Executio
 237    /// </summary>
 238    public T? Local
 239    {
 240        get
 241        {
 242            return (_localReference.Value ?? GlobalReference.Service) as T;
 243        }
 244        set
 245        {
 246            if (value == null) SuppressGlobalUsingLocalOverride();
 247            else _localReference.Value = value;
 248        }
 249    }
 250    /// <summary>
 251    /// An event that will notify subscribers when a global service implementation is changed.
 252    /// The notification may happen on any arbitrary thread.
 253    /// Thread-safe.
 254    /// </summary>
 255    /// <remarks>
 256    /// In order to avoid memory leaks, most subscribers will want to subscribe a static method or use the weak event li
 257    /// because this instance lives forever.
 258    /// Because the event might be raised simultaneously on other threads or call contexts (due to multiple changes happ
 259    /// the fact that each notification may proceed at a different pace, notifications may appear to come in a different
 260    /// As a result, subscribers should query the latest value if needed when they receive the event notification.
 261    /// This way if multiple changes happen, they will always end up with the latest value.
 262    /// Subscribers must take care to avoid race conditions that may be caused by such out-of-order notifications.
 263    /// </remarks>
 264    public event EventHandler<EventArgs> GlobalChanged
 265    {
 266        add
 267        {
 268            GlobalReference.ServiceChanged += value;
 269        }
 270        remove
 271        {
 272            GlobalReference.ServiceChanged -= value;
 273        }
 274    }
 275}
 276/// <summary>
 277/// A scoping class that overrides the global service implementation with a specified local one during its scope.
 278/// Note that call context variables can sometimes survive returning from a function and calling into another function,
 279/// so it is important to reset a local override before returning from the function where the override is used.
 280/// As a result, depending on how contexts are reused, restoring the original may be needed.
 281/// For example, in unit tests, the same call context is used for multiple unit tests, so any overrides need
 282/// to be undone when the test is complete just in case another test subsequently runs using the same call context.
 283/// </summary>
 284/// <remarks>
 285/// <pitch>The <c>using</c>-block way to substitute (or remove) a service implementation for just the current call conte
 286/// <pledge>Construction captures the raw call-context slot — including a pre-existing suppression — and sets the local 
 287/// <plan>A thin wrapper over <see cref="AmbientService{T}.Local"/>: it snapshots the raw <see cref="AsyncLocal{T}"/> va
 288/// </remarks>
 289/// <typeparam name="T">The service interface type.</typeparam>
 290public sealed class ScopedLocalServiceOverride<
 291#if NETCOREAPP3_0_OR_GREATER
 292    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPu
 293#endif
 294    T> : IDisposable where T : class
 295{
 296    private static readonly AmbientService<T> _Reference = Ambient.GetService<T>();
 297
 298    private readonly object? _oldRawOverride;
 299    /// <summary>
 300    /// Gets the old local override in case it is needed by the overriding implementation.  (Mostly for debugging).
 301    /// </summary>
 302    public T? OldOverride { get; }
 303    /// <summary>
 304    /// Gets the old global implementation in case it is needed by the overriding implementation.  (Mostly for debugging
 305    /// </summary>
 306    public T? OldGlobal { get; }
 307
 308    /// <summary>
 309    /// Constructs a scoped override that changes the service implementation for this call context until this instance i
 310    /// </summary>
 311    /// <param name="temporaryLocalService">The service to temporarily use in this call context.</param>
 312    public ScopedLocalServiceOverride(T? temporaryLocalService)
 313    {
 314        _oldRawOverride = _Reference.RawLocalOverride;
 315        OldGlobal = _Reference.Global;
 316        OldOverride = _Reference.Override;
 317        _Reference.Local = temporaryLocalService;
 318    }
 319
 320    #region IDisposable Support
 321    private bool _disposed; // To detect redundant calls
 322
 323    private void Dispose(bool disposing)
 324    {
 325        if (!_disposed)
 326        {
 327            if (disposing)
 328            {
 329                _Reference.SetRawLocalOverride(_oldRawOverride);
 330            }
 331            _disposed = true;
 332        }
 333    }
 334    /// <summary>
 335    /// Disposes of the instance.
 336    /// </summary>
 337    public void Dispose()
 338    {
 339        // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
 340        Dispose(true);
 341    }
 342    #endregion
 343}
 344/// <summary>
 345/// A scoping class that overrides both the global and local service implementation with a specified global one during i
 346/// </summary>
 347/// <remarks>
 348/// <pitch>Temporarily replaces the process-wide implementation for the duration of a <c>using</c> block — for the rare 
 349/// <pledge>Construction captures both the global implementation and the raw call-context slot, then assigns the given i
 350/// <plan>A thin wrapper over <see cref="AmbientService{T}.Global"/> and the raw <see cref="AsyncLocal{T}"/> local slot;
 351/// </remarks>
 352/// <typeparam name="T">The service interface type.</typeparam>
 353public sealed class ScopedGlobalServiceOverride<
 354#if NETCOREAPP3_0_OR_GREATER
 355    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPu
 356#endif
 357    T> : IDisposable where T : class
 358{
 359    private static readonly AmbientService<T> _Reference = Ambient.GetService<T>();
 360
 361    private readonly object? _oldRawOverride;
 362    /// <summary>
 363    /// Gets the old local override in case it is needed by the overriding implementation.  (Mostly for debugging).
 364    /// </summary>
 365    public T? OldOverride { get; }
 366    /// <summary>
 367    /// Gets the old global implementation in case it is needed by the overriding implementation.  (Mostly for debugging
 368    /// </summary>
 369    public T? OldGlobal { get; }
 370    /// <summary>
 371    /// Constructs a scoped override that changes the service implementation for this call context until this instance i
 372    /// </summary>
 373    /// <param name="temporaryGlobalService">The optional service to temporarily use in this call context.</param>
 374    public ScopedGlobalServiceOverride(T? temporaryGlobalService = null)
 375    {
 376        _oldRawOverride = _Reference.RawLocalOverride;
 377        OldGlobal = _Reference.Global;
 378        OldOverride = _Reference.Override;
 379        _Reference.Global = temporaryGlobalService;
 380    }
 381
 382    #region IDisposable Support
 383    private bool _disposed; // To detect redundant calls
 384
 385    private void Dispose(bool disposing)
 386    {
 387        if (!_disposed)
 388        {
 389            if (disposing)
 390            {
 391                _Reference.Global = OldGlobal;
 392                _Reference.SetRawLocalOverride(_oldRawOverride);
 393            }
 394            _disposed = true;
 395        }
 396    }
 397    /// <summary>
 398    /// Disposes of the instance.
 399    /// </summary>
 400    public void Dispose()
 401    {
 402        // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
 403        Dispose(true);
 404    }
 405    #endregion
 406}
 407
 408/// <summary>
 409/// A generic class used to ensure that only one instance of the default service implementation gets created.
 410/// </summary>
 411/// <remarks>
 412/// <pitch>The once-per-concrete-type construction guard for discovered default implementations.</pitch>
 413/// <plan>Exploits CLR static-initializer semantics: the singleton lives in a static field of the closed generic type, s
 414/// </remarks>
 415/// <typeparam name="T">The concrete type of the service.</typeparam>
 416internal class DefaultServiceImplementation<
 417#if NETCOREAPP3_0_OR_GREATER
 418    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPu
 419#endif
 420    T> where T : class
 421{
 422    private static readonly T _ImplementationSingleton = CreateInstance();
 423    private static T CreateInstance()
 424    {
 425        ConstructorInfo ci = typeof(T).GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPubl
 426            ?? throw new InvalidOperationException($"Classes with [DefaultAmbientService] attributes applied type must h
 427        return (T)ci.Invoke([]);
 428    }
 429    public static T GetImplementation() { return _ImplementationSingleton; }
 430}
 431/// <summary>
 432/// A class that manages a global service reference.
 433/// </summary>
 434/// <remarks>
 435/// <pitch>The process-wide half of an <see cref="AmbientService{T}"/>: holds the one global implementation, discovers t
 436/// <pledge>The getter returns the registered implementation, the discovered default, or null when none exists or the se
 437/// <plan>A single <c>object?</c> field interpreted as: null (default not yet resolved — retry discovery on each read un
 438/// </remarks>
 439/// <typeparam name="T">The interface type for the service being managed.</typeparam>
 440internal class GlobalServiceReference<
 441#if NETCOREAPP3_0_OR_GREATER
 442    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPu
 443#endif
 444T> where T : class
 445{
 446    /// <summary>
 447    /// A generic object whose instance is used to indicate that the default service implementation has been suppressed.
 448    /// </summary>
 449    private static readonly object SuppressedService = new();
 450
 451    /// <summary>
 452    /// A reference to the current service implementation.  Null if not yet initialized.  <see cref="SuppressedService"/
 453    /// </summary>
 454    private object? _service;
 455
 456    internal GlobalServiceReference()
 457    {
 458        _service = DefaultImplementation();
 459    }
 460    private static T? DefaultImplementation()
 461    {
 462        try
 463        {
 464            Type? impType = DefaultAmbientServices.TryFind(typeof(T));
 465            if (impType == null) return null;       // there is no default implementation (yet)
 466            Type type = typeof(DefaultServiceImplementation<>).MakeGenericType(impType);
 467            MethodInfo mi = type.GetMethod(nameof(DefaultServiceImplementation<T>.GetImplementation))!; // DefaultServic
 468            T implementation = (T)mi.Invoke(null, [])!;  // DefaultServiceImplementation<T> returns a non-null T
 469            return implementation;
 470        }
 471        catch (Exception ex)
 472        {
 473            string traceMessage = $"Error constructing default {typeof(T).FullName}: {ex}!";
 474            System.Diagnostics.Trace.WriteLine(traceMessage);
 475            Console.WriteLine(traceMessage);
 476            Ambient.NotifyInitializationError(ex);
 477        }
 478        return null;
 479    }
 480    private T? LateAssignedDefaultServiceImplementation()
 481    {
 482        T? newDefaultImplementation = DefaultImplementation();
 483        // still no default implementation registered?  try again later
 484        if (newDefaultImplementation == null) return null;
 485        // we should almost always get a null back here below, but it's theoretically possible if two attempts to retrie
 486        // as a result, the non-null case below is unlikely to get covered by tests
 487        return (Interlocked.CompareExchange(ref _service, newDefaultImplementation, null) is not T oldDefaultImplementat
 488            ? newDefaultImplementation
 489            : oldDefaultImplementation;
 490    }
 491
 492    /// <summary>
 493    /// Gets or sets the service.
 494    /// If set to null, suppresses the default service (so that the getter returns null).
 495    /// When setting the service, overwrites any previous service and raises the <see cref="ServiceChanged"/> event.
 496    /// Thread-safe.
 497    /// </summary>
 498    public T? Service
 499    {
 500        get
 501        {
 502            return (_service ?? LateAssignedDefaultServiceImplementation()) as T;
 503        }
 504        set
 505        {
 506            T? oldImplementation = Interlocked.Exchange(ref _service, value ?? SuppressedService) as T;
 507            ServiceChanged?.Invoke(typeof(AmbientService<T>), EventArgs.Empty);
 508        }
 509    }
 510    /// <summary>
 511    /// An event that will notify subscribers when the global service implementation is changed.
 512    /// The notification will happen on the thread and within the call context from which the change is initiated.
 513    /// Thread-safe.
 514    /// </summary>
 515    /// <remarks>
 516    /// In order to avoid memory leaks, most subscribers will want to subscribe a static method or use the weak event li
 517    /// because the service reference lives forever.
 518    /// Because the event might be raised simultaneously on other threads or call contexts (due to multiple changes happ
 519    /// and each notification may proceed at a different pace, notifications may appear to come in a different order tha
 520    /// Subscribers should query the latest value if needed when they receive the event notification.
 521    /// This way if multiple changes happen, they will always end up with the latest value.
 522    /// Subscribers must take care to avoid race conditions that may be caused by such out-of-order notifications.
 523    /// </remarks>
 524    public event EventHandler<EventArgs>? ServiceChanged;
 525}

Methods/Properties

.ctor(System.Exception)