diff --git a/.gitattributes b/.gitattributes
index 21f83a7..cd1f4e4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -12,5 +12,9 @@
*.fsproj
*.dbproj
+# Verify snapshot files
+*.verified.txt text eol=lf working-tree-encoding=UTF-8
+*.verified.cs text eol=lf working-tree-encoding=UTF-8
+
# Don't check for trailing whitespace at end of lines in the doc pages
*.md -whitespace=blank-at-eol
diff --git a/.gitignore b/.gitignore
index 0f0f4e1..c79b456 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,9 @@
# SourceGenerator Launch Settings
src/CompositeKey.SourceGeneration/Properties/launchSettings.json
+# Verify snapshot testing
+*.received.*
+
# Claude Code
.claude/settings.local.json
.claude/todos/
diff --git a/CLAUDE.md b/CLAUDE.md
index e7b3551..0fcb78e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -63,6 +63,14 @@ The solution is split into four main projects plus corresponding test projects:
- Commit messages follow Conventional Commits (`feat:`, `fix:`, `perf:`, etc.) — GitVersion and `.versionrc` drive versioning and changelog
- EditorConfig: 4-space indent, UTF-8, LF line endings, 120-char max line length
-- Test stack: xunit + Shouldly + AutoFixture
+- Test stack: xunit + Shouldly + AutoFixture + Verify (snapshot testing)
- Unit tests use `CompilationHelper` to create in-memory Roslyn compilations
- Functional tests define real key types and test format/parse round-trips
+- Snapshot tests (`Snapshots/` in `SourceGeneration.UnitTests`) use
+ Verify.SourceGenerators to capture generated `.g.cs` output and
+ `GenerationSpec` models as `.verified.*` baselines — any change to
+ emitted code or parser output will cause a snapshot diff failure
+- `*.received.*` files are gitignored; only `*.verified.*` baselines
+ are committed
+- The `ModuleInitializer` scrubs the non-deterministic
+ `GeneratedCodeAttribute` version string to keep snapshots stable
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj b/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj
index 188c1da..89a3a22 100644
--- a/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj
+++ b/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj
@@ -15,6 +15,8 @@
+
+
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs b/src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs
new file mode 100644
index 0000000..36c33c8
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs
@@ -0,0 +1,24 @@
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using VerifyXunit;
+
+namespace CompositeKey.SourceGeneration.UnitTests;
+
+public static class ModuleInitializer
+{
+ [ModuleInitializer]
+ public static void Init()
+ {
+ VerifySourceGenerators.Initialize();
+
+ // Scrub the non-deterministic informational version (includes git commit hash)
+ // from the GeneratedCodeAttribute emitted by the source generator.
+ // e.g. [GeneratedCodeAttribute("CompositeKey.SourceGeneration", "1.6.0+7675481fa523...")]
+ // -> [GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ VerifierSettings.ScrubLinesWithReplace(line =>
+ Regex.Replace(
+ line,
+ @"GeneratedCodeAttribute\(""CompositeKey\.SourceGeneration"", ""[^""]*""\)",
+ @"GeneratedCodeAttribute(""CompositeKey.SourceGeneration"", ""VERSION"")"));
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt
new file mode 100644
index 0000000..b755a27
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt
@@ -0,0 +1,298 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true,
+ EnumSpec: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum,
+ UnderlyingType: int,
+ Members: [
+ {
+ Name: One,
+ Value: 0
+ },
+ {
+ Name: Two,
+ Value: 1
+ },
+ {
+ Name: Three,
+ Value: 2
+ }
+ ],
+ IsSequentialFromZero: true
+ }
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ AllParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: b,
+ LengthRequired: 38,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: ConstantValue,
+ LengthRequired: 13,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true,
+ EnumSpec: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum,
+ UnderlyingType: int,
+ Members: [
+ {
+ Name: One,
+ Value: 0
+ },
+ {
+ Name: Two,
+ Value: 1
+ },
+ {
+ Name: Three,
+ Value: 2
+ }
+ ],
+ IsSequentialFromZero: true
+ }
+ },
+ Format: g,
+ ParseType: Enum,
+ FormatType: Enum,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PartitionKeyParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: b,
+ LengthRequired: 38,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PrimaryDelimiterKeyPart: {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ SortKeyParts: [
+ {
+ Value: ConstantValue,
+ LengthRequired: 13,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true,
+ EnumSpec: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum,
+ UnderlyingType: int,
+ Members: [
+ {
+ Name: One,
+ Value: 0
+ },
+ {
+ Name: Two,
+ Value: 1
+ },
+ {
+ Name: Three,
+ Value: 2
+ }
+ ],
+ IsSequentialFromZero: true
+ }
+ },
+ Format: g,
+ ParseType: Enum,
+ FormatType: Enum,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..a26951a
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,311 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : ICompositePrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(54 + SecondPart.Length + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ state.SecondPart.CopyTo(destination[position..]);
+ position += state.SecondPart.Length;
+
+ destination[position] = '|';
+ position += 1;
+
+ "ConstantValue".CopyTo(destination[position..]);
+ position += 13;
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(39 + SecondPart.Length, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ state.SecondPart.CopyTo(destination[position..]);
+ position += state.SecondPart.Length;
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#{SecondPart}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public string ToSortKeyString()
+ {
+ return string.Create(14 + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) =>
+ {
+ int position = 0;
+
+ "ConstantValue".CopyTo(destination[position..]);
+ position += 13;
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:g}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result);
+ }
+
+ public static BasicPrimaryKey Parse(string partitionKey, string sortKey)
+ {
+ ArgumentNullException.ThrowIfNull(partitionKey);
+ ArgumentNullException.ThrowIfNull(sortKey);
+
+ return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey)
+ {
+ if (partitionKey.Length < 40)
+ throw new FormatException("Unrecognized format.");
+
+ if (sortKey.Length < 15)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[1]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString();
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal))
+ throw new FormatException("Unrecognized format.");
+
+ if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (partitionKey is null || sortKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (partitionKey.Length < 40)
+ return false;
+
+ if (sortKey.Length < 15)
+ return false;
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ return false;
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart))
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[1]].Length == 0)
+ return false;
+
+ string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString();
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal))
+ return false;
+
+ if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+ file class CustomEnumHelper
+ {
+ private static readonly int[] Lengths = new[] { 3, 3, 5 };
+ private static readonly string[] Names = new[] { "One", "Two", "Three" };
+
+ public static int GetFormattedLength(global::UnitTests.CustomEnum value)
+ {
+ return Lengths[(uint)value];
+ }
+
+ public static bool TryFormat(global::UnitTests.CustomEnum value, Span destination, out int charsWritten)
+ {
+ charsWritten = 0;
+ if ((uint)value >= Names.Length)
+ return false;
+
+ int formattedLength = Lengths[(uint)value];
+ if (destination.Length < formattedLength)
+ return false;
+
+ charsWritten = formattedLength;
+ Names[(uint)value].CopyTo(destination);
+ return true;
+ }
+
+ public static bool TryParse(in ReadOnlySpan value, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out global::UnitTests.CustomEnum result)
+ {
+ switch (value)
+ {
+ case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.One), StringComparison.Ordinal):
+ result = global::UnitTests.CustomEnum.One;
+ return true;
+ case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Two), StringComparison.Ordinal):
+ result = global::UnitTests.CustomEnum.Two;
+ return true;
+ case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Three), StringComparison.Ordinal):
+ result = global::UnitTests.CustomEnum.Three;
+ return true;
+ default:
+ result = default;
+ return false;
+ }
+ }
+
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt
new file mode 100644
index 0000000..40d6385
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt
@@ -0,0 +1,298 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicNonSequentialEnum,
+ FullyQualifiedName: global::UnitTests.BasicNonSequentialEnum
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicNonSequentialEnum
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true,
+ EnumSpec: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum,
+ UnderlyingType: int,
+ Members: [
+ {
+ Name: One,
+ Value: 3
+ },
+ {
+ Name: Two,
+ Value: 2
+ },
+ {
+ Name: Three,
+ Value: 1
+ }
+ ],
+ IsSequentialFromZero: false
+ }
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicNonSequentialEnum
+ },
+ Key: {
+ AllParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: b,
+ LengthRequired: 38,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: ConstantValue,
+ LengthRequired: 13,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true,
+ EnumSpec: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum,
+ UnderlyingType: int,
+ Members: [
+ {
+ Name: One,
+ Value: 3
+ },
+ {
+ Name: Two,
+ Value: 2
+ },
+ {
+ Name: Three,
+ Value: 1
+ }
+ ],
+ IsSequentialFromZero: false
+ }
+ },
+ Format: g,
+ ParseType: Enum,
+ FormatType: Enum,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PartitionKeyParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: b,
+ LengthRequired: 38,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PrimaryDelimiterKeyPart: {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ SortKeyParts: [
+ {
+ Value: ConstantValue,
+ LengthRequired: 13,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true,
+ EnumSpec: {
+ Name: CustomEnum,
+ FullyQualifiedName: global::UnitTests.CustomEnum,
+ UnderlyingType: int,
+ Members: [
+ {
+ Name: One,
+ Value: 3
+ },
+ {
+ Name: Two,
+ Value: 2
+ },
+ {
+ Name: Three,
+ Value: 1
+ }
+ ],
+ IsSequentialFromZero: false
+ }
+ },
+ Format: g,
+ ParseType: Enum,
+ FormatType: Enum,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs
new file mode 100644
index 0000000..02ae103
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs
@@ -0,0 +1,322 @@
+//HintName: UnitTests.BasicNonSequentialEnum.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicNonSequentialEnum : ICompositePrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(54 + SecondPart.Length + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ state.SecondPart.CopyTo(destination[position..]);
+ position += state.SecondPart.Length;
+
+ destination[position] = '|';
+ position += 1;
+
+ "ConstantValue".CopyTo(destination[position..]);
+ position += 13;
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(39 + SecondPart.Length, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ state.SecondPart.CopyTo(destination[position..]);
+ position += state.SecondPart.Length;
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#{SecondPart}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public string ToSortKeyString()
+ {
+ return string.Create(14 + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) =>
+ {
+ int position = 0;
+
+ "ConstantValue".CopyTo(destination[position..]);
+ position += 13;
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:g}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicNonSequentialEnum Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicNonSequentialEnum Parse(ReadOnlySpan primaryKey)
+ {
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result)
+ {
+ result = null;
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result);
+ }
+
+ public static BasicNonSequentialEnum Parse(string partitionKey, string sortKey)
+ {
+ ArgumentNullException.ThrowIfNull(partitionKey);
+ ArgumentNullException.ThrowIfNull(sortKey);
+
+ return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey);
+ }
+
+ public static BasicNonSequentialEnum Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey)
+ {
+ if (partitionKey.Length < 40)
+ throw new FormatException("Unrecognized format.");
+
+ if (sortKey.Length < 15)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[1]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString();
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal))
+ throw new FormatException("Unrecognized format.");
+
+ if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicNonSequentialEnum(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result)
+ {
+ if (partitionKey is null || sortKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result)
+ {
+ result = null;
+
+ if (partitionKey.Length < 40)
+ return false;
+
+ if (sortKey.Length < 15)
+ return false;
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ return false;
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart))
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[1]].Length == 0)
+ return false;
+
+ string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString();
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal))
+ return false;
+
+ if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart))
+ return false;
+
+ result = new BasicNonSequentialEnum(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicNonSequentialEnum IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum result) => TryParse(s, out result);
+
+ ///
+ static BasicNonSequentialEnum ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum result) => TryParse(s, out result);
+ }
+ file class CustomEnumHelper
+ {
+ public static int GetFormattedLength(global::UnitTests.CustomEnum value)
+ {
+ return value switch
+ {
+ global::UnitTests.CustomEnum.One => 3,
+ global::UnitTests.CustomEnum.Two => 3,
+ global::UnitTests.CustomEnum.Three => 5,
+ _ => throw new ArgumentOutOfRangeException(nameof(value), value, "The value provided is out of range.")
+ };
+ }
+
+ public static bool TryFormat(global::UnitTests.CustomEnum value, Span destination, out int charsWritten)
+ {
+ charsWritten = GetFormattedLength(value);
+ if (destination.Length < charsWritten)
+ return false;
+
+ switch (value)
+ {
+ case global::UnitTests.CustomEnum.One:
+ "One".CopyTo(destination);
+ return true;
+ case global::UnitTests.CustomEnum.Two:
+ "Two".CopyTo(destination);
+ return true;
+ case global::UnitTests.CustomEnum.Three:
+ "Three".CopyTo(destination);
+ return true;
+ default:
+ charsWritten = 0;
+ return false;
+ }
+ }
+
+ public static bool TryParse(in ReadOnlySpan value, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out global::UnitTests.CustomEnum result)
+ {
+ switch (value)
+ {
+ case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.One), StringComparison.Ordinal):
+ result = global::UnitTests.CustomEnum.One;
+ return true;
+ case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Two), StringComparison.Ordinal):
+ result = global::UnitTests.CustomEnum.Two;
+ return true;
+ case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Three), StringComparison.Ordinal):
+ result = global::UnitTests.CustomEnum.Three;
+ return true;
+ default:
+ result = default;
+ return false;
+ }
+ }
+
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt
new file mode 100644
index 0000000..8bb9792
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt
@@ -0,0 +1,147 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: n,
+ LengthRequired: 32,
+ ExactLengthRequirement: true
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..64711a1
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,181 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(106, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "n", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(106, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "n", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#{ThirdPart:n}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length != 106)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 32 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "n", out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length != 106)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 32 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "n", out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt
new file mode 100644
index 0000000..3095ddb
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt
@@ -0,0 +1,234 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::AnotherNamespace.BasicPrimaryKey
+ },
+ Namespace: AnotherNamespace,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ AllParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: ConstantValue,
+ LengthRequired: 13,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: x,
+ LengthRequired: 32,
+ ExactLengthRequirement: false
+ }
+ ],
+ PartitionKeyParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PrimaryDelimiterKeyPart: {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ SortKeyParts: [
+ {
+ Value: ConstantValue,
+ LengthRequired: 13,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: x,
+ LengthRequired: 32,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..d9749f8
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,218 @@
+//HintName: AnotherNamespace.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace AnotherNamespace
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : ICompositePrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}|ConstantValue#{ThirdPart:x}");
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(37 + SecondPart.Length, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ state.SecondPart.CopyTo(destination[position..]);
+ position += state.SecondPart.Length;
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public string ToSortKeyString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:x}");
+ }
+
+ public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:x}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result);
+ }
+
+ public static BasicPrimaryKey Parse(string partitionKey, string sortKey)
+ {
+ ArgumentNullException.ThrowIfNull(partitionKey);
+ ArgumentNullException.ThrowIfNull(sortKey);
+
+ return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey)
+ {
+ if (partitionKey.Length < 38)
+ throw new FormatException("Unrecognized format.");
+
+ if (sortKey.Length < 46)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[1]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString();
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal))
+ throw new FormatException("Unrecognized format.");
+
+ if (!Guid.TryParseExact(sortKey[sortKeyPartRanges[1]], "x", out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (partitionKey is null || sortKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (partitionKey.Length < 38)
+ return false;
+
+ if (sortKey.Length < 46)
+ return false;
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ return false;
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[1]].Length == 0)
+ return false;
+
+ string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString();
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal))
+ return false;
+
+ if (!Guid.TryParseExact(sortKey[sortKeyPartRanges[1]], "x", out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..9a40717
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,181 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(112, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "p", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(112, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "p", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:p}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:p}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:p}#{ThirdPart:d}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length != 112)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 38 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "p", out var secondPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length != 112)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 38 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "p", out var secondPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt
new file mode 100644
index 0000000..8285960
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt
@@ -0,0 +1,149 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: firstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: secondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: thirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: SpanParsable,
+ FormatType: SpanFormattable,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..9328625
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,125 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}");
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}");
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length < 40)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString();
+
+ if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length < 40)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length == 0)
+ return false;
+
+ string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString();
+
+ if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt
new file mode 100644
index 0000000..368b866
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt
@@ -0,0 +1,170 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: true,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: true,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: true,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: firstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: secondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: thirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ PropertyInitializers: [
+ {
+ PropertyType: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ MatchesConstructorParameter: true
+ },
+ {
+ PropertyType: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1,
+ MatchesConstructorParameter: true
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: true,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: true,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: true,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ },
+ ParseType: SpanParsable,
+ FormatType: SpanFormattable,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..c3de9dc
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,125 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}");
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}");
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length < 40)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString();
+
+ if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart) { FirstPart = firstPart, SecondPart = secondPart };
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length < 40)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length == 0)
+ return false;
+
+ string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString();
+
+ if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart) { FirstPart = firstPart, SecondPart = secondPart };
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt
new file mode 100644
index 0000000..722a76f
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt
@@ -0,0 +1,149 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: firstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: secondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: thirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Int32,
+ FullyQualifiedName: int
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ },
+ ParseType: SpanParsable,
+ FormatType: SpanFormattable,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..9328625
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,125 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}");
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}");
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length < 40)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString();
+
+ if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length < 40)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length == 0)
+ return false;
+
+ string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString();
+
+ if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt
new file mode 100644
index 0000000..6cc2547
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt
@@ -0,0 +1,150 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ PropertyInitializers: [
+ {
+ PropertyType: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ MatchesConstructorParameter: false
+ },
+ {
+ PropertyType: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1,
+ MatchesConstructorParameter: false
+ },
+ {
+ PropertyType: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2,
+ MatchesConstructorParameter: false
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..18eb8b6
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,181 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(110, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(110, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#{ThirdPart:d}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length != 110)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey() { FirstPart = firstPart, SecondPart = secondPart, ThirdPart = thirdPart };
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length != 110)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey() { FirstPart = firstPart, SecondPart = secondPart, ThirdPart = thirdPart };
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt
new file mode 100644
index 0000000..7564d22
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt
@@ -0,0 +1,216 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Double,
+ FullyQualifiedName: double
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: Double,
+ FullyQualifiedName: double
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ AllParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: b,
+ LengthRequired: 38,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Double,
+ FullyQualifiedName: double
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: SpanParsable,
+ FormatType: SpanFormattable,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ },
+ {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PartitionKeyParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: b,
+ LengthRequired: 38,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Double,
+ FullyQualifiedName: double
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: SpanParsable,
+ FormatType: SpanFormattable,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PrimaryDelimiterKeyPart: {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ SortKeyParts: [
+ {
+ Property: {
+ Type: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ ParseType: String,
+ FormatType: String,
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: false
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..fb9a482
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,190 @@
+//HintName: UnitTests.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : ICompositePrimaryKey
+ {
+ public override string ToString()
+ {
+ return $"{FirstPart:b}#{SecondPart}|{ThirdPart}";
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return $"{FirstPart:b}#{SecondPart}";
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => $"{FirstPart:b}",
+ (0, true) => $"{FirstPart:b}#",
+ (1, false) => $"{FirstPart:b}#{SecondPart}",
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public string ToSortKeyString()
+ {
+ return string.Create(0 + ThirdPart.Length, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ state.ThirdPart.CopyTo(destination[position..]);
+ position += state.ThirdPart.Length;
+ });
+ }
+
+ public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => $"{ThirdPart}",
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result);
+ }
+
+ public static BasicPrimaryKey Parse(string partitionKey, string sortKey)
+ {
+ ArgumentNullException.ThrowIfNull(partitionKey);
+ ArgumentNullException.ThrowIfNull(sortKey);
+
+ return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey)
+ {
+ if (partitionKey.Length < 40)
+ throw new FormatException("Unrecognized format.");
+
+ if (sortKey.Length < 1)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (!double.TryParse(partitionKey[partitionKeyPartRanges[1]], out var secondPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (sortKey.Length == 0)
+ throw new FormatException("Unrecognized format.");
+
+ string thirdPart = sortKey.ToString();
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (partitionKey is null || sortKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (partitionKey.Length < 40)
+ return false;
+
+ if (sortKey.Length < 1)
+ return false;
+
+ const int expectedPartitionKeyParts = 2;
+ Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1];
+ if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts)
+ return false;
+
+ if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart))
+ return false;
+
+ if (!double.TryParse(partitionKey[partitionKeyPartRanges[1]], out var secondPart))
+ return false;
+
+ if (sortKey.Length == 0)
+ return false;
+
+ string thirdPart = sortKey.ToString();
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt
new file mode 100644
index 0000000..ad47dbb
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt
new file mode 100644
index 0000000..fabb714
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt
@@ -0,0 +1,17 @@
+{
+ Diagnostics: [
+ {
+ Location: : (7,22)-(7,37),
+ Message: The 'CompositeKey' type 'BasicPrimaryKey' has no obvious constructor, at present, only types with either a single constructor or types with a parameterless constructor are supported.,
+ Severity: Error,
+ Descriptor: {
+ Id: COMPOSITE0004,
+ Title: The type annotated with the 'CompositeKey' attribute has no obvious constructor.,
+ MessageFormat: The 'CompositeKey' type '{0}' has no obvious constructor, at present, only types with either a single constructor or types with a parameterless constructor are supported.,
+ Category: CompositeKey.SourceGeneration,
+ DefaultSeverity: Error,
+ IsEnabledByDefault: true
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt
new file mode 100644
index 0000000..44d4068
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt
@@ -0,0 +1,115 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.OutermostClass.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ private partial record BasicPrimaryKey,
+ public static partial class OutermostClass
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: Constant,
+ LengthRequired: 8,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..c61e979
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,176 @@
+//HintName: UnitTests.OutermostClass.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ public static partial class OutermostClass
+ {
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ private partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(82, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ "Constant".CopyTo(destination[position..]);
+ position += 8;
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(82, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ "Constant".CopyTo(destination[position..]);
+ position += 8;
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#Constant"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#Constant#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#Constant#{SecondPart:d}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length != 82)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (!primaryKey[primaryKeyPartRanges[1]].Equals("Constant", StringComparison.Ordinal))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var secondPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length != 82)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (!primaryKey[primaryKeyPartRanges[1]].Equals("Constant", StringComparison.Ordinal))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var secondPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt
new file mode 100644
index 0000000..c9fc114
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt
@@ -0,0 +1,148 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: BasicPrimaryKey,
+ FullyQualifiedName: global::UnitTests.OutermostClass.BasicPrimaryKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record BasicPrimaryKey,
+ public static partial class OutermostClass
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ }
+ ],
+ ConstructorParameters: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ ParameterIndex: 1
+ },
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ ParameterIndex: 2
+ }
+ ],
+ TypeName: BasicPrimaryKey
+ },
+ Key: {
+ Parts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: FirstPart,
+ CamelCaseName: firstPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: SecondPart,
+ CamelCaseName: secondPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: #,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: ThirdPart,
+ CamelCaseName: thirdPart,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: true
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs
new file mode 100644
index 0000000..6d7c901
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs
@@ -0,0 +1,184 @@
+//HintName: UnitTests.OutermostClass.BasicPrimaryKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ public static partial class OutermostClass
+ {
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record BasicPrimaryKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ return string.Create(110, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(110, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += firstPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += secondPartCharsWritten;
+ }
+
+ destination[position] = '#';
+ position += 1;
+
+ {
+ if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += thirdPartCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"),
+ (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"),
+ (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}"),
+ (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#"),
+ (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#{ThirdPart:d}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public static BasicPrimaryKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length != 110)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart))
+ throw new FormatException("Unrecognized format.");
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart))
+ throw new FormatException("Unrecognized format.");
+
+ return new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length != 110)
+ return false;
+
+ const int expectedPrimaryKeyParts = 3;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart))
+ return false;
+
+ if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart))
+ return false;
+
+ result = new BasicPrimaryKey(firstPart, secondPart, thirdPart);
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+
+ ///
+ static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result);
+ }
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt
new file mode 100644
index 0000000..d106e4f
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt
@@ -0,0 +1,184 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: UserTagKey,
+ FullyQualifiedName: global::UnitTests.UserTagKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record UserTagKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: UserId,
+ CamelCaseName: userId,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ },
+ {
+ Type: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false,
+ CollectionType: List
+ }
+ ],
+ PropertyInitializers: [
+ {
+ PropertyType: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: UserId,
+ CamelCaseName: userId,
+ MatchesConstructorParameter: false
+ },
+ {
+ PropertyType: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ ParameterIndex: 1,
+ MatchesConstructorParameter: false
+ }
+ ],
+ TypeName: UserTagKey
+ },
+ Key: {
+ AllParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: UserId,
+ CamelCaseName: userId,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: TAG,
+ LengthRequired: 3,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: _,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false,
+ CollectionType: List
+ },
+ Separator: #,
+ InnerParseType: String,
+ InnerFormatType: String,
+ InnerType: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ PartitionKeyParts: [
+ {
+ Property: {
+ Type: {
+ Name: Guid,
+ FullyQualifiedName: global::System.Guid
+ },
+ Name: UserId,
+ CamelCaseName: userId,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false
+ },
+ Format: d,
+ LengthRequired: 36,
+ ExactLengthRequirement: true
+ }
+ ],
+ PrimaryDelimiterKeyPart: {
+ Value: |,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ SortKeyParts: [
+ {
+ Value: TAG,
+ LengthRequired: 3,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: _,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false,
+ CollectionType: List
+ },
+ Separator: #,
+ InnerParseType: String,
+ InnerFormatType: String,
+ InnerType: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs
new file mode 100644
index 0000000..f6db9c3
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs
@@ -0,0 +1,270 @@
+//HintName: UnitTests.UserTagKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record UserTagKey : ICompositePrimaryKey
+ {
+ public override string ToString()
+ {
+ if (Tags.Count == 0)
+ throw new FormatException("Collection must contain at least one item.");
+
+ var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(5, 1, global::System.Globalization.CultureInfo.InvariantCulture);
+ handler.AppendFormatted(UserId, "d");
+ handler.AppendLiteral("|");
+ handler.AppendLiteral("TAG");
+ handler.AppendLiteral("_");
+ for (int i = 0; i < Tags.Count; i++)
+ {
+ if (i > 0)
+ handler.AppendLiteral("#");
+
+ handler.AppendFormatted(Tags[i]);
+ }
+
+ return handler.ToStringAndClear();
+ }
+
+ public string ToPartitionKeyString()
+ {
+ return string.Create(36, this, static (destination, state) =>
+ {
+ int position = 0;
+
+ {
+ if (!((ISpanFormattable)state.UserId).TryFormat(destination[position..], out int userIdCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture))
+ throw new FormatException();
+
+ position += userIdCharsWritten;
+ }
+ });
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ return (throughPartIndex, includeTrailingDelimiter) switch
+ {
+ (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{UserId:d}"),
+ _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided")
+ };
+ }
+
+ public string ToSortKeyString()
+ {
+ if (Tags.Count == 0)
+ throw new FormatException("Collection must contain at least one item.");
+
+ var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(4, 0, global::System.Globalization.CultureInfo.InvariantCulture);
+ handler.AppendLiteral("TAG");
+ handler.AppendLiteral("_");
+ for (int i = 0; i < Tags.Count; i++)
+ {
+ if (i > 0)
+ handler.AppendLiteral("#");
+
+ handler.AppendFormatted(Tags[i]);
+ }
+
+ return handler.ToStringAndClear();
+ }
+
+ public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ switch (throughPartIndex, includeTrailingDelimiter)
+ {
+ case (0, false): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG");
+ case (0, true): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_");
+ }
+
+ int fixedPartCount = 1;
+ int repeatIndex = throughPartIndex - fixedPartCount;
+ int repeatCount = Math.Min(repeatIndex + 1, Tags.Count);
+ if (repeatCount <= 0)
+ throw new InvalidOperationException("Invalid throughPartIndex for repeating section.");
+
+ var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(0, 0, global::System.Globalization.CultureInfo.InvariantCulture);
+ handler.AppendFormatted(string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_"));
+
+ for (int i = 0; i < repeatCount; i++)
+ {
+ if (i > 0)
+ {
+ handler.AppendLiteral("#");
+ }
+
+ handler.AppendFormatted(Tags[i]);
+ }
+
+ if (includeTrailingDelimiter)
+ {
+ handler.AppendLiteral("#");
+ }
+
+ return handler.ToStringAndClear();
+ }
+
+ public static UserTagKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static UserTagKey Parse(ReadOnlySpan primaryKey)
+ {
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]);
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result)
+ {
+ result = null;
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result);
+ }
+
+ public static UserTagKey Parse(string partitionKey, string sortKey)
+ {
+ ArgumentNullException.ThrowIfNull(partitionKey);
+ ArgumentNullException.ThrowIfNull(sortKey);
+
+ return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey);
+ }
+
+ public static UserTagKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey)
+ {
+ if (partitionKey.Length != 36)
+ throw new FormatException("Unrecognized format.");
+
+ if (sortKey.Length < 5)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (partitionKey.Length != 36 || !Guid.TryParseExact(partitionKey, "d", out var userId))
+ throw new FormatException("Unrecognized format.");
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal))
+ throw new FormatException("Unrecognized format.");
+
+ Span tagsRanges = stackalloc Range[128];
+ int tagsCount = sortKey[sortKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (tagsCount < 1)
+ throw new FormatException("Unrecognized format.");
+
+ var tags = new global::System.Collections.Generic.List();
+ for (int ri = 0; ri < tagsCount; ri++)
+ {
+ if (sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+ tags.Add(sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].ToString());
+ }
+
+ if (tags.Count == 0)
+ throw new FormatException("Unrecognized format.");
+
+ return new UserTagKey() { UserId = userId, Tags = tags };
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result)
+ {
+ if (partitionKey is null || sortKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result)
+ {
+ result = null;
+
+ if (partitionKey.Length != 36)
+ return false;
+
+ if (sortKey.Length < 5)
+ return false;
+
+ const int expectedSortKeyParts = 2;
+ Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1];
+ if (sortKey.Split(sortKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts)
+ return false;
+
+ if (partitionKey.Length != 36 || !Guid.TryParseExact(partitionKey, "d", out var userId))
+ return false;
+
+ if (!sortKey[sortKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal))
+ return false;
+
+ Span tagsRanges = stackalloc Range[128];
+ int tagsCount = sortKey[sortKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (tagsCount < 1)
+ return false;
+
+ var tags = new global::System.Collections.Generic.List();
+ for (int ri = 0; ri < tagsCount; ri++)
+ {
+ if (sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].Length == 0)
+ return false;
+ tags.Add(sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].ToString());
+ }
+
+ if (tags.Count == 0)
+ return false;
+
+ result = new UserTagKey() { UserId = userId, Tags = tags };
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static UserTagKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey result) => TryParse(s, out result);
+
+ ///
+ static UserTagKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey result) => TryParse(s, out result);
+ }
+}
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt
new file mode 100644
index 0000000..c4809a6
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt
@@ -0,0 +1,80 @@
+[
+ {
+ TargetType: {
+ Type: {
+ Name: TagKey,
+ FullyQualifiedName: global::UnitTests.TagKey
+ },
+ Namespace: UnitTests,
+ TypeDeclarations: [
+ public partial record TagKey
+ ],
+ Properties: [
+ {
+ Type: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false,
+ CollectionType: List
+ }
+ ],
+ PropertyInitializers: [
+ {
+ PropertyType: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ MatchesConstructorParameter: false
+ }
+ ],
+ TypeName: TagKey
+ },
+ Key: {
+ Parts: [
+ {
+ Value: TAG,
+ LengthRequired: 3,
+ ExactLengthRequirement: true
+ },
+ {
+ Value: _,
+ LengthRequired: 1,
+ ExactLengthRequirement: true
+ },
+ {
+ Property: {
+ Type: {
+ Name: List,
+ FullyQualifiedName: global::System.Collections.Generic.List
+ },
+ Name: Tags,
+ CamelCaseName: tags,
+ IsRequired: false,
+ HasGetter: true,
+ HasSetter: true,
+ IsInitOnlySetter: false,
+ CollectionType: List
+ },
+ Separator: #,
+ InnerParseType: String,
+ InnerFormatType: String,
+ InnerType: {
+ Name: String,
+ FullyQualifiedName: string
+ },
+ LengthRequired: 1,
+ ExactLengthRequirement: false
+ }
+ ],
+ InvariantFormatting: true
+ }
+ }
+]
\ No newline at end of file
diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs
new file mode 100644
index 0000000..8c2eea7
--- /dev/null
+++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs
@@ -0,0 +1,191 @@
+//HintName: UnitTests.TagKey.g.cs
+//
+
+#nullable enable annotations
+#nullable disable warnings
+
+// Suppress warnings about [Obsolete] member usage in generated code.
+#pragma warning disable CS0612, CS0618
+
+using System;
+using CompositeKey;
+
+namespace UnitTests
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")]
+ public partial record TagKey : IPrimaryKey
+ {
+ public override string ToString()
+ {
+ if (Tags.Count == 0)
+ throw new FormatException("Collection must contain at least one item.");
+
+ var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(4, 0, global::System.Globalization.CultureInfo.InvariantCulture);
+ handler.AppendLiteral("TAG");
+ handler.AppendLiteral("_");
+ for (int i = 0; i < Tags.Count; i++)
+ {
+ if (i > 0)
+ handler.AppendLiteral("#");
+
+ handler.AppendFormatted(Tags[i]);
+ }
+
+ return handler.ToStringAndClear();
+ }
+
+ public string ToPartitionKeyString()
+ {
+ if (Tags.Count == 0)
+ throw new FormatException("Collection must contain at least one item.");
+
+ var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(4, 0, global::System.Globalization.CultureInfo.InvariantCulture);
+ handler.AppendLiteral("TAG");
+ handler.AppendLiteral("_");
+ for (int i = 0; i < Tags.Count; i++)
+ {
+ if (i > 0)
+ handler.AppendLiteral("#");
+
+ handler.AppendFormatted(Tags[i]);
+ }
+
+ return handler.ToStringAndClear();
+ }
+
+ public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)
+ {
+ switch (throughPartIndex, includeTrailingDelimiter)
+ {
+ case (0, false): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG");
+ case (0, true): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_");
+ }
+
+ int fixedPartCount = 1;
+ int repeatIndex = throughPartIndex - fixedPartCount;
+ int repeatCount = Math.Min(repeatIndex + 1, Tags.Count);
+ if (repeatCount <= 0)
+ throw new InvalidOperationException("Invalid throughPartIndex for repeating section.");
+
+ var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(0, 0, global::System.Globalization.CultureInfo.InvariantCulture);
+ handler.AppendFormatted(string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_"));
+
+ for (int i = 0; i < repeatCount; i++)
+ {
+ if (i > 0)
+ {
+ handler.AppendLiteral("#");
+ }
+
+ handler.AppendFormatted(Tags[i]);
+ }
+
+ if (includeTrailingDelimiter)
+ {
+ handler.AppendLiteral("#");
+ }
+
+ return handler.ToStringAndClear();
+ }
+
+ public static TagKey Parse(string primaryKey)
+ {
+ ArgumentNullException.ThrowIfNull(primaryKey);
+
+ return Parse((ReadOnlySpan)primaryKey);
+ }
+
+ public static TagKey Parse(ReadOnlySpan primaryKey)
+ {
+ if (primaryKey.Length < 5)
+ throw new FormatException("Unrecognized format.");
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ throw new FormatException("Unrecognized format.");
+
+ if (!primaryKey[primaryKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal))
+ throw new FormatException("Unrecognized format.");
+
+ Span tagsRanges = stackalloc Range[128];
+ int tagsCount = primaryKey[primaryKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (tagsCount < 1)
+ throw new FormatException("Unrecognized format.");
+
+ var tags = new global::System.Collections.Generic.List();
+ for (int ri = 0; ri < tagsCount; ri++)
+ {
+ if (primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].Length == 0)
+ throw new FormatException("Unrecognized format.");
+ tags.Add(primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].ToString());
+ }
+
+ if (tags.Count == 0)
+ throw new FormatException("Unrecognized format.");
+
+ return new TagKey() { Tags = tags };
+ }
+
+ public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey? result)
+ {
+ if (primaryKey is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse((ReadOnlySpan)primaryKey, out result);
+ }
+
+ public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey? result)
+ {
+ result = null;
+
+ if (primaryKey.Length < 5)
+ return false;
+
+ const int expectedPrimaryKeyParts = 2;
+ Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1];
+ if (primaryKey.Split(primaryKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts)
+ return false;
+
+ if (!primaryKey[primaryKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal))
+ return false;
+
+ Span tagsRanges = stackalloc Range[128];
+ int tagsCount = primaryKey[primaryKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ if (tagsCount < 1)
+ return false;
+
+ var tags = new global::System.Collections.Generic.List();
+ for (int ri = 0; ri < tagsCount; ri++)
+ {
+ if (primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].Length == 0)
+ return false;
+ tags.Add(primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].ToString());
+ }
+
+ if (tags.Count == 0)
+ return false;
+
+ result = new TagKey() { Tags = tags };
+ return true;
+ }
+
+ ///
+ string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString();
+
+ ///
+ static TagKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s);
+
+ ///
+ static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey result) => TryParse(s, out result);
+
+ ///