< Summary

Information
Class: AmbientServices.AmbientLogFilter
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/LoggerHelpers.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 27
Uncovered lines: 0
Coverable lines: 27
Total lines: 942
Line coverage: 100%
Branch coverage
100%
Covered branches: 30
Total branches: 30
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%88100%
get_LogLevel()100%11100%
IsTypeBlocked(...)100%66100%
IsCategoryBlocked(...)100%88100%
IsLevelBlocked(...)100%22100%
IsBlocked(...)100%66100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/LoggerHelpers.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using System.Reflection;
 6using System.Text;
 7using System.Text.Json;
 8using System.Text.Json.Serialization;
 9using System.Text.RegularExpressions;
 10using System.Threading;
 11using System.Threading.Tasks;
 12
 13namespace AmbientServices;
 14
 15/// <summary>
 16/// A delegate that can be used to render a simple log message.
 17/// </summary>
 18/// <param name="utcNow">The timestamp for the log (in UTC), which the renderer may choose to add to the log information
 19/// <param name="level">The <see cref="AmbientLogLevel"/> of the log entry.</param>
 20/// <param name="structuredData">The structured data to be logged.</param>
 21/// <param name="ownerType">The optional name of the type that owns the log source.</param>
 22/// <param name="category">The optional log entry category.</param>
 23/// <returns>A simple log message to be given to one or more ambient <see cref="IAmbientLogger"/>s.</returns>
 24public delegate string LogMessageRenderer(DateTime utcNow, AmbientLogLevel level, object structuredData, string? ownerTy
 25
 26/// <summary>
 27/// A delegate that can be used to render log data.
 28/// </summary>
 29/// <param name="utcNow">The timestamp for the log (in UTC), which the renderer may choose to add to the log information
 30/// <param name="level">The <see cref="AmbientLogLevel"/> of the log entry.</param>
 31/// <param name="structuredData">The structured data to be logged.</param>
 32/// <param name="ownerType">The optional name of the type that owns the log source.</param>
 33/// <param name="category">The optional log entry category.</param>
 34/// <returns>A structured log entry to be given to one or more ambient <see cref="IAmbientStructuredLogger"/>s.</returns
 35public delegate object LogEntryRenderer(DateTime utcNow, AmbientLogLevel level, object structuredData, string? ownerType
 36
 37/// <summary>
 38/// An interface that can be implemented by <see cref="Exception"/> classes that provides exception-specific information
 39/// </summary>
 40/// <remarks>
 41/// <pitch>Implement this on a custom exception to get its domain-specific details (identifiers, codes, retry hints) aut
 42/// <pledge>The exposed key-value pairs are merged into the structured error data whenever the exception is logged throu
 43/// </remarks>
 44public interface IExceptionLogInformation
 45{
 46    /// <summary>
 47    /// Gets an enumeration of special key-value pairs that should be added to the log entry.
 48    /// </summary>
 49    IEnumerable<(string Key, object Value)> LogInformation { get; }
 50}
 51
 52/// <summary>
 53/// A type-specific logging class.  The name of the type is prepended to each log message.
 54/// When the log target requires I/O (as it usually will), the log messages should be buffered asynchronously so that on
 55/// While this isn't the most basic logging interface, using it can be as simple as just passing in a string, or as deta
 56/// As code complexity grows over time, more and more details are usually logged, so this interface provides a way to do
 57/// Log filtering is generally done centrally, so it does not need to be abstracted or ambient and should be done by usi
 58/// Categories are used primarily for filtering and may or may not be inserted as part of the message string (depending 
 59/// To more efficiently handle filtering, this class only provides conditional access to <see cref="AmbientFilteredLogge
 60/// This avoids using generating complicated log data when the messages are set to be filtered anyway by returning a nul
 61/// For example, in C#, you simply write something like this:
 62/// <code>Logger.Filtered()?.Log(string.Join(",", MessageListToLog()), new { List = StructuredListToLog() });</code>
 63/// In this scenario, MessageListToLog() and StructuredListToLog() will not be called at all when the logging is being f
 64/// </summary>
 65/// <remarks>
 66/// <pitch>The front door for logging: a per-type facade that decides <em>whether</em> an entry should be logged before 
 67/// <pledge>
 68/// Filtering happens first and is settings-driven (by level, owner type, and category, with block patterns taking prece
 69/// A single entry is delivered to the structured logger (rendered by the entry renderer: standard fields, then <see cre
 70/// A logger constructed from just an owner type follows the ambient local logger services dynamically, so per-context o
 71/// </pledge>
 72/// <plan>
 73/// Built on <see cref="AmbientService{T}"/> accessors for <see cref="IAmbientLogger"/>/<see cref="IAmbientStructuredLog
 74/// Trade-off profile: the reflection-and-JSON rendering cost is paid only by entries that survive the filter; the filte
 75/// </plan>
 76/// <priority>
 77/// 1. Never building log data that will be discarded over a conventional logging call: the filter decision comes first 
 78/// 2. Delivering a degraded entry over dropping it: a structured-data serialization failure falls back to per-property 
 79/// 3. Following the ambient loggers dynamically over binding once: a logger built from an owner type resolves the servi
 80/// </priority>
 81/// </remarks>
 82public class AmbientLogger
 83{
 84    private static readonly IAmbientSetting<string> _MessageFormatString = AmbientSettings.GetAmbientSetting(nameof(Ambi
 85    private static readonly AmbientService<IAmbientLogger> _AmbientSimpleLogger = Ambient.GetService<IAmbientLogger>();
 86    private static readonly AmbientService<IAmbientStructuredLogger> _AmbientLogger = Ambient.GetService<IAmbientStructu
 87    internal static readonly JsonSerializerOptions DefaultSerializer = InitDefaultSerializerOptions();
 88    private static JsonSerializerOptions InitDefaultSerializerOptions()
 89    {
 90        JsonSerializerOptions options = new() { WriteIndented = true, NumberHandling = JsonNumberHandling.AllowNamedFloa
 91        options.Converters.Add(new IPAddressConverter());
 92        options.Converters.Add(new IPAddressConverterFactory());
 93#if NETCOREAPP1_0_OR_GREATER
 94        options.Converters.Add(new IPEndPointConverter());
 95        options.Converters.Add(new IPEndPointConverterFactory());
 96#endif
 97        return options;
 98    }
 99
 100    private readonly string _typeName;
 101    private readonly bool _useAmbientLocalLogger;
 102    private readonly IAmbientLogger? _simpleLogger;
 103    private readonly IAmbientStructuredLogger? _logger;
 104    private readonly AmbientLogFilter _logFilter;
 105
 106    /// <summary>
 107    /// Gets the <see cref="IAmbientStructuredLogger"/> used for logging.
 108    /// </summary>
 109    public IAmbientLogger? DynamicSimpleLogger => _useAmbientLocalLogger ? _AmbientSimpleLogger.Local : _simpleLogger;
 110    /// <summary>
 111    /// Gets the <see cref="IAmbientStructuredLogger"/> used for logging.
 112    /// </summary>
 113    public IAmbientStructuredLogger? DynamicLogger => _useAmbientLocalLogger ? _AmbientLogger.Local : _logger;
 114    /// <summary>
 115    /// Gets a string containing the logger types (simple/structured).
 116    /// </summary>
 117    public string LoggerType => $"{DynamicSimpleLogger?.GetType().Name}/{DynamicLogger?.GetType().Name}";
 118
 119    private LogMessageRenderer? _simpleRenderer;
 120    private LogEntryRenderer? _renderer;
 121
 122    /// <summary>
 123    /// Constructs an AmbientLogger using the ambient logger and ambient settings set.
 124    /// </summary>
 125    /// <param name="type">The type doing the logging.</param>
 126    public AmbientLogger(Type type)
 127        : this(type, null, null)
 128    {
 129        _useAmbientLocalLogger = true;
 130    }
 131    /// <summary>
 132    /// Constructs an AmbientLogger with the specified logger and settings set.
 133    /// </summary>
 134    /// <param name="type">The type doing the logging.</param>
 135    /// <param name="logger">The <see cref="IAmbientLogger"/> to use for the logging.</param>
 136    /// <param name="structuredLogger">The <see cref="IAmbientStructuredLogger"/> to use for the logging.</param>
 137    /// <param name="loggerSettingsSet">A <see cref="IAmbientSettingsSet"/> from which the settings should be queried.</
 138    public AmbientLogger(Type type, IAmbientLogger? logger, IAmbientStructuredLogger? structuredLogger, IAmbientSettings
 139    {
 140#if NET5_0_OR_GREATER
 141        ArgumentNullException.ThrowIfNull(type);
 142#else
 143        if (type is null) throw new ArgumentNullException(nameof(type));
 144#endif
 145        _typeName = type!.Name;
 146        _simpleLogger = logger;
 147        _logger = structuredLogger;
 148        _logFilter = (loggerSettingsSet == null) ? AmbientLogFilter.Default : new AmbientLogFilter(_typeName, loggerSett
 149    }
 150    /// <summary>
 151    /// Gets or sets the <see cref="LogMessageRenderer"/> which renders log message strings for <see cref="IAmbientLogge
 152    /// If null, the default renderer is used.
 153    /// </summary>
 154    public LogMessageRenderer? MessageRenderer
 155    {
 156        get
 157        {
 158            return _simpleRenderer;
 159        }
 160        set
 161        {
 162            Interlocked.Exchange(ref _simpleRenderer, value);
 163        }
 164    }
 165    /// <summary>
 166    /// Gets or sets the <see cref="LogEntryRenderer"/> which is the last step to alter message and/or structure data be
 167    /// If null, the default renderer is used.
 168    /// </summary>
 169    public LogEntryRenderer? Renderer
 170    {
 171        get
 172        {
 173            return _renderer;
 174        }
 175        set
 176        {
 177            Interlocked.Exchange(ref _renderer, value);
 178        }
 179    }
 180    /// <summary>
 181    /// Checks to see if the specified level (with no category) should be filtered.  If not, returns a logger that can b
 182    /// </summary>
 183    /// <param name="level">The <see cref="AmbientLogLevel"/> to check.  Defaults to <see cref="AmbientLogLevel.Informat
 184    /// <returns>An optional <see cref="AmbientFilteredLogger"/>, which can be conditionally called into for logging.  N
 185    public AmbientFilteredLogger? Filter(AmbientLogLevel level = AmbientLogLevel.Information)
 186    {
 187        return Filter(null, level);
 188    }
 189    /// <summary>
 190    /// Checks to see if the specified category and level should be filtered.  If not, returns a logger that can be used
 191    /// </summary>
 192    /// <param name="categoryName">The optional category name.</param>
 193    /// <param name="level">The <see cref="AmbientLogLevel"/> to check.  Defaults to <see cref="AmbientLogLevel.Informat
 194    /// <returns>An optional <see cref="AmbientFilteredLogger"/>, which can be conditionally called into for logging.  N
 195    public AmbientFilteredLogger? Filter(string? categoryName, AmbientLogLevel level = AmbientLogLevel.Information)
 196    {
 197        IAmbientLogger? simpleLogger = DynamicSimpleLogger;
 198        IAmbientStructuredLogger? logger = DynamicLogger;
 199        if ((simpleLogger == null && logger == null) || _logFilter.IsBlocked(level, _typeName, null)) return null;
 200        return new AmbientFilteredLogger(this, level, categoryName);
 201    }
 202
 203    /// <summary>
 204    /// Augments <paramref name="anonymous"/> with standard structured data for an error.  This is useful for logging st
 205    /// </summary>
 206    /// <param name="ex">The exception to log.</param>
 207    /// <param name="anonymous">The anonymous object to convert to a dictionary and add the error information to.</param
 208    /// <returns>The dictionary with the error information added.</returns>
 209    public static Dictionary<string, object?> AugmentStructuredDataWithExceptionInformation(Exception ex, object anonymo
 210    {
 211#if NET5_0_OR_GREATER
 212        ArgumentNullException.ThrowIfNull(anonymous);
 213#else
 214        if (anonymous is null) throw new ArgumentNullException(nameof(anonymous));
 215#endif
 216        Dictionary<string, object?> dictionary = StructuredDataToDictionary(anonymous);
 217        AddExceptionInformationToDictionary(dictionary, ex);
 218        return dictionary;
 219    }
 220    /// <summary>
 221    /// Augments <paramref name="dictionary"/> with standard structured data for an error.  This is useful for logging s
 222    /// </summary>
 223    /// <param name="dictionary">The dictionary to add the properties and values to.</param>
 224    /// <param name="ex">The exception to log.</param>
 225    /// <returns>The dictionary with the error information added.</returns>
 226    public static void AddExceptionInformationToDictionary(Dictionary<string, object?> dictionary, Exception ex)
 227    {
 228#if NET5_0_OR_GREATER
 229        ArgumentNullException.ThrowIfNull(ex);
 230        ArgumentNullException.ThrowIfNull(dictionary);
 231#else
 232        if (ex is null) throw new ArgumentNullException(nameof(ex));
 233        if (dictionary is null) throw new ArgumentNullException(nameof(dictionary));
 234#endif
 235        CopyStructuredDataToDictionary(dictionary, new ErrorLogInfo(ex));
 236        if (ex is IExceptionLogInformation exli)
 237        {
 238            // loop through key-value pairs explicitly exposed by the exception for the purpose of logging
 239            foreach ((string key, object value) in exli.LogInformation)
 240            {
 241                dictionary[key] = value;
 242            }
 243        }
 244    }
 245    /// <summary>
 246    /// Logs an exception with standard structured data.
 247    /// </summary>
 248    /// <param name="ex">The <see cref="Exception"/> that caused the error and whose information should be added to the 
 249    /// <param name="contextDescription">A message to identify the context of where the exception occurred.</param>
 250    /// <param name="level">The <see cref="AmbientLogLevel"/> identifying the severity of the information.</param>
 251    public void Error(Exception ex, string? contextDescription = null, AmbientLogLevel level = AmbientLogLevel.Error)
 252    {
 253#if NET5_0_OR_GREATER
 254        ArgumentNullException.ThrowIfNull(ex);
 255#else
 256        if (ex is null) throw new ArgumentNullException(nameof(ex));
 257#endif
 258        Dictionary<string, object?> dictionary = AugmentStructuredDataWithExceptionInformation(ex, new { });
 259        if (contextDescription != null) CopyStructuredDataToDictionary(dictionary, new LogSummaryInfo(contextDescription
 260        Filter(level)?.Log(dictionary);
 261    }
 262    internal void LogFiltered(AmbientLogLevel level, string? categoryName, object structuredData)
 263    {
 264        // prefer the structured logger
 265        IAmbientStructuredLogger? logger = DynamicLogger;
 266        IAmbientLogger? simpleLogger = DynamicSimpleLogger;
 267        LogEntryRenderer entryRenderer = _renderer ?? DefaultRenderer;
 268        LogMessageRenderer messageRenderer = _simpleRenderer ?? DefaultMessageRenderer;
 269        LogFiltered(logger, entryRenderer, messageRenderer, simpleLogger, _typeName, level, categoryName, structuredData
 270    }
 271    internal static void LogFiltered(IAmbientStructuredLogger? logger, LogEntryRenderer entryRenderer, LogMessageRendere
 272    {
 273        // prefer the structured logger
 274        if (logger != null)
 275        {
 276            // by the time we get here, we have already determined that no filtering should be done, so we can just log 
 277            structuredData = entryRenderer(AmbientClock.UtcNow, level, structuredData, typeName, categoryName);
 278            logger.Log(structuredData);
 279        }
 280        // only log to the simple logger if it's not the same instance as the structured logger
 281        if (simpleLogger != null && (simpleLogger is not IAmbientStructuredLogger sl || sl != logger))
 282        {
 283            string message = ConvertStructuredDataIntoSimpleMessage(messageRenderer, typeName, level, categoryName, stru
 284            simpleLogger.Log(message);
 285        }
 286    }
 287    /// <summary>
 288    /// Converts a structured data log entry (possibly an anonymous object) into a dictionary.
 289    /// </summary>
 290    /// <param name="structuredData">The structured object.</param>
 291    /// <returns>The dictionary containing the properties and values in the structured data object.</returns>
 292    public static Dictionary<string, object?> StructuredDataToDictionary(object structuredData)
 293    {
 294        return CopyStructuredDataToDictionary(new(), structuredData);
 295    }
 296    /// <summary>
 297    /// Copies the data in a structured data log entry (possibly an anonymous object) into a dictionary.
 298    /// </summary>
 299    /// <param name="dictionary">The dictionary to add the properties and values to.</param>
 300    /// <param name="structuredData">The structured object.</param>
 301    /// <param name="logNullValues">Whether to log null values or not.</param>
 302    /// <returns>The dictionary containing the properties and values in the structured data object.</returns>
 303    public static Dictionary<string, object?> CopyStructuredDataToDictionary(Dictionary<string, object?> dictionary, obj
 304    {
 305#if NET5_0_OR_GREATER
 306        ArgumentNullException.ThrowIfNull(dictionary);
 307        ArgumentNullException.ThrowIfNull(structuredData);
 308#else
 309        if (dictionary is null) throw new ArgumentNullException(nameof(dictionary));
 310        if (structuredData is null) throw new ArgumentNullException(nameof(structuredData));
 311#endif
 312        if (structuredData is string sds)
 313        {
 314            CopyStructuredDataToDictionary(dictionary, new LogSummaryInfo(sds));
 315        }
 316        else if (structuredData is Dictionary<string, object?> sddo)
 317        {
 318            foreach (KeyValuePair<string, object?> kvp in sddo)
 319            {
 320                dictionary[kvp.Key] = AmbientLogSensitiveFieldFilters.MaskValueIfSensitive(kvp.Key, kvp.Value);
 321            }
 322        }
 323        else if (structuredData is Dictionary<string, string?> sdds)
 324        {
 325            foreach (KeyValuePair<string, string?> kvp in sdds)
 326            {
 327                dictionary[kvp.Key] = AmbientLogSensitiveFieldFilters.MaskValueIfSensitive(kvp.Key, kvp.Value);
 328            }
 329        }
 330        else
 331        {
 332            foreach (PropertyInfo property in structuredData.GetType().GetProperties(BindingFlags.Instance | BindingFlag
 333            {
 334                object? propertyValue = property.GetValue(structuredData);
 335                if (logNullValues || propertyValue != null)
 336                {
 337                    dictionary[property.Name] = AmbientLogSensitiveFieldFilters.MaskValueIfSensitive(property.Name, prop
 338                }
 339            }
 340        }
 341        return dictionary;
 342    }
 343    /// <summary>
 344    /// Converts the specified structured data into a simple log message.
 345    /// </summary>
 346    /// <param name="level">The <see cref="AmbientLogLevel"/> in case the renderer needs it.</param>
 347    /// <param name="categoryName">The optional name of the category.</param>
 348    /// <param name="structuredData">The structured data object.</param>
 349    /// <returns>The simple log message</returns>
 350    public string ConvertStructuredDataIntoSimpleMessage(AmbientLogLevel level, string? categoryName, object structuredD
 351    {
 352        // by the time we get here, we have already determined that no filtering should be done, so we can just log the 
 353        LogMessageRenderer renderer = _simpleRenderer ?? DefaultMessageRenderer;
 354        return ConvertStructuredDataIntoSimpleMessage(renderer, _typeName, level, categoryName, structuredData);
 355    }
 356    private static string ConvertStructuredDataIntoSimpleMessage(LogMessageRenderer renderer, string typeName, AmbientLo
 357    {
 358        // by the time we get here, we have already determined that no filtering should be done, so we can just log the 
 359        string message = renderer(AmbientClock.UtcNow, level, structuredData, typeName, categoryName);
 360        return message;
 361    }
 362    /// <summary>
 363    /// Renders the structured data into a simple log entry.
 364    /// </summary>
 365    /// <param name="structuredData">The structured data (often an anonymous object).</param>
 366    /// <param name="summaryStructuredDelimiter">The delimiter to use between the summary data and the structured data.<
 367    /// <returns>The simple log entry string.</returns>
 368    public static string ConvertStructuredDataIntoSimpleMessage(object structuredData, string summaryStructuredDelimiter
 369    {
 370#if NET5_0_OR_GREATER
 371        ArgumentNullException.ThrowIfNull(structuredData);
 372#else
 373        if (structuredData is null) throw new ArgumentNullException(nameof(structuredData));
 374#endif
 375        (string summary, string structured) = RenderStructuredData(structuredData);
 376        string structuredEntry = (string.IsNullOrEmpty(summary) ? "" : summaryStructuredDelimiter) + structured;
 377        return summary + structuredEntry;
 378    }
 379    /// <summary>
 380    /// Renders a simple log message from the specified structured data.
 381    /// </summary>
 382    /// <param name="utcNow">The timestamp for the log (in UTC), which the renderer may choose to add to the log informa
 383    /// <param name="level">The <see cref="AmbientLogLevel"/> of the log entry.</param>
 384    /// <param name="structuredData">The structured data to be logged.</param>
 385    /// <param name="ownerType">The optional name of the type that owns the log source.</param>
 386    /// <param name="category">The optional log entry category.</param>
 387    /// <returns>The rendered message.</returns>
 388    public static string DefaultMessageRenderer(DateTime utcNow, AmbientLogLevel level, object structuredData, string? o
 389    {
 390#if NET5_0_OR_GREATER
 391        ArgumentNullException.ThrowIfNull(structuredData);
 392#else
 393        if (structuredData is null) throw new ArgumentNullException(nameof(structuredData));
 394#endif
 395        (string summary, string structured) = RenderStructuredData(structuredData);
 396        string ownerTypePart = string.IsNullOrEmpty(ownerType) ? "" : $":{ownerType}";
 397        string entryPart = string.IsNullOrEmpty(structured) ? "" : $"{Environment.NewLine}{structured}";
 398        string renderedMessage = $"{utcNow:yyMMdd HHmmss.fff} [{level}{ownerTypePart}] {summary}{entryPart}";
 399        return renderedMessage;
 400    }
 401    private static (string Summary, string StructuredJson) RenderStructuredData(object structuredData)
 402    {
 403        string summary;
 404        string structured;
 405        if (structuredData is string sds)
 406        {
 407            summary = sds;
 408            structured = "";
 409        }
 410        else
 411        {
 412            if (structuredData is Dictionary<string, object?> sd)
 413            {
 414                // look for a "Summary" entry to use as a summary.  We remove this below so it's not redundant
 415                if (sd.TryGetValue(nameof(LogSummaryInfo.Summary), out object? summaryValue))
 416                {
 417                    // make a copy so we don't alter the original
 418                    sd = new(sd);
 419                    summary = summaryValue?.ToString() ?? "";
 420                }
 421                else summary = "";
 422            }
 423            else
 424            {
 425                // look for a "Summary" property to use as a summary.  We remove this below so it's not redundant
 426                PropertyInfo? summaryProperty = structuredData.GetType().GetProperty(nameof(LogSummaryInfo.Summary), Bin
 427                summary = (summaryProperty == null) ? "" : summaryProperty.GetValue(structuredData)?.ToString() ?? "";
 428                sd = StructuredDataToDictionary(structuredData);
 429            }
 430            _ = sd.Remove(nameof(LogSummaryInfo.Summary));
 431            structured = JsonSerialize(sd);
 432        }
 433        return (summary, structured);
 434    }
 435    private static string JsonSerialize(object structuredData, Type? containingType = null, int depth = 0)
 436    {
 437        try
 438        {
 439            return JsonSerializer.Serialize(structuredData, DefaultSerializer);
 440        }
 441        catch (Exception ex)
 442        {
 443            if (depth > 32 || structuredData.GetType() == containingType) return $"{containingType?.Name} Recursion Erro
 444            return (structuredData is CultureInfo) ? HandleUnserializable(new { CultureInfo = structuredData.ToString() 
 445        }
 446    }
 447
 448    internal static string HandleUnserializable(object structuredData, Exception ex, int depth = 0)
 449    {
 450        Dictionary<string, object?> dict = StructuredDataToDictionary(structuredData);
 451        StringBuilder sb = new();
 452        _ = sb.Append($"{{\"{nameof(ex)}\":");
 453        string jsonEncodedMessage = JsonSerializer.Serialize(ex.Message, DefaultSerializer);
 454        _ = sb.Append(jsonEncodedMessage);
 455        try
 456        {
 457            foreach (KeyValuePair<string, object?> kvp in dict)
 458            {
 459                string jsonEncodedKeyName = JsonSerializer.Serialize(kvp.Key, DefaultSerializer);
 460                string jsonEncodedValue;
 461                try
 462                {
 463                    jsonEncodedValue = (kvp.Value != null) ? JsonSerialize(kvp.Value, structuredData.GetType(), depth + 
 464                }
 465                catch (Exception valueEx)
 466                {
 467                    valueEx = UnwrapTargetInvocationChain(valueEx);
 468                    jsonEncodedValue = JsonSerializer.Serialize(kvp.Value?.ToString() + "--" + valueEx.Message, DefaultS
 469                }
 470                _ = sb.Append(',');
 471                _ = sb.Append(jsonEncodedKeyName);
 472                _ = sb.Append(':');
 473                _ = sb.Append(jsonEncodedValue);
 474            }
 475        }
 476        catch (Exception fallbackEx)
 477        {
 478            HandleFallbackException(sb, fallbackEx);
 479        }
 480        _ = sb.Append('}');
 481        return sb.ToString();
 482    }
 483
 484    private static Exception UnwrapTargetInvocationChain(Exception ex)
 485    {
 486        while (ex is TargetInvocationException tie && tie.InnerException != null)
 487        {
 488            ex = tie.InnerException;
 489        }
 490        return ex;
 491    }
 492
 493    internal static void HandleFallbackException(StringBuilder sb, Exception fallbackEx)
 494    {
 495        fallbackEx = UnwrapTargetInvocationChain(fallbackEx);
 496        sb.Append(',');
 497        sb.Append(nameof(fallbackEx));
 498        sb.Append(':');
 499        string jsonEncodedMessage = JsonSerializer.Serialize(fallbackEx.Message, DefaultSerializer);
 500        sb.Append(jsonEncodedMessage);
 501    }
 502    /// <summary>
 503    /// The default log object renderer, which renders the log object as a dictionary with the standard properties added
 504    /// </summary>
 505    /// <param name="utcNow">The <see cref="DateTime"/> to use as the timestamp for the log entry.</param>
 506    /// <param name="level">The <see cref="AmbientLogLevel"/> indicating they type of log entry.</param>
 507    /// <param name="structuredData">An object with properties to be logged (usually an anonymous object).</param>
 508    /// <param name="ownerType">An optional log entry owner name.</param>
 509    /// <param name="category">An optional log category name.</param>
 510    /// <returns></returns>
 511    public static object DefaultRenderer(DateTime utcNow, AmbientLogLevel level, object structuredData, string? ownerTyp
 512    {
 513        // add in the standard log entry properties (just level for now--we assume that ownerType and category are just 
 514        Dictionary<string, object?> dict = StructuredDataToDictionary(new StandardRequestLogInfo(level));
 515        // look for additional context-specific data to add to the log entry (request-tracking information, for example)
 516        foreach ((string key, object? value) in AmbientLogContext.ContextLogPairs.Reverse())
 517        {
 518            dict[key] = value;
 519        }
 520        // add in any data from the structuredData object
 521        CopyStructuredDataToDictionary(dict, structuredData);
 522        return dict;
 523    }
 524
 525
 526    // deprecated functions
 527
 528    internal void LogDeprecated(string message, string? category = null, AmbientLogLevel level = AmbientLogLevel.Informa
 529    {
 530        if (!_logFilter.IsBlocked(level, _typeName, category))
 531        {
 532            if (!string.IsNullOrEmpty(category)) category += ":";
 533            message = string.Format(System.Globalization.CultureInfo.InvariantCulture, _MessageFormatString.Value, Ambie
 534            DynamicSimpleLogger!.Log(message);  // the calling of this method is short-circuited when DynamicLogger is n
 535        }
 536    }
 537    /// <summary>
 538    /// Logs the specified message.
 539    /// </summary>
 540    /// <param name="message">The message to log.</param>
 541    /// <param name="category">The (optional) category to attach to the message.</param>
 542    /// <param name="level">The <see cref="AmbientLogLevel"/> for the message.</param>
 543    [Obsolete("Use more natural and efficient Filter(...).Log(...) or a custom extension method now")]
 544    public void Log(string message, string? category = null, AmbientLogLevel level = AmbientLogLevel.Information)
 545    {
 546        if (DynamicSimpleLogger == null) return;
 547        LogDeprecated(message, category, level);
 548    }
 549    /// <summary>
 550    /// Logs the message returned by the delegate.
 551    /// </summary>
 552    /// <param name="messageLambda">A delegate that creates a message.</param>
 553    /// <param name="category">The (optional) category to attach to the message.</param>
 554    /// <param name="level">The <see cref="AmbientLogLevel"/> for the message.</param>
 555    [Obsolete("Use more natural and efficient Filter(...).Log(...) or a custom extension method now")]
 556    public void Log(Func<string> messageLambda, string? category = null, AmbientLogLevel level = AmbientLogLevel.Informa
 557    {
 558        if (DynamicSimpleLogger == null) return;
 559#if NET5_0_OR_GREATER
 560            ArgumentNullException.ThrowIfNull(messageLambda);
 561#else
 562        if (messageLambda is null) throw new ArgumentNullException(nameof(messageLambda));
 563#endif
 564        LogDeprecated(messageLambda(), category, level);
 565    }
 566    /// <summary>
 567    /// Logs the specified exception.
 568    /// </summary>
 569    /// <param name="ex">An <see cref="Exception"/> to log.</param>
 570    /// <param name="category">The (optional) category to attach to the message.</param>
 571    /// <param name="level">The <see cref="AmbientLogLevel"/> for the message.</param>
 572    [Obsolete("Use more natural and efficient Filter(...).Log(...) or a custom extension method now")]
 573    public void Log(Exception ex, string? category = null, AmbientLogLevel level = AmbientLogLevel.Error)
 574    {
 575        if (DynamicSimpleLogger == null) return;
 576#if NET5_0_OR_GREATER
 577            ArgumentNullException.ThrowIfNull(ex);
 578#else
 579        if (ex is null) throw new ArgumentNullException(nameof(ex));
 580#endif
 581        LogDeprecated(ex.ToString(), category, level);
 582    }
 583    /// <summary>
 584    /// Logs the specified message and exception.
 585    /// </summary>
 586    /// <param name="message">The message to log.</param>
 587    /// <param name="ex">An <see cref="Exception"/> to log.  The exception will be appended after the message.</param>
 588    /// <param name="category">The (optional) category to attach to the message.</param>
 589    /// <param name="level">The <see cref="AmbientLogLevel"/> for the message.</param>
 590    [Obsolete("Use more natural and efficient Filter(...).Log(...) or a custom extension method now")]
 591    public void Log(string message, Exception ex, string? category = null, AmbientLogLevel level = AmbientLogLevel.Error
 592    {
 593        if (DynamicSimpleLogger == null) return;
 594#if NET5_0_OR_GREATER
 595            ArgumentNullException.ThrowIfNull(ex);
 596#else
 597        if (ex is null) throw new ArgumentNullException(nameof(ex));
 598#endif
 599        LogDeprecated(message + Environment.NewLine + ex.ToString(), category, level);
 600    }
 601    /// <summary>
 602    /// Logs the specified message (returned by a delegate) and exception.
 603    /// </summary>
 604    /// <param name="messageLambda">A delegate that creates a message.</param>
 605    /// <param name="ex">An <see cref="Exception"/> to log.  The exception will be appended after the message.</param>
 606    /// <param name="category">The (optional) category to attach to the message.</param>
 607    /// <param name="level">The <see cref="AmbientLogLevel"/> for the message.</param>
 608    [Obsolete("Use more natural and efficient Filter(...).Log(...) or a custom extension method now")]
 609    public void Log(Func<string> messageLambda, Exception ex, string? category = null, AmbientLogLevel level = AmbientLo
 610    {
 611#if NET5_0_OR_GREATER
 612            ArgumentNullException.ThrowIfNull(ex);
 613#else
 614        if (ex is null) throw new ArgumentNullException(nameof(ex));
 615#endif
 616        if (DynamicSimpleLogger == null) return;
 617#if NET5_0_OR_GREATER
 618            ArgumentNullException.ThrowIfNull(messageLambda);
 619#else
 620        if (messageLambda is null) throw new ArgumentNullException(nameof(messageLambda));
 621#endif
 622        LogDeprecated(messageLambda() + Environment.NewLine + ex.ToString(), category, level);
 623    }
 624    /// <summary>
 625    /// Flushes all the logs.
 626    /// </summary>
 627    /// <param name="onlyUsedLoggers">Whether to flush only the loggers we were using rather than all the accessible log
 628    public async ValueTask Flush(bool onlyUsedLoggers = true)
 629    {
 630        if (_useAmbientLocalLogger || !onlyUsedLoggers)
 631        {
 632            if (_AmbientSimpleLogger.Local != null) await _AmbientSimpleLogger.Local.Flush();
 633            if (_AmbientLogger.Local != null) await _AmbientLogger.Local.Flush();
 634        }
 635        if (!_useAmbientLocalLogger || !onlyUsedLoggers)
 636        {
 637            if (_logger != null) await _logger.Flush();
 638            if (_simpleLogger != null) await _simpleLogger.Flush();
 639        }
 640    }
 641}
 642/// <summary>
 643/// A class that allows construction of a log entry after already determining that the log entry should not be filtered.
 644/// Instances of this class are returned by <see cref="AmbientLogger"/> only when filtering for the specified type, cate
 645/// </summary>
 646/// <remarks>
 647/// <pitch>The post-filter handle: holding one means the filtering decision is already made in your favor, so anything y
 648/// <pledge>Instances are only obtainable from a filter check that passed; logging through one performs no further filte
 649/// <plan>A three-field wrapper (owning <see cref="AmbientLogger"/>, level, pre-rendered category prefix) whose log call
 650/// </remarks>
 651public class AmbientFilteredLogger
 652{
 653    private readonly AmbientLogger _logger;
 654    private readonly AmbientLogLevel _level;
 655    private readonly string? _categoryName;
 656
 657    /// <summary>
 658    /// Constructs an AmbientFilteredLogger for a category that belongs to the specified logger.
 659    /// </summary>
 660    /// <param name="logger">The <see cref="AmbientLogger"/> that generated this filtered logger.</param>
 661    /// <param name="level">The <see cref="AmbientLogLevel"/> for the logging.</param>
 662    /// <param name="categoryName">The optional name of the category.</param>
 663    internal AmbientFilteredLogger(AmbientLogger logger, AmbientLogLevel level, string? categoryName)
 664    {
 665        _logger = logger;
 666        _level = level;
 667        _categoryName = string.IsNullOrEmpty(categoryName) ? "" : $"{categoryName}:";
 668    }
 669
 670    /// <summary>
 671    /// Logs the specified structured log data.
 672    /// </summary>
 673    /// <param name="structuredData">Structured data to log, usually either an anonymous type or a dictionary of name-va
 674    public void Log(object structuredData)
 675    {
 676        _logger.LogFiltered(_level, _categoryName, structuredData);
 677    }
 678    /// <summary>
 679    /// Logs the specified structured log data, adding the standard data from the specified exception to the structured 
 680    /// </summary>
 681    /// <param name="structuredData">Structured data to log, usually either an anonymous type or a dictionary of name-va
 682    /// <param name="ex">An <see cref="Exception"/> whose data should be added to the log entry.</param>
 683    public void Log(object structuredData, Exception ex)
 684    {
 685        structuredData = AmbientLogger.AugmentStructuredDataWithExceptionInformation(ex, structuredData);
 686        _logger.LogFiltered(_level, _categoryName, structuredData);
 687    }
 688}
 689/// <summary>
 690/// A generic type-specific logging class.  The name of the type is prepended to each log message.
 691/// When the log target requires I/O (as it usually will), the log messages should be buffered asynchronously so that on
 692/// Note that some functions take a delegate-generating string rather than a string.  This is to be used when computatio
 693/// While this isn't the most basic logging interface, using it can be as simple as just passing in a string.
 694/// As code complexity grows over time, more and more details are usually logged, so this interface provides a way to do
 695/// Log filtering is generally done centrally, so it does not need to be abstracted or ambient and should be done by usi
 696/// </summary>
 697/// <typeparam name="TOWNER">The type that owns the log messages.</typeparam>
 698/// <remarks>
 699/// <pitch>The usual way to declare a logger: a generic convenience over <see cref="AmbientLogger"/> that takes the owne
 700/// <pledge>Identical to <see cref="AmbientLogger"/> constructed with <c>typeof(TOWNER)</c>; it adds no behavior of its 
 701/// <plan>A constructor-only subclass forwarding <c>typeof(TOWNER)</c> to the base.</plan>
 702/// </remarks>
 703public class AmbientLogger<TOWNER> : AmbientLogger
 704{
 705    /// <summary>
 706    /// Constructs an AmbientLogger using the ambient logger and ambient settings set.
 707    /// </summary>
 708    public AmbientLogger()
 709        : base(typeof(TOWNER))
 710    {
 711    }
 712    /// <summary>
 713    /// Constructs an AmbientLogger with the specified logger and settings set.
 714    /// </summary>
 715    /// <param name="logger">The <see cref="IAmbientLogger"/> to use for the logging.</param>
 716    /// <param name="structuredLogger">An optional <see cref="IAmbientStructuredLogger"/> to use for the logging.</param
 717    /// <param name="loggerSettingsSet">A <see cref="IAmbientSettingsSet"/> from which the settings should be queried.</
 718    public AmbientLogger(IAmbientLogger? logger, IAmbientStructuredLogger? structuredLogger = null, IAmbientSettingsSet?
 719        : base (typeof(TOWNER), logger, structuredLogger, loggerSettingsSet)
 720    {
 721    }
 722}
 723/// <summary>
 724/// A settings-driven filter that decides which log entries should be dropped.
 725/// </summary>
 726/// <remarks>
 727/// <pitch>The central knob for controlling log volume at runtime: level, owner-type, and category filtering configured 
 728/// <pledge>An entry is blocked when its level is more verbose than the configured maximum, when its owner type or categ
 729/// <plan>Five <see cref="IAmbientSetting{T}"/>-backed values per filter name (<c>{name}-AmbientLogFilter-LogLevel/TypeA
 730/// </remarks>
 731internal class AmbientLogFilter
 732{
 733    /// <summary>
 734    /// Gets the default log filter.
 735    /// </summary>
 2736    public static AmbientLogFilter Default { get; } = new("Default");
 737
 738    private readonly IAmbientSetting<AmbientLogLevel> _logLevelSetting;
 739    private readonly IAmbientSetting<Regex?> _typeAllowSetting;
 740    private readonly IAmbientSetting<Regex?> _typeBlockSetting;
 741    private readonly IAmbientSetting<Regex?> _categoryAllowSetting;
 742    private readonly IAmbientSetting<Regex?> _categoryBlockSetting;
 743
 744    public AmbientLogFilter(string name)
 2745        : this (name, null)
 746    {
 2747    }
 2748    internal AmbientLogFilter(string name, IAmbientSettingsSet? settingsSet)
 749    {
 2750        Name = name;
 751#if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER
 2752        _logLevelSetting = AmbientSettings.GetSetting(settingsSet, name + "-" + nameof(AmbientLogFilter) + "-LogLevel", 
 753#else
 754        _logLevelSetting = AmbientSettings.GetSetting(settingsSet, name + "-" + nameof(AmbientLogFilter) + "-LogLevel", 
 755#endif
 2756        _typeAllowSetting = AmbientSettings.GetSetting(settingsSet, name + "-" + nameof(AmbientLogFilter) + "-TypeAllow"
 2757        _typeBlockSetting = AmbientSettings.GetSetting(settingsSet, name + "-" + nameof(AmbientLogFilter) + "-TypeBlock"
 2758        _categoryAllowSetting = AmbientSettings.GetSetting(settingsSet, name + "-" + nameof(AmbientLogFilter) + "-Catego
 2759        _categoryBlockSetting = AmbientSettings.GetSetting(settingsSet, name + "-" + nameof(AmbientLogFilter) + "-Catego
 2760    }
 761    internal string Name { get; }
 2762    internal AmbientLogLevel LogLevel => _logLevelSetting.Value;
 763
 764    internal bool IsTypeBlocked(string typeName)
 765    {
 766        System.Diagnostics.Debug.Assert(typeName != null);
 2767        bool blocked = _typeBlockSetting.Value?.IsMatch(typeName) ?? false;
 2768        if (blocked) return true;
 2769        bool allowed = _typeAllowSetting.Value?.IsMatch(typeName) ?? true;
 2770        return !allowed;
 771    }
 772    internal bool IsCategoryBlocked(string? categoryName)
 773    {
 2774        categoryName ??= "";
 2775        bool blocked = _categoryBlockSetting.Value?.IsMatch(categoryName) ?? false;
 2776        if (blocked) return true;
 2777        bool allowed = _categoryAllowSetting.Value?.IsMatch(categoryName) ?? true;
 2778        return !allowed;
 779    }
 780    internal bool IsLevelBlocked(AmbientLogLevel level)
 781    {
 2782        if (level > _logLevelSetting.Value) return true;
 2783        return false;
 784    }
 785    internal bool IsBlocked(AmbientLogLevel level, string typeName, string? categoryName)
 786    {
 2787        if (level > _logLevelSetting.Value) return true;
 2788        if (IsTypeBlocked(typeName)) return true;
 2789        if (IsCategoryBlocked(categoryName)) return true;
 2790        return false;
 791    }
 792}
 793record struct StandardRequestLogInfo(AmbientLogLevel Level);
 794record struct ErrorLogInfo(string ErrorType, string ErrorMessage, string? ErrorStackTrace, ErrorLogInfo[]? InnerExceptio
 795{
 796    public const string ExceptionSuffix = "Exception";
 797
 798    public ErrorLogInfo(Exception ex) : this(GetErrorType(ex), ex.Message, ex.StackTrace, ex is AggregateException aex
 799        ? aex.InnerExceptions?.Select(e => new ErrorLogInfo(e)).ToArray()
 800        : (ex.InnerException == null)
 801        ? null
 802        : new ErrorLogInfo[] { new(ex.InnerException) })
 803    {
 804    }
 805    private static string GetErrorType(Exception error)
 806    {
 807        string errorType = error.GetType().Name;
 808        if (errorType.EndsWith(ExceptionSuffix, StringComparison.Ordinal)) errorType = errorType.Substring(0, errorType.
 809        return errorType;
 810    }
 811}
 812record struct LogSummaryInfo(string? Summary);
 813
 814/// <summary>
 815/// A JsonConverterFactory for System.Net.IPAddress.
 816/// </summary>
 817public class IPAddressConverterFactory : JsonConverterFactory
 818{
 819    /// <summary>
 820    /// Checks to see if the specified type can be converted.
 821    /// </summary>
 822    /// <param name="typeToConvert">The type to check.</param>
 823    /// <returns>Whether or not the type can be converted.</returns>
 824    public override bool CanConvert(Type typeToConvert)
 825    {
 826#if NET5_0_OR_GREATER
 827        ArgumentNullException.ThrowIfNull(typeToConvert);
 828#else
 829        if (typeToConvert is null) throw new ArgumentNullException(nameof(typeToConvert));
 830#endif
 831        return typeof(System.Net.IPAddress).IsAssignableFrom(typeToConvert);
 832    }
 833    /// <summary>
 834    /// Creates a JsonConverter for the specified type.
 835    /// </summary>
 836    /// <param name="typeToConvert">The type to convert.</param>
 837    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
 838    /// <returns>The <see cref="JsonConverter"/>.</returns>
 839    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new IPAddressCo
 840}
 841
 842/// <summary>
 843/// A JsonConverter for System.Net.IPAddress.
 844/// </summary>
 845[JsonConverter(typeof(System.Net.IPAddress)), ProxyType(typeof(string))]
 846public class IPAddressConverter : JsonConverter<System.Net.IPAddress>
 847{
 848    /// <summary>
 849    /// Reads the JSON representation of the object.
 850    /// </summary>
 851    /// <param name="reader">The <see cref="Utf8JsonReader"/> to read the object from.</param>
 852    /// <param name="typeToConvert">The type to convert.</param>
 853    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use to interpret the formatting.</param>
 854    /// <returns>The <see cref="System.Net.IPAddress"/> that was deserialized.</returns>
 855    public override System.Net.IPAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions optio
 856    {
 857        string? ip = reader.GetString();
 858        System.Net.IPAddress? ipAddress = (ip == null) ? null : System.Net.IPAddress.Parse(ip);
 859        return ipAddress ?? System.Net.IPAddress.None;
 860    }
 861    /// <summary>
 862    /// Writes the JSON representation of the object.
 863    /// </summary>
 864    /// <param name="writer">The <see cref="Utf8JsonWriter"/> to write the object into.</param>
 865    /// <param name="value">The <see cref="System.Net.IPAddress"/> to write.</param>
 866    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use for formatting the data.</param>
 867    public override void Write(Utf8JsonWriter writer, System.Net.IPAddress value, JsonSerializerOptions options)
 868    {
 869#if NET5_0_OR_GREATER
 870        ArgumentNullException.ThrowIfNull(writer);
 871#else
 872        if (writer is null) throw new ArgumentNullException(nameof(writer));
 873#endif
 874        writer.WriteStringValue(value?.ToString());
 875    }
 876}
 877
 878#if NETCOREAPP1_0_OR_GREATER
 879/// <summary>
 880/// A JsonConverterFactory for System.Net.IPEndPoint.
 881/// </summary>
 882public class IPEndPointConverterFactory : JsonConverterFactory
 883{
 884    /// <summary>
 885    /// Checks to see if the specified type can be converted.
 886    /// </summary>
 887    /// <param name="typeToConvert">The type to check.</param>
 888    /// <returns>Whether or not the type can be converted.</returns>
 889    public override bool CanConvert(Type typeToConvert)
 890    {
 891#if NET5_0_OR_GREATER
 892        ArgumentNullException.ThrowIfNull(typeToConvert);
 893#else
 894        if (typeToConvert is null) throw new ArgumentNullException(nameof(typeToConvert));
 895#endif
 896        return typeof(System.Net.IPEndPoint).IsAssignableFrom(typeToConvert);
 897    }
 898    /// <summary>
 899    /// Creates a JsonConverter for the specified type.
 900    /// </summary>
 901    /// <param name="typeToConvert">The type to convert.</param>
 902    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
 903    /// <returns>The <see cref="JsonConverter"/>.</returns>
 904    public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new IPEndPointC
 905}
 906
 907/// <summary>
 908/// A JsonConverter for System.Net.IPEndPoint.
 909/// </summary>
 910[JsonConverter(typeof(System.Net.IPEndPoint)), ProxyType(typeof(string))]
 911public class IPEndPointConverter : JsonConverter<System.Net.IPEndPoint>
 912{
 913    /// <summary>
 914    /// Reads the JSON representation of the object.
 915    /// </summary>
 916    /// <param name="reader">The <see cref="Utf8JsonReader"/> to read the object from.</param>
 917    /// <param name="typeToConvert">The type to convert.</param>
 918    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use to interpret the formatting.</param>
 919    /// <returns>The <see cref="System.Net.IPEndPoint"/> that was deserialized.</returns>
 920    public override System.Net.IPEndPoint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions opti
 921    {
 922        string? ip = reader.GetString();
 923        System.Net.IPEndPoint? ipEndPoint = (ip == null) ? null : System.Net.IPEndPoint.Parse(ip);
 924        return ipEndPoint ?? System.Net.IPEndPoint.Parse("0");
 925    }
 926    /// <summary>
 927    /// Writes the JSON representation of the object.
 928    /// </summary>
 929    /// <param name="writer">The <see cref="Utf8JsonWriter"/> to write the object into.</param>
 930    /// <param name="value">The <see cref="System.Net.IPEndPoint"/> to write.</param>
 931    /// <param name="options">The <see cref="JsonSerializerOptions"/> to use for formatting the data.</param>
 932    public override void Write(Utf8JsonWriter writer, System.Net.IPEndPoint value, JsonSerializerOptions options)
 933    {
 934#if NET5_0_OR_GREATER
 935        ArgumentNullException.ThrowIfNull(writer);
 936#else
 937        if (writer is null) throw new ArgumentNullException(nameof(writer));
 938#endif
 939        writer.WriteStringValue(value?.ToString());
 940    }
 941}
 942#endif