程序地带

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



上一篇文章,我介绍了使用 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



版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/myshowtime/p/14288608.html

随机推荐

操作系统和CPU联系

1、CPU指令集:(主流)ARM和X86两类。CPU指令集取决于CPU的体系架构2、操作系统:LINUX和WINDOWS等。   LINUX优点...

浩澜大大 阅读(593)

大数据分析之纳税人画像-实现和优化思路

1.背景环境本文章来自最近做的项目模块的思考和总结,主要讲思路不涉及过多的基础和实现细节。需求:统计出来纳税人名称、行业、近一年业务量(办税服务厅、电子税务局、自助渠道),近一年业务量top5(办税服...

有理想的coder 阅读(464)

linux 学习笔记#03 常用目录含义

linux 学习笔记#03 常用目录含义

linux常用目录含义上一次的笔记讲到,linux的目录从根开始,相当于只有一个C盘,输入ls/可以查看根目录下的几所有目录。那么它们分别是什么含义呢...

PF___ 阅读(441)

Linux(CentOS)安装MySql

Linux(CentOS)安装MySql

安装mysqlyumrepositorywget-i-chttp://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpmyum-...

iBrake 阅读(872)

JS实现四舍五入

场景前端计算金额时,经常会出现浮点型过长的问题,所以需要一个四舍五入的方法来保证小数的位数。functionmain(){console.log(0.06*40.08)}main()得出结果:1.使用...

阿布QAQ~ 阅读(766)

Java程序打包运行(Netty框架运行为例)

一:项目文件打Jar包1、拷贝项目包目录文件夹(com文件夹)至任意文件夹(个人项目目录结构如下)2、运行cmd命令窗口找到所选的任意文件夹下,并运行命令(可生成.bat命...

CapRobin 阅读(772)

思绪混乱

寒假来了学的微电子专业以后想从事数字前端方向的工作抓紧时间把太冷。。。先学学fpga没板子就是学习下各种内容的思路仿真下fpga好像也有很多方向不能瞎整啊svuvm看了点感觉内容上像软件但是又没办法像...

weixin_49804982 阅读(504)

Mysql、sqlserver、oracle指定返回记录数

近期新接触sqlserver、oracle数据库,发现指定返回记录总数居然都和mysql不同:Mysql:selectXXXwhereXXXlimitNSqlserver:selectTOPNXXXO...

AmyZYX 阅读(674)

C语言实验06_数学

C语言实验06_数学实验06(01)判断素数的函数6.2写一个判断素数的函数,在主函数中输出1~100间的素数信息输入描述无输出描述输出1~100之间所有的素数,中间用空格...

数学小学霸 阅读(362)