Seekatar.OptionToStringGenerator
0.3.0.69-prerelease
See the version list below for details.
dotnet add package Seekatar.OptionToStringGenerator --version 0.3.0.69-prerelease
NuGet\Install-Package Seekatar.OptionToStringGenerator -Version 0.3.0.69-prerelease
<PackageReference Include="Seekatar.OptionToStringGenerator" Version="0.3.0.69-prerelease" />
paket add Seekatar.OptionToStringGenerator --version 0.3.0.69-prerelease
#r "nuget: Seekatar.OptionToStringGenerator, 0.3.0.69-prerelease"
// Install Seekatar.OptionToStringGenerator as a Cake Addin #addin nuget:?package=Seekatar.OptionToStringGenerator&version=0.3.0.69-prerelease&prerelease // Install Seekatar.OptionToStringGenerator as a Cake Tool #tool nuget:?package=Seekatar.OptionToStringGenerator&version=0.3.0.69-prerelease&prerelease
OptionsToString Incremental Source Generator
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.
The methods to mask the values can be used outside of the generated code, too. See below for details.
This package generates an OptionsToString
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().OptionsToString());
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.OptionsToString());
Usage
- Add the OptionToStringGenerator NuGet package to your project.
- If you can update the class
- Decorate a class with the
OptionsToString
attribute. - Optionally decorate properties with an
Output*
attribute to specify how you want them to be masked. If you don't decorate a property, its full text is dumped out.
- Decorate a class with the
- If you don't want to or can't update the class
- Add a property to your class of the Type you want to dump out.
- 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.OptionsToString());
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.OptionsToString());
}
[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.
- Properties will be in the order they are defined in the class, unless
Sort=true
is set on theOptionsToString
attribute. - Parent class properties are included by default. Use
ExcludeParents = true
on theOptionsToString
attribute to exclude them. - 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 customToString()
method on a class to format its output then it will be masked as theAClass
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 aRegex
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. - To customize the formatting of masked output see below
Formatting Options
There are properties on the OptionsToStringAttribute
for classes and OutputPropertyFormat
for properties 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 |
Sort |
Sort the properties | 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"
Per-Property Formatting Options
For types that take a format string to ToString()
such as DateTime
, numbers, etc., you can use the OutputFormatToString
attribute. You can also supply a custom method to format a property. For example flattening an array and masking its values. The sample below shows a few examples:
# comma separate thousands
[OutputFormatToString("N0")]
public int PlainInt { get; set; } = 423433;
# two decimal places
[OutputFormatToString("0.00")]
public double PlainDouble { get; set; } = 3.141;
# use the U format for DateTime
[OutputFormatToString("R")]
public DateTime PlainDateTime { get; set; } = new DateTime(2020, 1, 2, 3, 4, 5);
[OutputFormatProvider(typeof(FormatOptions), nameof(MyFormatter))]
public List<string> Secrets { get; set; } = new List<string> { "secret", "hushhush", "psssst" };
# mask each string in the array showing only the first 3 characters
public static string? MyFormatter(List<string> o)
{
if (o is null) return null;
return string.Join(",", o.Select(s => Mask.MaskSuffix(s, 3)));
}
Output:
PlainInt : 423,433
PlainDouble : 3.14
PlainDateTime : Thu, 02 Jan 2020 03:04:05 GMT
Secrets : "sec***,hus*****,pss***"
Collections
Instead of using OutputFormatProvider
, 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();
}
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 |
OutputFormatToString | Member | Format the value using ToString() with a format string |
OutputFormatProvider | Member | Format the value using a custom method |
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 6.0.416 or higher. You can check your version with dotnet --list-sdks
.
Using Seekatar.Mask
The methods used by the generated code to mask a value are available when you include the source generator NuGet package. They are in the Seekatar.Mask
namespace.
using static Seekatar.Mask;
...
MaskSuffix("abc123", 3) // returns "abc***"
Methods are as follows. Each of these corresponds to an attribute as described above. All take object?
and return string?
. Check each for parameters that control usage.
Method | Description |
---|---|
MaskAll | Return a string of the same length as the input, with all characters masked |
MaskLengthOnly | Return Len <length> |
MaskPrefix | Mask the prefix of the string, showing only a few suffix characters |
MaskPrefixSuffix | Show only a few prefix and suffix characters |
MaskRegex | Mask capture groups of a regex |
MaskSuffix | Mask the suffix of the string, showing only a few prefix characters |
Product | Versions 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. |
-
.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 |