StronglyTypedPrimitives 0.0.0-alpha.0.26
dotnet add package StronglyTypedPrimitives --version 0.0.0-alpha.0.26
NuGet\Install-Package StronglyTypedPrimitives -Version 0.0.0-alpha.0.26
<PackageReference Include="StronglyTypedPrimitives" Version="0.0.0-alpha.0.26" />
paket add StronglyTypedPrimitives --version 0.0.0-alpha.0.26
#r "nuget: StronglyTypedPrimitives, 0.0.0-alpha.0.26"
// Install StronglyTypedPrimitives as a Cake Addin #addin nuget:?package=StronglyTypedPrimitives&version=0.0.0-alpha.0.26&prerelease // Install StronglyTypedPrimitives as a Cake Tool #tool nuget:?package=StronglyTypedPrimitives&version=0.0.0-alpha.0.26&prerelease
Strongly Typed Primitives
A source generator for creating strongly-typed primitive types that makes it easy to avoid the primitive obsession anti pattern.
Features
- Ensure that a strongly-typed primitive is always valid (per
IsValueValid
method), or equal toEmpty
. - Any generated method or property can be overridden by the user. Don't like the generated code, just declare the method or property in the type and the generator will not generate it.
- Interoperable with other source generators. The "Value" property is visible to them and can be used in their generated code.
- Generates implementation of the following interfaces, if the underlying type supports them:
System.IParsable<TSelf>
System.ISpanParsable<TSelf>
System.IUtf8SpanParsable<TSelf>
System.IComparable<TSelf>
System.IComparable
System.IFormattable
System.ISpanFormattable
System.IUtf8SpanFormattable
- All types are marked with
IStronglyTypedPrimitive<TPrimitiveType>
andIStronglyTypedPrimitive
. - Supported primitive types:
string
int
Guid
DateTime
DateTimeOffset
TimeSpan
decimal
byte
Getting started
To get started, download the nuget StronglyTypedPrimitives and
add a [StronglyTyped]
attribute to a partial record struct that has one of the
supported primitive types as the first (and only) argument in it's constructor, for example:
namespace Examples;
[StronglyTyped]
public readonly partial record struct Example(int Value);
To constrain what values are legal for an strongly-typed primitive, implement the IsValueValid
method:
[StronglyTyped]
public readonly partial record struct ExampleWithConstraints(int Value)
{
public static bool IsValueValid(int value, bool throwIfInvalid)
{
if (value > 5)
return true;
if (throwIfInvalid)
throw new ArgumentException("Value must be at larger than 5", nameof(value));
return false;
}
}
The generated type will ensure that the value is always valid or Empty
, i.e.:
var tooLowValue = 5;
var goodValue = 6;
// The default value for a stringly typed primitive is the same as `Empty`.
// This makes it easy to test if an instance is valid or not.
Assert.Equal(ExampleWithConstraints.Empty, default(ExampleWithConstraints));
// Creating an instance with an invalid value results in an exception, both
// when instantiating an new instance of when cloning/with'ing the record.
Assert.Throws<ArgumentException>(() => new ExampleWithConstraints(tooLowValue));
Assert.Throws<ArgumentException>(() => ExampleWithConstraints.Empty with { Value = tooLowValue });
Generator output (int)
Given this type declaration:
namespace Examples;
[StronglyTyped]
public readonly partial record struct Example(int Value);
The following code is generated:
namespace Examples;
[System.CodeDom.Compiler.GeneratedCodeAttribute("StronglyTypedPrimitives, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "0.0.0.0")]
public readonly partial record struct Example : StronglyTypedPrimitives.IStronglyTypedPrimitive<int>, System.IParsable<Examples.Example>, System.ISpanParsable<Examples.Example>, System.IUtf8SpanParsable<Examples.Example>, System.IComparable<Examples.Example>, System.IComparable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable
{
public static readonly Example Empty = default;
private readonly int @value = ThrowIfValueIsInvalid(Value);
public int Value
{
get => @value;
init
{
@value = ThrowIfValueIsInvalid(value);
}
}
private static int ThrowIfValueIsInvalid(int value)
{
IsValueValid(value, throwIfInvalid: true);
return value;
}
public override string ToString() => Value.ToString();
public static bool IsValueValid(int value, bool throwIfInvalid)
=> true;
public static Example Parse(string s, System.IFormatProvider? provider)
{
var rawValue = int.Parse(s, provider);
IsValueValid(rawValue, throwIfInvalid: true);
return new Example(rawValue);
}
public static bool TryParse(string? s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(returnValue: false)] out Examples.Example result)
{
if (int.TryParse(s, provider, out var rawValue) && IsValueValid(rawValue, throwIfInvalid: false))
{
result = new Example(rawValue);
return true;
}
result = Example.Empty;
return false;
}
public static Example Parse(System.ReadOnlySpan<char> s, System.IFormatProvider? provider)
{
var rawValue = int.Parse(s, provider);
IsValueValid(rawValue, throwIfInvalid: true);
return new Example(rawValue);
}
public static bool TryParse(System.ReadOnlySpan<char> s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(returnValue: false)] out Examples.Example result)
{
if (int.TryParse(s, provider, out var rawValue) && IsValueValid(rawValue, throwIfInvalid: false))
{
result = new Example(rawValue);
return true;
}
result = Example.Empty;
return false;
}
public static Example Parse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider)
{
var rawValue = int.Parse(utf8Text, provider);
IsValueValid(rawValue, throwIfInvalid: true);
return new Example(rawValue);
}
public static bool TryParse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(returnValue: false)] out Examples.Example result)
{
if (int.TryParse(utf8Text, provider, out var rawValue) && IsValueValid(rawValue, throwIfInvalid: false))
{
result = new Example(rawValue);
return true;
}
result = Example.Empty;
return false;
}
public int CompareTo(Examples.Example other)
=> Value.CompareTo(other.Value);
public int CompareTo(object? obj)
{
if (obj is null)
{
return 1;
}
if (obj is Example other)
{
return Value.CompareTo(other.Value);
}
return ((System.IComparable)Value).CompareTo(obj);
}
public string ToString(string? format, System.IFormatProvider? formatProvider)
=> Value.ToString(format, formatProvider);
public bool TryFormat(System.Span<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider)
=> ((System.ISpanFormattable)Value).TryFormat(destination, out charsWritten, format, provider);
public bool TryFormat(System.Span<byte> utf8Destination, out int bytesWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider)
=> ((System.IUtf8SpanFormattable)Value).TryFormat(utf8Destination, out bytesWritten, format, provider);
public static bool operator > (Example a, Example b) => a.CompareTo(b) > 0;
public static bool operator < (Example a, Example b) => a.CompareTo(b) < 0;
public static bool operator >=(Example a, Example b) => a.CompareTo(b) >= 0;
public static bool operator <=(Example a, Example b) => a.CompareTo(b) <= 0;
}
Alternatives
There are other alternatives to this source generator that you can consider if you need something different:
Both are excellent and I have used them in the past.
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.0.0-alpha.0.26 | 49 | 2/26/2025 |
0.0.0-alpha.0.24 | 52 | 2/26/2025 |
0.0.0-alpha.0.23 | 53 | 2/26/2025 |
0.0.0-alpha.0.10 | 49 | 2/26/2025 |