In .NET Core
you can use Spans for better performance and no memory allocation.
using System.Buffers.Text;
using System.Runtime.InteropServices;
namespace Extensions;
public static class GuidExtensions
{
private const char Dash = '-';
private const char EqualsChar = '=';
private const byte ForwardSlashByte = (byte)Slash;
private const char Plus = '+';
private const byte PlusByte = (byte)Plus;
private const char Slash = '/';
private const char Underscore = '_';
private const int Base64LengthWithoutEquals = 22;
public static string EncodeBase64String(this Guid guid)
{
Span<byte> guidBytes = stackalloc byte[16];
Span<byte> encodedBytes = stackalloc byte[24];
MemoryMarshal.TryWrite(guidBytes, ref guid);
Base64.EncodeToUtf8(guidBytes, encodedBytes, out _, out _);
Span<char> chars = stackalloc char[Base64LengthWithoutEquals];
// Replace any characters which are not URL safe.
// And skip the final two bytes as these will be '==' padding we don't need.
for (int i = 0; i < Base64LengthWithoutEquals; i++)
{
chars[i] = encodedBytes[i] switch
{
ForwardSlashByte => Dash,
PlusByte => Underscore,
_ => (char)encodedBytes[i],
};
}
return new(chars);
}
public static Guid DecodeBase64String(this ReadOnlySpan<char> id)
{
Span<char> base64Chars = stackalloc char[24];
for (var i = 0; i < Base64LengthWithoutEquals; i++)
{
base64Chars[i] = id[i] switch
{
Dash => Slash,
Underscore => Plus,
_ => id[i],
};
}
base64Chars[22] = EqualsChar;
base64Chars[23] = EqualsChar;
Span<byte> idBytes = stackalloc byte[16];
Convert.TryFromBase64Chars(base64Chars, idBytes, out _);
return new(idBytes);
}
}
using AutoFixture.Xunit2;
using FluentAssertions;
using Extensions;
using Xunit;
namespace ExtensionTests;
public class GuidExtensionsTests
{
private const int Base64LengthWithoutEquals = 22;
private const string EmptyBase64 = "AAAAAAAAAAAAAAAAAAAAAA";
[Theory]
[AutoData]
public void EncodeBase64String_DecodeBase64String_Should_ReturnInitialGuid(Guid guid)
{
string actualBase64 = guid.EncodeBase64String();
actualBase64.Should().NotBe(string.Empty)
.And.HaveLength(Base64LengthWithoutEquals);
Guid actualGuid = ((ReadOnlySpan<char>)actualBase64).DecodeBase64String();
actualGuid.Should().Be(guid);
}
[Theory]
[InlineData(EmptyBase64)]
public void EncodeBase64String_Should_ReturnEmptyBase64_When_GuidIsEmpty(string expected)
{
string actualBase64 = Guid.Empty.EncodeBase64String();
actualBase64.Should().Be(expected);
}
[Theory]
[InlineData(EmptyBase64)]
public void DecodeBase64String_Should_ReturnEmptyGuid_When_StringIsEmptyBase64(string base64)
{
Guid actual = ((ReadOnlySpan<char>)base64).DecodeBase64String();
actual.Should().Be(Guid.Empty);
}
}
For more info read about using high-performance techniques to base64 encode a guid, and a very nice video explanation.
.Replace("+", "_")
and vice versa andReplace("/", "-")
and vice versa, use.Replace("+", "-")
and vice versa andReplace("/", "_")
and vice versa. This would make the encoding compliant with RFC 4648base64url
(see tools.ietf.org/html/rfc4648#section-5) – Scirrhous