Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] 馃悶 ToQueryString does not work properly when source type has TypeConverter #5

Closed
candoumbe opened this issue May 1, 2021 · 1 comment
Assignees
Labels
bug Something isn't working

Comments

@candoumbe
Copy link
Owner

candoumbe commented May 1, 2021

Usage Information

Package version : 0.5.1

Relevant Code / Invocations

public static class StronglyTypedIdHelper
    {
        private static readonly ConcurrentDictionary<Type, Delegate> StronglyTypedIdFactories = new();

        public static Func<TValue, object> GetFactory<TValue>(Type stronglyTypedIdType)
            where TValue : notnull
        {
            return (Func<TValue, object>)StronglyTypedIdFactories.GetOrAdd(
                stronglyTypedIdType,
                CreateFactory<TValue>);
        }

        private static Func<TValue, object> CreateFactory<TValue>(Type stronglyTypedIdType)
            where TValue : notnull
        {
            if (!IsStronglyTypedId(stronglyTypedIdType))
            {
                throw new ArgumentException($"Type '{stronglyTypedIdType}' is not a strongly-typed id type", nameof(stronglyTypedIdType));
            }

            System.Reflection.ConstructorInfo ctor = stronglyTypedIdType.GetConstructor(new[] { typeof(TValue) });
            if (ctor is null)
            {
                throw new ArgumentException($"Type '{stronglyTypedIdType}' doesn't have a constructor with one parameter of type '{typeof(TValue)}'", nameof(stronglyTypedIdType));
            }

            ParameterExpression param = Expression.Parameter(typeof(TValue), "value");
            NewExpression body = Expression.New(ctor, param);
            Expression<Func<TValue, object>> lambda = Expression.Lambda<Func<TValue, object>>(body, param);

            return lambda.Compile();
        }

        public static bool IsStronglyTypedId(Type type) => IsStronglyTypedId(type, out _);

        public static bool IsStronglyTypedId(Type type, [NotNullWhen(true)] out Type idType)
        {
            if (type is null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            bool isStronglyType = false;
            if (type.BaseType is Type baseType &&
                baseType.IsGenericType &&
                baseType.GetGenericTypeDefinition() == typeof(StronglyTypedId<>))
            {
                idType = baseType.GetGenericArguments()[0];
                isStronglyType = true;
            }
            else if (type.BaseType == typeof(StronglyTypedGuidId))
            {
                idType = typeof(Guid);
                isStronglyType = true;
            }
            else
            {
                idType = null;
            }

            return isStronglyType;
        }
    }

public class StronglyTypedIdConverter<TValue> : TypeConverter where TValue : notnull
    {
        private static readonly TypeConverter IdValueConverter = GetIdValueConverter();

        private static TypeConverter GetIdValueConverter()
        {
            TypeConverter converter = TypeDescriptor.GetConverter(typeof(TValue));
            if (!converter.CanConvertFrom(typeof(string)))
            {
                throw new InvalidOperationException(
                    $"Type '{typeof(TValue)}' doesn't have a converter that can convert from string");
            }

            return converter;
        }

        private readonly Type _type;
        public StronglyTypedIdConverter(Type type)
        {
            _type = type;
        }

        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string)
                || sourceType == typeof(TValue)
                || base.CanConvertFrom(context, sourceType);
        }

        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return destinationType == typeof(string)
                || destinationType == typeof(TValue)
                || base.CanConvertTo(context, destinationType);
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is string s)
            {
                value = IdValueConverter.ConvertFrom(s);
            }

            object result;

            if (value is TValue idValue)
            {
                Func<TValue, object> factory = StronglyTypedIdHelper.GetFactory<TValue>(_type);
                result = factory(idValue);
            }
            else
            {
                result = base.ConvertFrom(context, culture, value);
            }

            return result;
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (value is null)
            {
                throw new ArgumentNullException(nameof(value));
            }

            StronglyTypedId<TValue> stronglyTypedId = (StronglyTypedId<TValue>)value;
            TValue idValue = stronglyTypedId.Value;

            object result;

            if (destinationType == typeof(string))
            {
                result = idValue.ToString()!;
            }
            else if (destinationType == typeof(TValue))
            {
                result = idValue;
            }
            else
            {
                result = base.ConvertTo(context, culture, value, destinationType);
            }

            return result;
        }
    }



[TypeConverter(typeof(StronglyTypedIdConverter))]
public abstract record StronglyTypedId<TValue>
        where TValue : notnull
    {
        public TValue Value { get; }

        protected StronglyTypedId(TValue value) => Value = value;

        public override string ToString() => Value.ToString();
    }

public record Identifier : StronglyTypedId<Guid>(Value)
{
    public static Identifier New() => new(Guid.NewGuid());
}

and a RouteValueDictionary instance as follow

RouteValueDictionary routeValues = new() {
    ["simpleId"] = "abc",
    ["strongId"] = Identifier.New()
};

routesValue.ToQueryString() // "simpleId=abc"

Expected Behavior

Given that there is a TypeConverterthat can convert to a primitive type, the consumer of the library would expect the output to be simpleId=abc&strongId=<value of the guid>

Stacktrace / Exception

@candoumbe candoumbe added the bug Something isn't working label May 1, 2021
@candoumbe candoumbe self-assigned this May 1, 2021
@candoumbe
Copy link
Owner Author

candoumbe commented May 1, 2021

One way to circumvent the issue is to use the ToQueryString overload that take a Func<(string, object), object>to unwrap the underlying value.

routesValue.ToQueryString((string key, object value) => (value as StronglyTypedId<Guid>)?.Value, ?? value)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant