Seekatar.OptionToStringGenerator 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Seekatar.OptionToStringGenerator --version 0.2.0                
NuGet\Install-Package Seekatar.OptionToStringGenerator -Version 0.2.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Seekatar.OptionToStringGenerator" Version="0.2.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Seekatar.OptionToStringGenerator --version 0.2.0                
#r "nuget: Seekatar.OptionToStringGenerator, 0.2.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Seekatar.OptionToStringGenerator as a Cake Addin
#addin nuget:?package=Seekatar.OptionToStringGenerator&version=0.2.0

// Install Seekatar.OptionToStringGenerator as a Cake Tool
#tool nuget:?package=Seekatar.OptionToStringGenerator&version=0.2.0                

OptionsToString Incremental Source Generator

OptionToStringGenerator codecov

Problem: I have a configuration class for use with IOptions and I want to safely log out its values at runtime.

Solution: Use an incremental source generator to generate an extension method to get a string with masked values for the properties.

This package generates an OptionToString extension method for a class. Using attributes you can control how the values are masked. You can use this to log out the values of your configuration at startup, or via a REST endpoint.

Quick Example

Edit the source of your configuration class and decorate it with attributes.

namespace Test;

[OptionsToString]
internal class PropertySimple
{
    [OutputMask]
    public string Secret { get; set; } = "Secret";

    public int RetryLimit { get; set; } = 5;

    [OutputRegex(Regex = "User Id=([^;]+).*Password=([^;]+)")]
    public string ConnectionString { get; set; } = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;";
}

// usage
_logger.LogInformation(new PropertySimple().OptionToString());

Output:

Test.PropertySimple:
  Secret           : "******"
  RetryLimit       : 5
  ConnectionString : "Server=myServerAddress;Database=myDataBase;User Id=***;Password=***;"

Alternatively, if you don't have the code for PropertySimple this will produce the same output.

internal class PropertyConfig
{
    [OutputPropertyMask(nameof(IOptionsSimple.Secret))]
    [OutputPropertyRegex(nameof(IOptionsSimple.ConnectionString), Regex = "User Id=([^;]+).*Password=([^;]+)")]
    public PropertySimple? PropertySimple { get; set; }
}

// usage
_logger.LogInformation(new PropertyConfig().PropertySimple.OptionToString());

Usage

  1. Add the OptionToStringGenerator NuGet package to your project.
  2. If you can update the class
    1. Decorate a class with the OptionsToString attribute.
    2. Optionally decorate properties with how you want them to be masked. If you don't decorate a property, its full text is dumped out.
  3. If you don't want to or can't update the class
    1. Add a property to your class of the Type you want to dump out.
    2. Decorate the property with multiple OutputProperty* attributes to control how the properties are masked.

Example of Editing a Class

Here's a larger sample class that uses all the different types of masking. Anything without an attribute has its value written out in the clear. The output follows.

namespace Test;
using Seekatar.OptionToStringGenerator;

[OptionsToString]
public class PublicOptions
{
    public class AClass
    {
        public string Name { get; set; } = "maybe this is secret";
        public override string ToString() => $"{nameof(AClass)}: {Name}";
    }

    public string PlainText { get; set; } = "hi mom";

    public char Why { get; set; } = 'Y';

    public int PlainInt { get; set; } = 42;

    public double PlainDouble { get; set; } = 3.141;

    public double PlainDecimal { get; set; } = 6.02;

    public DateTime PlainDateTime { get; set; } = new DateTime(2020, 1, 2, 3, 4, 5);

    public DateOnly PlainDatOnly { get; set; } = new DateOnly(2020, 1, 2);

    public TimeOnly PlainTimeOnly { get; set; } = new TimeOnly(12, 23, 2);

    public TimeSpan TimeSpan { get; set; } = new TimeSpan(1, 2, 3, 4, 5);

    public Guid UUID { get; set; } = Guid.Parse("6536b25c-3a45-48d8-8ea3-756e19f5bad1");

    public string? NullItem { get; set; }

    public AClass AnObject { get; set; } = new();

    [OutputRegex(Regex = @"AClass\:\s+(.*)")]
    public AClass AMaskedObject { get; set; } = new();

    [OutputMask]
    public string FullyMasked { get; set; } = "thisisasecret";

    [OutputMask(PrefixLen=3)]
    public string FirstThreeNotMasked { get; set; } = "abc1233435667";

    [OutputMask(SuffixLen=3)]
    public string LastThreeNotMasked { get; set; } = "abc1233435667";

