使用 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

使用 C# 9 的records作为强类型ID - 路由和查询参数的更多相关文章
- angular6 路由拼接查询参数如 ?id=1 并获取url参数
angular6 路由拼接查询参数如 ?id=1 并获取url参数 路由拼接参数: <div class="category-border" [routerLink]=&qu ...
- 使用 C# 9 的records作为强类型ID - 初次使用
强类型ID 实体通常是整数,GUID或者string类型,因为数据库直接支持这些类型,但是,如果实体的ID的类型是一样的,比如都是整数的ID,这有可能会出现ID值传错的问题,看下边的示例. publi ...
- 使用 C# 9 的records作为强类型ID - JSON序列化
在本系列的上一篇文章中,我们注意到强类型ID的实体,序列化为 JSON 的时候报错了,就像这样: { "id": { "value": 1 }, "n ...
- 当页面是动态时 如果后台存储id可以通过查询后台方式获取对象;当后台没有存储时候 只有通过前端标记了 例如标记数量为10 我们根据传递过来的10循环取值
当页面是动态时 如果后台存储id可以通过查询后台方式获取对象;当后台没有存储时候 只有通过前端标记了 例如标记数量为10 我们根据传递过来的10循环取值
- 数据库分库分表和带来的唯一ID、分页查询问题的解决
需求缘起(用一个公司的发展作为背景) 1.还是个小公司的时候,注册用户就20w,每天活跃用户1w,每天最大单表数据量就1000,然后高峰期每秒并发请求最多就10,此时一个16核32G的服务器,每秒请求 ...
- 错误:当你使用id作为sharepoint的自定义页面的查询参数时,总会提示项目不存在!
No item exists at http://SERVER/SITE/mypage.aspx?ID=1. It may have been deleted or renamed by anothe ...
- jquery mobile跳转到指定id时怎样传递参数
在jquery mobile 中,每一个页面都是一个page,当我们需要从一个页面跳转到另一个页面时,可以在href中指定id,可是该怎么把一个page中的参数传递到另外一个page中,几经琢磨,发现 ...
- 查询最小未使用ID的SQL查询
--每个都加一,以此来找出最小的未用ID SELECT Min(T1.ID)+1 FROM dbo.TestTable T1 -- 不用查询已经存在的ID WHERE (T1.ID+1) NOT IN ...
- MyBatis-执行插入语句的时候返回主键ID到传入的参数对象中
<!-- 保存项目信息 --> <insert id="saveItem" parameterType="pd" useGeneratedKe ...
随机推荐
- sublime text3 将tab转换为2个或4个空格,并显示空格
有很多软件并不能解析tab,而往往有的程序员喜欢使用tab键进行对齐,sublime text可以很好的解决这个问题. 首先打开sublime text,点击preferences->setti ...
- Spring Boot 启动事件和监听器,太强大了!
大家都知道,在 Spring 框架中事件和监听无处不在,打通了 Spring 框架的任督二脉,事件和监听也是 Spring 框架必学的核心知识之一. 一般来说,我们很少会使用到应用程序事件,但我们也不 ...
- Linux下MySQL数据库的备份与恢复
Linux下MySQL数据库的备份与恢复 作者:Grey 原文地址: Github 语雀 博客园 基于版本 MySQL5.7 Deepin Linux 15.11 xtrabackup-2.4.18 ...
- 记录一次mac访问Windows共享目录失败
一,起因 起因,有人联系我说他们的mac电脑连接不上Windows的共享目录,Windows的电脑连接正常,没有报错,连接框抖两下就没了 二,排查问题 1,我自己想mstsc登陆服务器看看,结果服务器 ...
- 服务器标配 SSH 协议,你了解多少?
年初,新冠肺炎疫情的出现,全国数千万名员工在家远程办公,使用个人设备通过家庭网络访问公司资料.因此,IT 安全团队面临了众多新挑战:如何实施更加安全的身份验证方案,以确保只有授权人员和设备才能访问公司 ...
- Elasticsearch.Net
今天使用Elasticsearch作开发,很简单的查询,就出现Elasticsearch.Net.UnexpectedElasticsearchClientException异常,看样子像是序列化的异 ...
- Python进阶——为什么GIL让多线程变得如此鸡肋?
本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 做 Python 开发时,想必你肯定听过 GIL,它经常被 Python 程序员吐槽,说 Pytho ...
- python并发编程——多线程
编程的乐趣在于让程序越来越快,这里将给大家介绍一个种加快程序运行的的编程方式--多线程 1 著名的全局解释锁(GIL) 说起python并发编程,就不得不说著名的全局解释锁(GIL)了.有兴趣的同 ...
- Autofac官方文档翻译--一、注册组件--1注册概念
官方文档:http://docs.autofac.org/en/latest/register/registration.html 一.注册概念 使用Autofac 注册组件,通过创建一个Contai ...
- ucore操作系统学习(七) ucore lab7同步互斥
1. ucore lab7介绍 ucore在前面的实验中实现了进程/线程机制,并在lab6中实现了抢占式的线程调度机制.基于中断的抢占式线程调度机制使得线程在执行的过程中随时可能被操作系统打断,被阻塞 ...