1.1 MacAddress value type

Permissive parser (colon, hyphen, unseparated; case-insensitive; surrounding
whitespace) with canonical lowercase no-separator ToString. Implements
IParsable/ISpanParsable for ASP.NET route binding and ships a System.Text.Json
converter that normalizes on read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 13:55:48 +02:00
parent ee77e5656e
commit c74dbd2602
4 changed files with 270 additions and 11 deletions

View File

@@ -0,0 +1,156 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FrameProcessor.Domain;
/// <summary>
/// A 48-bit MAC address. Canonical form is lowercase hex without separators (e.g. <c>aabbccddeeff</c>);
/// parsing accepts colon, hyphen, or unseparated hex with arbitrary surrounding whitespace.
/// </summary>
[JsonConverter(typeof(MacAddressJsonConverter))]
public readonly record struct MacAddress : IParsable<MacAddress>, ISpanParsable<MacAddress>
{
private readonly byte _b0;
private readonly byte _b1;
private readonly byte _b2;
private readonly byte _b3;
private readonly byte _b4;
private readonly byte _b5;
public MacAddress(ReadOnlySpan<byte> bytes)
{
if (bytes.Length != 6)
{
throw new ArgumentException("MAC address must be exactly 6 bytes.", nameof(bytes));
}
_b0 = bytes[0];
_b1 = bytes[1];
_b2 = bytes[2];
_b3 = bytes[3];
_b4 = bytes[4];
_b5 = bytes[5];
}
public override string ToString() => string.Create(12, this, static (span, mac) =>
{
WriteHex(span[..2], mac._b0);
WriteHex(span.Slice(2, 2), mac._b1);
WriteHex(span.Slice(4, 2), mac._b2);
WriteHex(span.Slice(6, 2), mac._b3);
WriteHex(span.Slice(8, 2), mac._b4);
WriteHex(span.Slice(10, 2), mac._b5);
});
public static bool TryParse(string? s, out MacAddress result)
{
if (s is null)
{
result = default;
return false;
}
return TryParse(s.AsSpan(), out result);
}
public static bool TryParse(ReadOnlySpan<char> s, out MacAddress result)
{
result = default;
s = s.Trim();
if (s.IsEmpty)
{
return false;
}
Span<char> hex = stackalloc char[12];
var written = 0;
foreach (var c in s)
{
if (c is ':' or '-')
{
continue;
}
if (written == 12)
{
return false;
}
if (!IsHexDigit(c))
{
return false;
}
hex[written++] = c;
}
if (written != 12)
{
return false;
}
Span<byte> bytes = stackalloc byte[6];
for (var i = 0; i < 6; i++)
{
if (!byte.TryParse(hex.Slice(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var b))
{
return false;
}
bytes[i] = b;
}
result = new MacAddress(bytes);
return true;
}
public static MacAddress Parse(string s)
{
ArgumentNullException.ThrowIfNull(s);
return TryParse(s, out var result)
? result
: throw new FormatException($"'{s}' is not a valid MAC address.");
}
public static MacAddress Parse(string s, IFormatProvider? provider) => Parse(s);
public static bool TryParse(string? s, IFormatProvider? provider, out MacAddress result)
=> TryParse(s, out result);
public static MacAddress Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
=> TryParse(s, out var result)
? result
: throw new FormatException($"'{s}' is not a valid MAC address.");
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out MacAddress result)
=> TryParse(s, out result);
private static bool IsHexDigit(char c)
=> (uint)(c - '0') <= 9 || (uint)(c - 'a') <= 5 || (uint)(c - 'A') <= 5;
private static void WriteHex(Span<char> dest, byte value)
{
dest[0] = HexChar(value >> 4);
dest[1] = HexChar(value & 0xF);
}
private static char HexChar(int nibble) => (char)(nibble < 10 ? '0' + nibble : 'a' + (nibble - 10));
}
internal sealed class MacAddressJsonConverter : JsonConverter<MacAddress>
{
public override MacAddress Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var s = reader.GetString();
if (!MacAddress.TryParse(s, out var mac))
{
throw new JsonException($"'{s}' is not a valid MAC address.");
}
return mac;
}
public override void Write(Utf8JsonWriter writer, MacAddress value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString());
}