graphql-platform: Uuid values with hyphens no longer supported

Bug description

I have a project where Guids are used as identifiers. The input model looks like this:

public sealed class ProjectQueryInput
{
    public Guid ProjectId { get; set; }
}

This gets translated as a Uuid! type which is fine, however, when sending an id with a GraphQL client (Insomnia in my case) as “a3a4e988-a84f-45f6-993b-ac7401222975”, we get back this error:

{
  "message": "The specified value type of field `projectId` does not match the field type.",
  "locations": [
	{
	  "line": 21,
	  "column": 18
	}
  ],
  "path": [
	"project",
	"project"
  ],
  "extensions": {
	"fieldName": "projectId",
	"fieldType": "Uuid!",
	"locationType": "Uuid!",
	"specifiedBy": "http://spec.graphql.org/June2018/#sec-Values-of-Correct-Type"
  }
}

Only after removing the hyphens, the query works.

Expected behavior Guid formatted with hyphens should also be accepted as Uuid.

Desktop

  • OS: Windows 10
  • HotChocolate Version: 11.1.0-preview.2

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 15 (8 by maintainers)

Most upvoted comments

Yes we have created a solution for the following case: Usually we get Uuids in format N (without hyphens) from the clients, but in some rare cases we get Uuids in format D (classic c# Guid with hyphens). To handle both I have registered the following class…

It first tries to parse the Uuids in the configured format and if the Uuid could not be parsed it tries the format D as alternate format. (You could hand over the alternate format also in the constructor if you want)

 public sealed class GuidType
        : ScalarType<Guid, StringValueNode>
    {
        private readonly string _format;
        private readonly string _alternateFormat = "D";

        /// <summary>
        /// Initializes a new instance of the <see cref="GuidType"/> class.
        /// </summary>
        public GuidType()
            : this('\0')
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="GuidType"/> class.
        /// </summary>
        public GuidType(char format)
            : this(ScalarNames.Uuid, format)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="GuidType"/> class.
        /// </summary>
        public GuidType(NameString name, char format = '\0')
            : this(name, null, format)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="GuidType"/> class.
        /// </summary>
        public GuidType(NameString name, string? description, char format = '\0')
            : base(name, BindingBehavior.Implicit)
        {
            Description = description;
            _format = CreateFormatString(format);
        }

        protected override bool IsInstanceOfType(StringValueNode valueSyntax)
        {
            if (Utf8Parser.TryParse(
                valueSyntax.AsSpan(), out Guid _, out int _, _format[0]))
            {
                return true;
            }
            
            return Utf8Parser.TryParse(
                valueSyntax.AsSpan(), out Guid _, out int _, _alternateFormat[0]);
        }

        protected override Guid ParseLiteral(StringValueNode valueSyntax)
        {
            if (Utf8Parser.TryParse(
                valueSyntax.AsSpan(), out Guid formatGuid, out int _, _format[0]))
            {
                return formatGuid;
            }

            if(Utf8Parser.TryParse(
                valueSyntax.AsSpan(), out Guid altFormatGuid, out int _, _alternateFormat[0]))
            {
                return altFormatGuid;
            }

            throw new SerializationException(
                ScalarCannotParseLiteral(Name, valueSyntax.GetType()),
                this);
        }

        protected override StringValueNode ParseValue(Guid runtimeValue)
        {
            return new StringValueNode(runtimeValue
                .ToString(_alternateFormat, CultureInfo.InvariantCulture));
        }

        public override IValueNode ParseResult(object? resultValue)
        {
            if (resultValue is null)
            {
                return NullValueNode.Default;
            }

            if (resultValue is string s)
            {
                return new StringValueNode(s);
            }

            if (resultValue is Guid g)
            {
                return ParseValue(g);
            }

            throw new SerializationException(
                ScalarCannotParseLiteral(Name, resultValue.GetType()),
                this);
        }

        public override bool TrySerialize(object? runtimeValue, out object? resultValue)
        {
            if (runtimeValue is null)
            {
                resultValue = null;
                return true;
            }

            if (runtimeValue is Guid uri)
            {
                resultValue = uri.ToString(_alternateFormat, CultureInfo.InvariantCulture);
                return true;
            }

            resultValue = null;
            return false;
        }

        public override bool TryDeserialize(object? resultValue, out object? runtimeValue)
        {
            if (resultValue is null)
            {
                runtimeValue = null;
                return true;
            }

            if (resultValue is string s && Guid.TryParse(s, out Guid guid))
            {
                runtimeValue = guid;
                return true;
            }

            if (resultValue is Guid)
            {
                runtimeValue = resultValue;
                return true;
            }

            runtimeValue = null;
            return false;
        }

        private static string CreateFormatString(char format)
        {
            if (format != '\0'
                && format != 'N'
                && format != 'D'
                && format != 'B'
                && format != 'P')
            {
                throw new ArgumentException(
                    "Unknown format. Guid supports the following format chars: " +
                    $"{{ `N`, `D`, `B`, `P` }}.{Environment.NewLine}" +
                    "https://docs.microsoft.com/en-us/dotnet/api/" +
                    "system.buffers.text.utf8parser.tryparse?" +
                    "view=netcore-3.1#System_Buffers_Text_Utf8Parser_" +
                    "TryParse_System_ReadOnlySpan_System_Byte__System_Guid__" +
                    "System_Int32__System_Char_",
                    nameof(format));
            }

            return format == '\0' ? "N" : format.ToString(CultureInfo.InvariantCulture);
        }

        private static string ScalarCannotParseLiteral(
            string typeName, Type literalType)
        {
            if (string.IsNullOrEmpty(typeName))
            {
                throw new ArgumentException(
                    "The typeName mustn't be null or empty.",
                    nameof(typeName));
            }

            if (literalType is null)
            {
                throw new ArgumentNullException(nameof(literalType));
            }

            return string.Format(
                CultureInfo.InvariantCulture,
                "{0} cannot parse the given literal of type `{1}",
                typeName,
                literalType.Name);
        }
    }

If you add it to the IRequestExecutorBuilder, then it overwrites the normal UuidType:


services
    .AddGraphQLServer()
    .BindRuntimeType<Guid, GuidType>()
    .
    .

I think, anything that Guid.Parse can parse should be allowed.

@PascalSenn Thanks

@nscheibe Looks like Guid properties in our C# class models are working correctly without any changes. We were using UuidType in a few ObjectTypeExtensions and after replacing those with GuidType, it’s working correctly now. Thank you.

Well, version 10 supported both.

For me, it’s a temporary problem as I had a lot of queries setup in my Insomnia client for testing purposes. The normal front-end doesn’t have this problem as all Guids it gets back from the HotChocolate server are formatted as N (without hyphens).

However, this can be a breaking change for others that use hard-coded Guids in their front-end application or fetch Guids from another place (like REST API, SignalR, …) formatted with hyphens.