前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 C# 9 的records作为强类型ID - 路由和查询参数

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

作者头像
全球技术精选
发布2021-01-21 14:56:17
1.9K0
发布2021-01-21 14:56:17
举报
文章被收录于专栏:全球技术精选

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

代码语言:javascript
复制
public record ProductId(int Value);

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

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

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

代码语言:javascript
复制
public record ProductId(int Value);

public class Product
{
    public ProductId Id { get; set; }
    public string Name { get; set; }
    public decimal UnitPrice { get; set; }
}

和这样的API接口:

代码语言:javascript
复制
[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

代码语言:javascript
复制
{
    "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,很简单:

代码语言:javascript
复制
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特性将该转换器与记录相关联:

代码语言:javascript
复制
[TypeConverter(typeof(ProductIdConverter))]
public record ProductId(int Value);

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

代码语言:javascript
复制
{
    "id": {
        "value": 1
    },
    "name": "Apple",
    "unitPrice": 0.8
}

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

通用强类型id转换器

首先,让我们创建一个Helper

•检查类型是否为强类型ID,并获取值的类型•获取值得类型,创建并缓存一个委托

代码语言:javascript
复制
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 帮助我们编写类型转换器,现在,我们可以编写通用转换器了。

代码语言:javascript
复制
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

代码语言:javascript
复制
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中出现的问题。

代码语言:javascript
复制
[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/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半栈程序员 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 路由和查询字符串参数的模型绑定
  • 实现类型转换器
  • 通用强类型id转换器
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档