当前位置 博文首页 > SpringLeee:使用 C# 9 的records作为强类型ID - 路由和查询参数

    SpringLeee:使用 C# 9 的records作为强类型ID - 路由和查询参数

    作者:SpringLeee 时间:2021-01-17 14:04

    上一篇文章,我介绍了使用 C# 9 的record类型作为强类型id,非常简洁

    public record ProductId(int Value);
    

    但是在强类型id真正可用之前,还有一些问题需要解决,比如,ASP.NET Core并不知道如何在路由参数或查询字符串参数中正确的处理它们,在这篇文章中,我将展示如何解决这个问题。

    路由和查询字符串参数的模型绑定

    假设我们有一个这样的实体:

    public record ProductId(int Value);
    
    public class Product
    {
        public ProductId Id { get; set; }
        public string Name { get; set; }
        public decimal UnitPrice { get; set; }
    }
    

    和这样的API接口:

    [ApiController]
    [Route("api/[controller]")]
    public class ProductController : ControllerBase
    {
        ...
    
        [HttpGet("{id}")]
        public ActionResult<Product> GetProduct(ProductId id)
        {
             return Ok(new Product { 
                    Id = id,
                    Name = "Apple",
                    UnitPrice = 0.8M  
    			 });
        }
    }
    

    现在,我们尝试用Get方式访问这个接口 /api/product/1

    {
        "type": "https://tools.ietf.org/html/rfc7231#section-6.5.13",
        "title": "Unsupported Media Type",
        "status": 415,
        "traceId": "00-3600640f4e053b43b5ccefabe7eebd5a-159f5ca18d189142-00"
    }
    

    现在问题就来了,返回了415,.NET Core 不知道怎么把URL的参数转换为ProductId,由于它不是int,是我们定义的强类型ID,并且没有关联的类型转换器。

    实现类型转换器

    这里的解决方案是为实现一个类型转换器ProductId,很简单:

    public class ProductIdConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
            sourceType == typeof(string);
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) =>
            destinationType == typeof(string);
    
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            return value switch
            {
                string s => new ProductId(int.Parse(s)),
                null => null,
                _ => throw new ArgumentException($"Cannot convert from {value} to ProductId", nameof(value))
            };
        }
    
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (destinationType == typeof(string))
            {
                return value switch
                {
                    ProductId id => id.Value.ToString(),
                    null => null,
                    _ => throw new ArgumentException($"Cannot convert {value} to string", nameof(value))
                };
            }
    
            throw new ArgumentException($"Cannot convert {value ?? "(null)"} to {destinationType}", nameof(destinationType));
        }
    }
    

    (请注意,为简洁起见,我只处理并转换string,在实际情况下,我们可能还希望支持转换int)

    我们的ProductId使用TypeConverter特性将该转换器与记录相关联:

    [TypeConverter(typeof(ProductIdConverter))]
    public record ProductId(int Value);
    

    现在,让我们尝试再次访问这个接口:

    {
        "id": {
            "value": 1
        },
        "name": "Apple",
        "unitPrice": 0.8
    }
    

    现在是返回了,但是还有点问题,id 在json中显示了一个对象,如何在json中处理,是我们下一篇文章给大家介绍的,现在还有一点是,我上面写了一个ProductId的转换器,但是如果我们的类型足够多,那也有很多工作量,所以需要一个公共的通用转换器。

    通用强类型id转换器

    首先,让我们创建一个Helper

    • 检查类型是否为强类型ID,并获取值的类型
    • 获取值得类型,创建并缓存一个委托
    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));
    
            var 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));
    
            var param = Expression.Parameter(typeof(TValue), "value");
            var body = Expression.New(ctor, param);
            var 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));
    
            if (type.BaseType is Type baseType &&
                baseType.IsGenericType &&
                baseType.GetGenericTypeDefinition() == typeof(StronglyTypedId<>))
            {
                idType = baseType.GetGenericArguments()[0];
                return true;
            }
    
            idType = null;
            return false;
        }
    }
    

    这个 Helper 帮助我们编写类型转换器,现在,我们可以编写通用转换器了。

    public class StronglyTypedIdConverter<TValue> : TypeConverter
        where TValue : notnull
    {
        private static readonly TypeConverter IdValueConverter = GetIdValueConverter();
    
        private static TypeConverter GetIdValueConverter()
        {
            var 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);
            }
    
            if (value is TValue idValue)
            {
                var factory = StronglyTypedIdHelper.GetFactory<TValue>(_type);
                return factory(idValue);
            }
    
            return base.ConvertFrom(context, culture, value);
        }
    
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            if (value is null)
                throw new ArgumentNullException(nameof(value));
    
            var stronglyTypedId = (StronglyTypedId<TValue>)value;
            TValue idValue = stronglyTypedId.Value;
            if (destinationType == typeof(string))
                return idValue.ToString()!;
            if (destinationType == typeof(TValue))
                return idValue;
            return base.ConvertTo(context, culture, value, destinationType);
        }
    }
    

    然后再创建一个非泛型的 Converter

    public class StronglyTypedIdConverter : TypeConverter
    {
        private static readonly ConcurrentDictionary<Type, TypeConverter> ActualConverters = new();
    
        private readonly TypeConverter _innerConverter;
    
        public StronglyTypedIdConverter(Type stronglyTypedIdType)
        {
            _innerConverter = ActualConverters.GetOrAdd(stronglyTypedIdType, CreateActualConverter);
        }
    
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) =>
            _innerConverter.CanConvertFrom(context, sourceType);
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) =>
            _innerConverter.CanConvertTo(context, destinationType);
        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) =>
            _innerConverter.ConvertFrom(context, culture, value);
        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) =>
            _innerConverter.ConvertTo(context, culture, value, destinationType);
    
    
        private static TypeConverter CreateActualConverter(Type stronglyTypedIdType)
        {
            if (!StronglyTypedIdHelper.IsStronglyTypedId(stronglyTypedIdType, out var idType))
                throw new InvalidOperationException($"The type '{stronglyTypedIdType}' is not a strongly typed id");
    
            var actualConverterType = typeof(StronglyTypedIdConverter<>).MakeGenericType(idType);
            return (TypeConverter)Activator.CreateInstance(actualConverterType, stronglyTypedIdType)!;
        }
    }
    

    到这里,我们可以直接删除之前的 ProductIdConvert, 现在有一个通用的可以使用,现在.NET Core 的路由匹配已经没有问题了,接下来的文章,我会介绍如何处理在JSON中出现的问题。

    [TypeConverter(typeof(StronglyTypedIdConverter))]
    public abstract record StronglyTypedId<TValue>(TValue Value)
        where TValue : notnull
    {
        public override string ToString() => Value.ToString();
    }
    

    原文作者: thomas levesque
    原文链接:https://thomaslevesque.com/2020/11/23/csharp-9-records-as-strongly-typed-ids-part-2-aspnet-core-route-and-query-parameters/

    最后

    欢迎扫码关注我们的公众号 【全球技术精选】,专注国外优秀博客的翻译和开源项目分享,也可以添加QQ群 897216102