    [OutputMask(PrefixLen = 3, SuffixLen=3)]
    public string FirstAndLastThreeNotMasked { get; set; } = "abc1233435667";

    [OutputMask(PrefixLen = 100)]
    public string NotMaskedSinceLongLength { get; set; } = "abc1233435667";

    [OutputLengthOnly]
    public string LengthOnly { get; set; } = "thisisasecretthatonlyshowsthelength";

    [OutputRegex(Regex="User Id=([^;]+).*Password=([^;]+)")]
    public string MaskUserAndPassword { get; set; } = "Server=server;Database=db;User Id=myUsername;Password=myPassword;";

    [OutputRegex(Regex="User Id=([^;]+).*Password=([^;]+)",IgnoreCase=true)]
    public string MaskUserAndPasswordIgnoreCase { get; set; } = "Server=server;Database=db;user Id=myUsername;Password=myPassword;";

    [OutputRegex(Regex = "User Id=([^;]+).*Password=([^;]+)")]
    public string RegexNotMatched { get; set; } = "Server=server;Database=db;user Id=myUsername;Password=myPassword;";

    public ConsoleColor Color { get; set; } = ConsoleColor.Red;

    [OutputIgnore]
    public string IgnoreMe { get; set; } = "abc1233435667";
}

// usage
var options = new PublicOptions();
_logger.LogInformation(options.OptionToString());

The output has the class name (by default) followed by an indented list of all the properties' values masked as specified.

Test.PublicOptions:
  PlainText                     : "hi mom"
  Why                           : "Y"
  PlainInt                      : 42
  PlainDouble                   : 3.141
  PlainDecimal                  : 6.02
  PlainDateTime                 : 01/02/2020 03:04:05
  PlainDatOnly                  : 01/02/2020
  PlainTimeOnly                 : 12:23
  TimeSpan                      : 1.02:03:04.0050000
  UUID                          : 6536b25c-3a45-48d8-8ea3-756e19f5bad1
  NullItem                      : null
  AnObject                      : "AClass: maybe this is secret"
  AMaskedObject                 : "AClass: ***"
  FullyMasked                   : "*************"
  FirstThreeNotMasked           : "abc**********"
  LastThreeNotMasked            : "**********667"
  FirstAndLastThreeNotMasked    : "abc*******667"
  NotMaskedSinceLongLength      : "abc1233435667"
  LengthOnly                    : Len = 35
  MaskUserAndPassword           : "Server=server;Database=db;User Id=***;Password=***;"
  MaskUserAndPasswordIgnoreCase : "Server=server;Database=db;user Id=***;Password=***;"
  RegexNotMatched               : "***Regex no match***!"
  Color                         : Red

Example of Using a Property

Here's a similar example where you don't have the source for the class, or don't want to change it. In this case, you use multiple OutputProperty* attributes, one for each property you want to mask.

This is from the tests where PropertyPublicClass is identical to PublicOptions, so the output will be the same aside from the class name.

namespace Test;
using Seekatar.OptionToStringGenerator;

public class PropertyTestOptions
{
    public MyClass(IOption<PropertyPublicClass> options, ILogger<PropertyTestOptions> logger)
    {
        _options =options.Value;
        logger.LogInformation(options.OptionToString());
    }

    [OutputPropertyRegex(nameof(PropertyPublicClass.AMaskedObject), Regex = @"AClass\:\s+(.*)")]
    [OutputPropertyMask(nameof(PropertyPublicClass.FullyMasked))]
    [OutputPropertyMask(nameof(PropertyPublicClass.FirstThreeNotMasked), PrefixLen = 3)]
    [OutputPropertyMask(nameof(PropertyPublicClass.LastThreeNotMasked), SuffixLen = 3)]
    [OutputPropertyMask(nameof(PropertyPublicClass.FirstAndLastThreeNotMasked), PrefixLen = 3, SuffixLen = 3)]
    [OutputPropertyMask(nameof(PropertyPublicClass.NotMaskedSinceLongLength), PrefixLen = 100)]
    [OutputPropertyLengthOnly(nameof(PropertyPublicClass.LengthOnly))]
    [OutputPropertyRegex(nameof(PropertyPublicClass.MaskUserAndPassword), Regex = "User Id=([^;]+).*Password=([^;]+)")]
    [OutputPropertyRegex(nameof(PropertyPublicClass.MaskUserAndPasswordIgnoreCase), Regex = "User Id=([^;]+).*Password=([^;]+)", IgnoreCase = true)]
    [OutputPropertyRegex(nameof(PropertyPublicClass.RegexNotMatched), Regex = "User Id=([^;]+).*Password=([^;]+)")]
    [OutputPropertyIgnore(nameof(PropertyPublicClass.IgnoreMe) )]
    public PropertyPublicClass? PublicClass { get; set; }
}

