Skip to content

Instantly share code, notes, and snippets.

@adnanalbeda
Last active September 12, 2024 19:47
Show Gist options
  • Save adnanalbeda/a1e20645f478826cec433a6350e2e78c to your computer and use it in GitHub Desktop.
Save adnanalbeda/a1e20645f478826cec433a6350e2e78c to your computer and use it in GitHub Desktop.
DotNetUuid

Uuid

A wrapper against Guid.

1. Purpose

I made this gist to solve the issue of serializing Guid to string for web apis.

It makes them url friendly with the usage of Base64 and short (22 characters).

It also solve the problem of desrializing them back to Guid from the 22 characters string.

All while mentaining storing them as plain Guid in Database.

2. Usage

Download this gist then copy files to your project.

Remove, add or edit whatever you want.

Use in you code:

public class Book
{
    public Uuid Id { get; set; }
    public required string Name { get; set; }

    // Maybe this attribute is not necessary. I need to test it.
    [JsonConverter(typeof(UuidConverters.NullableJsonConverter))]
    public Uuid? PublisherId { get; set; }
}

2.1. Files

  • Uuid.cs: main file.
  • UuidStringUtils.cs provide string utils. See more in Utils section.
  • IEquatable.cs provides necessary implementation of IEquatable interfaces.
  • TypeConverter.cs provides implementation of TypeConverter.
  • JsonConverter.cs provides implementation of JsonConverter.
  • EfConverter.cs provides implementation of ValueConverter.
  • EfConverterExtensions.cs provides extensions for DbContext to support Uuid.

3. Utils

Uuid provides:

  • New to create new value.
  • ValueOrNew checks if value is valid for use. Returns new one if not.
  • ValueOrDefault checks if value is valid for use. Returns default empty if not.
  • IsEmpty to check if value is null, default or empty guid.

String Utils:

  • ToShortId to convert Uuid to base64 22-character string.
  • FromShortId to convert string to Uuid if possible.
  • StringIsNotValidShortId to check whether it's possible to convert string back to Uuid or not.

4. Converters

Converters are provided by System.UuidConverters and Microsoft.EntityFramework.UuidConverters classes.

Note

All converters are using ToString() when converting to string value so implementation is unified accross them. If you feel you need to change anything, you can do it there.

4.1. System.UuidConverters

This class provides implementation for:

  • TypeConverter: applied by [TypeConverter] annotation. No need for extra setup.
  • JsonConverter: also applied by [JsonConverter] annotation. Might need to apply nullable converter.

4.2. Microsoft.EntityFramework.UuidConverters

This class provides an implementation of ValueConverter for EF Core.

4.2.1. Setup

Usage with DbContext:

public class DataContext : DbContext
{
    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        base.ConfigureConventions(configurationBuilder);

        // ... configs

        configurationBuilder
            .Properties<Uuid>()
            .HaveConversion<UuidConverters.ToGuidValueConverter>();
        configurationBuilder
            .Properties<Uuid?>()
            .HaveConversion<UuidConverters.ToNullableGuidValueConverter>();

        // or to store as string

        // configurationBuilder
        //     .Properties<Uuid>()
        //     .HaveConversion<UuidConverters.ToStringValueConverter>();
        // configurationBuilder
        //     .Properties<Uuid?>()
        //     .HaveConversion<UuidConverters.ToNullableStringValueConverter>();

        // ... configs
    }
}
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Microsoft.EntityFrameworkCore;
public static partial class UuidConverterExtensions
{
private static Action<PropertiesConfigurationBuilder<Uuid>> defaultValueDelegate = (_) => { };
private static Action<PropertiesConfigurationBuilder<Uuid?>> defaultNullableValueDelegate = (
_
) => { };
/// <summary>
///Calls both <see cref="MapUuidValueToGuid"/> and <see cref="MapUuidNullableToNullableGuid"/>.
/// </summary>
/// <param name="builder"></param>
/// <param name="extendValueProperty"></param>
/// <param name="extendNullableValueProperty"></param>
/// <returns></returns>
public static ModelConfigurationBuilder MapUuidToGuid(
this ModelConfigurationBuilder builder,
Action<PropertiesConfigurationBuilder<Uuid>>? extendValueProperty = null,
Action<PropertiesConfigurationBuilder<Uuid?>>? extendNullableValueProperty = null
) =>
builder
.MapUuidValueToGuid(extendValueProperty)
.MapUuidNullableToNullableGuid(extendNullableValueProperty);
public static ModelConfigurationBuilder MapUuidValueToGuid(
this ModelConfigurationBuilder builder,
Action<PropertiesConfigurationBuilder<Uuid>>? extendValueProperty = null
)
{
extendValueProperty ??= defaultValueDelegate;
extendValueProperty(
builder.Properties<Uuid>().HaveConversion<UuidConverters.ToGuidValueConverter>()
);
return builder;
}
public static ModelConfigurationBuilder MapUuidNullableToNullableGuid(
this ModelConfigurationBuilder builder,
Action<PropertiesConfigurationBuilder<Uuid?>>? extendNullableValueProperty = null
)
{
extendNullableValueProperty ??= defaultNullableValueDelegate;
extendNullableValueProperty(
builder
.Properties<Uuid?>()
.HaveConversion<UuidConverters.ToNullableGuidValueConverter>()
);
return builder;
}
/// <summary>
/// Calls both: <see cref="MapUuidValueToString"/> and <see cref="MapUuidNullableToString"/>
/// </summary>
/// <param name="builder"></param>
/// <param name="extendValueProperty"></param>
/// <param name="extendNullableValueProperty"></param>
/// <returns></returns>
public static ModelConfigurationBuilder MapUuidToString(
this ModelConfigurationBuilder builder,
Action<PropertiesConfigurationBuilder<Uuid>>? extendValueProperty = null,
Action<PropertiesConfigurationBuilder<Uuid?>>? extendNullableValueProperty = null
) =>
builder
.MapUuidValueToString(extendValueProperty)
.MapUuidNullableToString(extendNullableValueProperty);
public static ModelConfigurationBuilder MapUuidValueToString(
this ModelConfigurationBuilder builder,
Action<PropertiesConfigurationBuilder<Uuid>>? extendValueProperty = null
)
{
extendValueProperty ??= defaultValueDelegate;
extendValueProperty(
builder.Properties<Uuid>().HaveConversion<UuidConverters.ToStringValueConverter>()
);
return builder;
}
public static ModelConfigurationBuilder MapUuidNullableToString(
this ModelConfigurationBuilder builder,
Action<PropertiesConfigurationBuilder<Uuid?>>? extendNullableValueProperty = null
)
{
extendNullableValueProperty ??= defaultNullableValueDelegate;
extendNullableValueProperty(
builder
.Properties<Uuid?>()
.HaveConversion<UuidConverters.ToNullableStringValueConverter>()
);
return builder;
}
}
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Microsoft.EntityFrameworkCore;
public partial class UuidConverters
{
public sealed class ToGuidValueConverter : ValueConverter<Uuid, Guid>
{
public ToGuidValueConverter()
: base(v => v.Value, v => Guid.Empty == v ? default : new(v)) { }
};
public sealed class ToNullableGuidValueConverter : ValueConverter<Uuid?, Guid?>
{
public ToNullableGuidValueConverter()
: base(
v => !v.HasValue ? null : v.Value.Value,
v => !v.HasValue ? null : new Uuid(v.Value)
) { }
}
public sealed class ToStringValueConverter : ValueConverter<Uuid, string>
{
public ToStringValueConverter()
: base(v => v.ToString(), v => Uuid.FromShortId(v)) { }
};
public sealed class ToNullableStringValueConverter : ValueConverter<Uuid?, string?>
{
public ToNullableStringValueConverter()
: base(
v =>
!v.HasValue
? null
: string.IsNullOrEmpty(v.Value.ToString())
? null
: v.Value.ToString(),
v => string.IsNullOrWhiteSpace(v) ? null : Uuid.FromShortId(v)
) { }
}
}
namespace System;
// These interfaces are necessary for EF & converters to work as expected.
public partial record struct Uuid
: IEquatable<Uuid?>,
IEquatable<Uuid>,
IEquatable<Guid>,
IEquatable<string>
{
public bool Equals(Uuid? other)
{
return other.HasValue && this.Value.Equals(other.Value.Value);
}
public bool Equals(Guid other)
{
return this.Value.Equals(other);
}
public bool Equals(string? other)
{
return other is not null && other.Length == 22 && ToShortId() == other;
}
}
using System.Text.Json;
using System.Text.Json.Serialization;
namespace System;
[JsonConverter(typeof(UuidConverters.JsonConverter))]
public partial record struct Uuid;
public partial class UuidConverters
{
public sealed class JsonConverter : JsonConverter<Uuid>
{
public override Uuid Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
if (reader.TokenType is JsonTokenType.Null)
return default;
var value = JsonSerializer.Deserialize<string>(ref reader, options);
return Uuid.FromShortId(value);
}
public override void Write(Utf8JsonWriter writer, Uuid value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.ToString(), options);
}
}
public sealed class NullableJsonConverter : JsonConverter<Uuid?>
{
public override Uuid? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
if (reader.TokenType is JsonTokenType.Null)
return null;
var value = JsonSerializer.Deserialize<string>(ref reader, options);
return Uuid.FromShortId(value);
}
public override void Write(
Utf8JsonWriter writer,
Uuid? value,
JsonSerializerOptions options
)
{
if (value is null)
writer.WriteNullValue();
else
JsonSerializer.Serialize(writer, value.Value.ToString(), options);
}
}
}
using System.Diagnostics.CodeAnalysis;
namespace System;
public partial record struct Uuid
{
public static Uuid Parse(string? s) =>
s is null
? default(Uuid)
: StringIsNotValidShortId(s)
? Guid.Parse(s)
: _GuidFromShortId(s);
public static bool TryParse(
[NotNullWhen(true)] string? s,
[MaybeNullWhen(false)] out Uuid result
)
{
result = default;
if (s is null)
{
return false;
}
if (StringIsNotValidShortId(s))
if (Guid.TryParse(s, out Guid gid))
{
result = gid;
return true;
}
else
return false;
result = _GuidFromShortId(s);
return default == result;
}
}
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace System;
[TypeConverter(typeof(UuidConverters.TypeConverter))]
public partial record struct Uuid;
public partial class UuidConverters
{
public sealed class TypeConverter : ComponentModel.TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) =>
sourceType == typeof(Guid)
|| sourceType == typeof(Guid?)
|| sourceType == typeof(string)
|| base.CanConvertFrom(context, sourceType);
public override object? ConvertFrom(
ITypeDescriptorContext? context,
CultureInfo? culture,
object value
) =>
value is Guid gid
? Guid.Empty == gid
? default
: new Uuid(gid)
: value is string uuid
? Uuid.FromShortId(uuid)
: base.ConvertFrom(context, culture, value);
public override bool CanConvertTo(
ITypeDescriptorContext? context,
[NotNullWhen(true)] Type? destinationType
) =>
destinationType == typeof(Guid)
|| destinationType == typeof(Guid?)
|| destinationType == typeof(string)
|| base.CanConvertTo(context, destinationType);
public override object? ConvertTo(
ITypeDescriptorContext? context,
CultureInfo? culture,
object? value,
Type destinationType
)
{
if (value is not Uuid uid)
{
return base.ConvertTo(context, culture, value, destinationType);
}
if (destinationType == typeof(Guid) || destinationType == typeof(Guid?))
{
return uid.Value;
}
if (destinationType == typeof(string))
{
uid.ToString();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
using System.Diagnostics.CodeAnalysis;
namespace System;
public readonly partial record struct Uuid(Guid Value)
{
public static implicit operator Uuid(Guid id) => Guid.Empty == id ? default : new(id);
public static implicit operator Guid(Uuid id) => id.Value;
/// <summary>
/// Creates a new instance of <see cref="Uuid"/> with new guid value.
/// </summary>
/// <returns>New <see cref="Uuid"/> value.</returns>
public static Uuid New() => new(Guid.NewGuid());
/// <summary>
/// Checks if <paramref name="id"/> is valid as <see cref="Uuid"/> value by testing it against <see cref="Uuid.IsEmpty(Uuid?)"/>.
/// </summary>
/// <returns><see cref="Uuid"/> <paramref name="id"/> value if valid. Otherwise, a new value.</returns>
public static Uuid ValueOrNew(Uuid? id) => IsEmpty(id) ? New() : id.Value;
/// <summary>
/// Checks if <paramref name="id"/> is valid as <see cref="Uuid"/> value by testing it against <see cref="Uuid.IsEmpty(Uuid?)"/>.
/// </summary>
/// <returns><see cref="Uuid"/> <paramref name="id"/> value if valid. Otherwise, a default value.</returns>
public static Uuid ValueOrDefault(Uuid? id) => IsEmpty(id) ? default : id.Value;
/// <summary>
/// Checks if <paramref name="value"/> is null, default or its <see cref="Guid"/> value equals <see cref="Guid.Empty"/>
/// </summary>
/// <param name="value"></param>
/// <returns>true if <paramref name="value"/> is null, default or its <see cref="Guid"/> value equals <see cref="Guid.Empty"/>. Otherwise, false.</returns>
public static bool IsEmpty([NotNullWhen(false)] Uuid? value) =>
!value.HasValue || IsEmpty(value.Value);
/// <summary>
/// Checks if <paramref name="value"/> is default or its <see cref="Guid"/> value equals <see cref="Guid.Empty"/>
/// </summary>
/// <param name="value"></param>
/// <returns>true if <paramref name="value"/> is default or its <see cref="Guid"/> value equals <see cref="Guid.Empty"/>. Otherwise, false.</returns>
public static bool IsEmpty(Uuid value) => default == value || Guid.Empty == value.Value;
}
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace System;
public partial record struct Uuid
{
public static implicit operator string(Uuid shortUuid) => shortUuid.ToString();
public override string ToString()
{
return ToShortId();
}
/// <summary>
/// Converts <see cref="Uuid.Value"/> to Base64 22-character string.
/// </summary>
/// <param name="id"></param>
/// <returns>A Base64 string value.</returns>
public string ToShortId() => GuidToShortId(this.Value);
/// <summary>
/// Converts <paramref name="id"/>'s Value to Base64 22-character string.
/// </summary>
/// <param name="id"></param>
/// <returns>A Base64 string value.</returns>
public static string ToShortId(Uuid? id)
{
if (!id.HasValue)
return string.Empty;
return GuidToShortId(id.Value.Value);
}
/// <summary>
/// Converts <paramref name="id"/> to Base64 22-character string.
/// </summary>
/// <param name="id"></param>
/// <returns>A Base64 string value.</returns>
public static string GuidToShortId(Guid id)
{
if (Guid.Empty == id)
return string.Empty;
string encoded = Convert.ToBase64String(id.ToByteArray());
encoded = encoded.Replace("/", "_").Replace("+", "-");
return encoded.Substring(0, 22);
}
/// <summary>
/// Converts <paramref name="value"/>'s Value <see cref="Uuid"/> if possible..
/// </summary>
/// <param name="value"></param>
/// <returns>A <see cref="Uuid"/> value represented by <paramref name="value"/> short value. Or a default one if <paramref name="value"/> is not valid.</returns>
public static Uuid FromShortId(string? value)
{
if (StringIsNotValidShortId(value))
return default;
var gid = _GuidFromShortId(value);
return Guid.Empty == gid ? default : new Uuid(gid);
}
public static Guid GuidFromShortId(string? id)
{
if (StringIsNotValidShortId(id))
return Guid.Empty;
return _GuidFromShortId(id);
}
/// <summary>
/// Checks if <paramref name="value"/> is possibly not valid for conversion to <see cref="Uuid"/> using <see cref="Uuid.FromShortId(string?)"/>.
/// </summary>
/// <param name="value"></param>
/// <returns>true if possibly not valid. Otherwise, false.</returns>
public static bool StringIsNotValidShortId([NotNullWhen(false)] string? value) =>
value is null || value.Length != 22 || !Regex.IsMatch(value, "^[a-zA-Z0-9_-]{22}$");
private static Guid _GuidFromShortId(string id)
{
id = id.Replace("_", "/").Replace("-", "+");
byte[] buffer = Convert.FromBase64String(id + "==");
return new Guid(buffer);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment