58

Question: is there a better way to do that?

VB.Net

Function GuidToBase64(ByVal guid As Guid) As String
    Return Convert.ToBase64String(guid.ToByteArray).Replace("/", "-").Replace("+", "_").Replace("=", "")
End Function

Function Base64ToGuid(ByVal base64 As String) As Guid
    Dim guid As Guid
    base64 = base64.Replace("-", "/").Replace("_", "+") & "=="

    Try
        guid = New Guid(Convert.FromBase64String(base64))
    Catch ex As Exception
        Throw New Exception("Bad Base64 conversion to GUID", ex)
    End Try

    Return guid
End Function

C#

public string GuidToBase64(Guid guid)
{
    return Convert.ToBase64String(guid.ToByteArray()).Replace("/", "-").Replace("+", "_").Replace("=", "");
}

public Guid Base64ToGuid(string base64)
{
   Guid guid = default(Guid);
   base64 = base64.Replace("-", "/").Replace("_", "+") + "==";

   try {
       guid = new Guid(Convert.FromBase64String(base64));
   }
   catch (Exception ex) {
       throw new Exception("Bad Base64 conversion to GUID", ex);
   }

   return guid;
}
neminem
  • 2,658
  • 5
  • 27
  • 36
Fredou
  • 19,848
  • 10
  • 58
  • 113
  • Any special reason of removing standard special characters of Base64 encoding? – Hemant Jun 23 '09 at 12:56
  • Is there a particular reason you need to encode it? None of the characters in a GUID need encoding for URLs or attributes. – blowdart Jun 23 '09 at 12:56
  • @Hemant, because for URL, + and / and = doesn't work well in a GET, @blowdart, to make the url smaller – Fredou Jun 23 '09 at 12:59
  • @blowdart: I guess @Fredou wants resulting string as short as possible thats why he is encoding it. – Hemant Jun 23 '09 at 13:00
  • Base64 makes a string 33% BIGGER, not smaller –  Jun 23 '09 at 13:04
  • 3
    @Charlie: Base64 encoded string is smaller *than* hex encoding which is default formatting when using .ToString (). Offcourse no-one would like to transmit raw (non printable bytes) directly. – Hemant Jun 23 '09 at 13:06
  • 3
    @Charlie how "37945704-cf86-4b2e-a4b5-0db0204902c8" is bigger than "BFeUN4bPLkuktQ2wIEkCyA" – Fredou Jun 23 '09 at 13:07
  • 1
    @usr, can you give me 1 guid that would have "=" when converted into base64? – Fredou Aug 26 '12 at 14:39
  • 2
    I would consider not doing the .replace for URL encoding, or provide a seperate method for that. This would allow a separation of concerns where users of the API could choose whether they wanted real base64 encoding or the URL friendly base64 encoding, depending on what they are wanting to accomplish. I understand your goal was for URL use, but everything except the URL encoding step could potentially be used for those who want a shorter base64 encoding but aren't using it in a URL. – AaronLS Apr 01 '14 at 22:16
  • Run "aaaaaaaaaaaaaaaaaaaaaa" through Base64ToGuid(), then take the Guid returned and run it through GuidToBase64() and you get "aaaaaaaaaaaaaaaaaaaaaQ". Lots of other examples like this. – Rick Mar 28 '15 at 06:08
  • 1
    @Rick, sorry for the late reply. your right that the encoding to base64 give a different result but both give the same Guid. – Fredou Dec 21 '15 at 19:38
  • 4
    I would recommend changing the replacements, i.e. instead of `.Replace("+", "_")` and vice versa and `Replace("/", "-")` and vice versa, use `.Replace("+", "-")` and vice versa and `Replace("/", "_")` and vice versa. This would make the encoding compliant with RFC 4648 `base64url` (see https://tools.ietf.org/html/rfc4648#section-5) – Kasper van den Berg Sep 26 '16 at 08:14

6 Answers6

33

You might want to check out this site: http://prettycode.org/2009/11/12/short-guid/

It looks very close to what you're doing.

public class ShortGuid
{
    private readonly Guid guid;
    private readonly string value;

    /// <summary>Create a 22-character case-sensitive short GUID.</summary>
    public ShortGuid(Guid guid)
    {
        if (guid == null)
        {
            throw new ArgumentNullException("guid");
        }

        this.guid = guid;
        this.value = Convert.ToBase64String(guid.ToByteArray())
            .Substring(0, 22)
            .Replace("/", "_")
            .Replace("+", "-");
    }

    /// <summary>Get the short GUID as a string.</summary>
    public override string ToString()
    {
        return this.value;
    }

    /// <summary>Get the Guid object from which the short GUID was created.</summary>
    public Guid ToGuid()
    {
        return this.guid;
    }

    /// <summary>Get a short GUID as a Guid object.</summary>
    /// <exception cref="System.ArgumentNullException"></exception>
    /// <exception cref="System.FormatException"></exception>
    public static ShortGuid Parse(string shortGuid)
    {
        if (shortGuid == null)
        {
            throw new ArgumentNullException("shortGuid");
        }
        else if (shortGuid.Length != 22)
        {
            throw new FormatException("Input string was not in a correct format.");
        }

        return new ShortGuid(new Guid(Convert.FromBase64String
            (shortGuid.Replace("_", "/").Replace("-", "+") + "==")));
    }

    public static implicit operator String(ShortGuid guid)
    {
        return guid.ToString();
    }

    public static implicit operator Guid(ShortGuid shortGuid)
    {
        return shortGuid.guid;
    }
}
Todd
  • 12,995
  • 3
  • 30
  • 25
JamesBrownIsDead
  • 541
  • 2
  • 5
  • 9
  • I understand, that this answer is quite an old, but does your code actually work? String result produced by Guid.ToByteArray() + byte-to-hex conversion would be different to calling Guid.ToString(). As per .Net documentation [link](https://learn.microsoft.com/en-us/dotnet/api/system.guid.tobytearray): _the order of bytes in the returned byte array is different from the string representation of a Guid value_ – Mykola Klymyuk Jul 13 '20 at 18:55
22

One problem with using this technique to format a GUID for use in a URL or filename is that two distinct GUIDs can produce two values that differ only in case, e.g.:

    var b1 = GuidToBase64(new Guid("c9d045f3-e21c-46d0-971d-b92ebc2ab83c"));
    var b2 = GuidToBase64(new Guid("c9d045f3-e21c-46d0-971d-b92ebc2ab8a4"));
    Console.WriteLine(b1);  // 80XQyRzi0EaXHbkuvCq4PA
    Console.WriteLine(b2);  // 80XQyRzi0EaXHbkuvCq4pA

Since URLs are sometimes interpreted as being case-insensitive, and in Windows file paths and filenames are case-insensitive. this could lead to collisions.

Joe
  • 122,218
  • 32
  • 205
  • 338
  • 6
    This is incorrect for URLs. Only the scheme and host should be considered case-insensitive as per [RFC 3986](https://tools.ietf.org/html/rfc3986#section-6.2.2.1). Query, fragment, and even path should be considered case-sensitive. Of course, it's all up to your code/server implementation to adhere to this. – Roger Spurrell Apr 30 '19 at 18:47
  • 3
    @RogerSpurrell - good point re: URLs, but I was thinking more of application-specific handling of a URL such as `http://.../user/{id}`, where `{id}` could be a random guid-like id to avoid the OWASP Brute Force Predictable Resource Location vulnerability, and the id might be looked up in a case-insensitive database. – Joe Apr 30 '19 at 19:50
  • This should be a comment. It is incorrect and a bit far fetched w case-insensitive db (which would suggest use of base32). – Vajk Hermecz Sep 28 '22 at 07:59
  • 1
    @VajkHermecz I disagree. If you think that the answer is incorrect and/or not useful, you're perfectly free to downvote and/or comment to that effect, but it isn't NAA because it appears to be a good-faith attempt to answer the question. – EJoshuaS - Stand with Ukraine Sep 28 '22 at 14:02
20

I understand that the reason you are clipping == in the end is that because you can be certain that for GUID (of 16 bytes), encoded string will always end with ==. So 2 characters can be saved in every conversion.

Beside the point @Skurmedal already mentioned (should throw an exception in case of invalid string as input), I think the code you posted is just good enough.

Hemant
  • 19,486
  • 24
  • 91
  • 127
  • Didn't think of that first thing, a clever space saver when you think about it :) – Skurmedel Jun 23 '09 at 13:05
  • what would be best, dealing with an exception or querying the database anyway with something that doesn't exist? would it add more code in the end since I check if there is at least one row in the result? – Fredou Jun 23 '09 at 13:11
  • The point is only about *where* you want to put that check. My experience is that low level library routines should be as transparent as possible. Offcourse here you are the best judge of where the error checking code should go because *you* know your product and where this library/code stands. It was just a point for consideration. – Hemant Jun 23 '09 at 13:18
  • Well if you are dealing with an exception at least you know something has gone wrong. It might not matter now, but in the future maybe. I don't know your program good enoug :) I think querying the database for stuff that you (in theory) know doesn't exist is the least favourable solution. – Skurmedel Jun 23 '09 at 13:19
  • I think I agree with you guys, about throwing an exception, it make more sense – Fredou Jun 23 '09 at 13:33
  • You should place ´base64.Replace("-", "/")...´ inside of the ´try´-block to avoid an uncaught ´NullReferenceException+ if ´base64 == null´ – Felix Alcala Jan 05 '13 at 18:12
3

If your method cannot convert the Base64 passed to it to a GUID, shouldn't you throw an exception? The data passed to the method is clearly erronous.

Skurmedel
  • 21,515
  • 5
  • 53
  • 66
3

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.

Misha Zaslavsky
  • 8,414
  • 11
  • 70
  • 116
0

There is a method that I am using to encode as well as shorten for my URL (Guid): https://dotnetfiddle.net/iQ7nGv

public static void Main()
{
    Guid gg = Guid.NewGuid();
    string ss = Encode(gg);
    Console.WriteLine(gg);
    Console.WriteLine(ss);
    Console.WriteLine(Decode(ss));
}

public static string Encode(Guid guid)
{
    string encoded = Convert.ToBase64String(guid.ToByteArray());
    encoded = encoded.Replace("/", "_").Replace("+", "-");
    return encoded.Substring(0, 22);
}

public static Guid Decode(string value)
{
    value = value.Replace("_", "/").Replace("-", "+");
    byte[] buffer = Convert.FromBase64String(value + "==");
    return new Guid(buffer);
}
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 02 '23 at 00:49