Notes

  • All public properties are included by default and output as plain text.
  • Use the OutputIgnore attribute to exclude a property.
  • ToString() is called on the property's value, then the mask is applied. You can have a custom ToString() method on a class to format its output then it will be masked as the AClass example above.
  • When editing the class, only one Output* attribute is allowed per property. If more than one is set, you'll get a compile warning, and the last attribute set will be used.
  • Regex strings with back slashes need to use a verbatim string or escape the back slashes (e.g. @"\s+" or "\\s+").
  • OutputRegex must have a Regex parameter, or you'll get a compile error.
  • If the regex doesn't match the value, the output will be ***Regex no match***! to indicate it didn't match.

Collections

Currently. you can create your own method to handle collections. The MessagingOptions test class does so by overriding ToString to get its options and all the children.

public override string ToString()
{
    var sb = new StringBuilder(this.OptionsToString());
    sb.AppendLine();
    foreach (var c in Consumers ?? new Dictionary<string, ClientOptions>())
    {
        sb.AppendLine(c.Value.OptionsToString());
    }
    foreach (var p in Producers ?? new Dictionary<string, ClientOptions>())
    {
        sb.AppendLine(p.Value.OptionsToString());
    }

    return sb.ToString();
}

Formatting Options

There are some properties on the OptionToStringAttribute for classes and OutputPropertyFormat to control how the output is generated.

Name Description Default
Indent The indenting string " " (Two spaces)
Separator The name-value separator ":"
Title The title to use for the output. See below Class name
Json Format the output as JSON false

In addition to literal text, the Title parameter can include property names in braces. For example

// for a class
[OptionsToString(Title = nameof(TitleOptions) + "_{StringProp}_{IntProp}")]
public class TitleOptions
{
    public int IntProp { get; set; } = 42;
    public string StringProp { get; set; } = "hi mom";
}

// for a property
internal class PropertyTestSimple
{
    [OutputPropertyFormat(Title = nameof(TitleOptions) + "_{StringProp}_{IntProp}")]
    public TitleOptions TitleOptions { get; set; } = new ();
}

Both will output

TitleOptions_hi mom_42:
  IntProp    : 42
  StringProp : "hi mom"

Attributes

For a class use these attributes.

Name On Description
OptionsToString Class Marker for the class, and has formatting options
OutputMask Member Mask the value with asterisks, with optional prefix and suffix clear
OutputRegex Member Mask the value with a regex
OutputLengthOnly Member Only output the length of the value
OutputIgnore Member Ignore the property

For a property, use these attributes on the property

Name Description
OutputPropertyFormat Optional Formatting options
OutputPropertyMask Mask the value with asterisks, with optional prefix and suffix
OutputPropertyRegex Mask the value with a regex
OutputPropertyLengthOnly Only output the length of the value
OutputPropertyIgnore Ignore the property

Warnings and Errors

If attributes have invalid parameters you will get warnings or errors from the compiler. They are documented here.

Trouble Shooting

error CS9057

You may get an error when compiling your code that uses this package.

##[error]#15 7.135 CSC : error CS9057: The analyzer assembly '/root/.nuget/packages/seekatar.optiontostringgenerator/0.1.4/analyzers/dotnet/cs/Seekatar.OptionToStringGenerator.dll' references version '4.6.0.0' of the compiler, which is newer than the currently running version '4.4.0.0'.

You must use the .NET SDK 7.0.201 or higher. You can check your version with dotnet --list-sdks.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.3.4 69 11/2/2024
0.3.3.82-prerelease 53 11/2/2024
0.3.3.77-prerelease 91 5/28/2024
0.3.3 5,279 5/28/2024
0.3.2.75-prerelease 86 5/27/2024
0.3.2 92 5/27/2024
0.3.1.72-prerelease 93 2/10/2024
0.3.1.71-prerelease 92 2/10/2024
0.3.1 202 2/10/2024
0.3.0.69-prerelease 97 1/31/2024
0.3.0.67-prerelease 97 1/13/2024
0.3.0 230 1/13/2024
0.3.0-prerelease 86 1/13/2024
0.2.3 164 1/1/2024
0.2.2 303 12/4/2023
0.2.1 27,321 11/14/2023
0.2.0 125 11/11/2023
0.1.4 309 10/12/2023
0.1.3 152 10/9/2023
0.1.2-prerelease 126 9/5/2023
0.1.1-prerelease 121 9/4/2023