使用 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#p-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> : TypeConverterwhere 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305694.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux sudo 必须属于用户ID0,sudo:/usr/bin/sudo 务必属于用户 ID 0(的用户)并且设置 setuid 位...

今天手抖本来要修改/usr/bin/下面的一个文件属性为 user:user&#xff0c;执行chown -R /usr/bin/的时候&#xff0c;再按tab之前直接敲了回车&#xff1b;结果导致执行sudo命令时出现错误提示&#xff1a;“sudo&#xff1a;/usr/bin/sudo 必须属于用户 ID 0(的用户)并且设置…

企业形象广告的几个突破要点

企业形象广告的使用不能简单的说好与不好&#xff0c;特定的企业&#xff0c;特定的行业&#xff0c;有不同的功能和效果&#xff0c;一般大众化消费品较为适宜&#xff0c;但也要注意&#xff1a; 1、 如果一个企业的产品处于一个完全成熟的品牌阶段&#xff0c;不易投入大量的…

公司高层要我转Java 我直接邮件回怼...

2020年艰难而短暂&#xff0c;而互联网的历史车轮还是轰隆隆一路向前&#xff0c;服务网格/云原生/Serverless架构&#xff0c;各种新架构大行其道。重新审视当下主流的编程语言&#xff0c;天下第一的Java已老&#xff0c;下行多年的.NET却浴火重生&#xff0c;焕发生机&#…

linux如何实现网络高级编程,嵌入式Linux网络编程之:网络高级编程-嵌入式系统-与非网...

10.3 网络高级编程在实际情况中&#xff0c;人们往往遇到多个客户端连接服务器端的情况。由于之前介绍的如connet()、recv()和send()等都是阻塞性函数&#xff0c;如果资源没有准备好&#xff0c;则调用该函数的进程将进入睡眠状态&#xff0c;这样就无法处理I/O多路复用的情况…

.NET斗鱼直播弹幕客户端(2021)

.NET斗鱼直播弹幕客户端(2021)离之前更新的两篇《.NET斗鱼直播弹幕客户端》已经有一段时间&#xff0c;近期有许多客户向我反馈刚好有这方面的需求&#xff0c;但之前的代码不能用了——但网上许多流传的Node.js、Python脚本却可以用&#xff0c;这岂能忍&#xff1f;&#xff…

McAfee推免费版SiteAdvisor安全上网工具

McAfee近期宣布推出完全免费版McAfee SiteAdvisor上网工具。McAfee SiteAdvisor是业界第一款Web安全工具&#xff0c;能够主动地提醒用户在浏览、搜索和即时通信或收发电子邮件时所遇到的危险站点&#xff0c;避免遭到网络钓鱼、间谍软件等恶意程序的***。 McAfee SiteAdvisor是…

9050 端口 linux 进程,windows和linux查看端口占用情况

一、Windows平台在windows命令行窗口下执行&#xff1a;1.查看所有的端口占用情况C:\>netstat -ano协议 本地地址 外部地址 状态 PIDTCP 127.0.0.1:1434 0.0.0.0:0 LISTENING 3236TC…

Exceptionless服务端本地化部署

背景分布式异常日志收集框架Exceptionless是开源的工具&#xff0c;根据官方给出的说明&#xff1a;Exceptionless提供两种使用方式&#xff0c;一种是官网创建账号,需要付费&#xff0c;免费版有限制&#xff1b;一种是自己搭建本地项目&#xff0c;无任何限制。准备安装包准备…

微软所谓的无人工介入的自动的机器翻译系统

近日在微软网站上查找资料,发现一个资料里有如下的声明性描述:注意&#xff1a;这篇文章是由无人工介入的自动的机器翻译系统翻译完成。这些文章是微软为不懂英语的用户提供的, 以使他们能够理解这些文章的内容。微软不保证机器翻译的正确度&#xff0c;也不对由于内容的误译或…

ABP vnext模块化架构的最佳实践的实现

在上一篇文章《手把手教你用Abp vnext构建API接口服务》中&#xff0c;我们用ABP vnext实现了WebAPI接口服务&#xff0c;但是并非ABP模块化架构的最佳实践。我本身也在学习ABP&#xff0c;我认为ABP新手应该从最佳实践开始学习&#xff0c;可以少走很多弯路&#xff0c;所以写…

WebCast学习链接

全部下载列表1. C#面向对象设计模式纵横谈系列课程 讲师&#xff1a;李建忠 上海祝成信息科技有限公司 高级培训讲师 MSDN特邀讲师2. ASP.NET AJAX深入浅出系列课程 讲师: 老赵 课程(1)&#xff1a;ASP.NET AJAX 概述 课程(2)&#xff1a;UpdatePanel的使用(…

linux删除第二次出现的字符,linux下 怎样删除文件名中包含特殊字符的文件

目录中无意间出现了 -- 这个文件[rootdev tmp]# ls-- 00 01 02 03 04 05 06 07 08 09[rootdev tmp]# lltotal 0-rw-r--r-- 1 root root 0 Oct 23 15:31 ---rw-r--r-- 1 root root 0 Oct 23 15:37 00-rw-r--r-- 1 root root 0 Oct 23 15:37 01-rw-r--r-- 1 root root 0 Oct 23 1…

全球知名跨境电商,.Net软件工程师招聘,约么?

公司&#xff1a;际客国际电子商务有限公司&#xff0c;网址&#xff1a;http://geekbuy.cn/工作地点&#xff1a;深圳市龙岗区五和大道南雅宝路1号星河WORLD B座岗位职责&#xff1a;中级开发工程师。薪资待遇&#xff1a;10K-20K&#xff0c;具体面议。 职位要求&#x…

被关起来日子的流水帐

2007年3月19日 老上号经过3个半小时的颠簸&#xff0c;在晚上7&#xff1a;30到达了哈尔滨&#xff0c;还是住在黑龙江大学旁边的学府宾馆&#xff0c;由于是在学校的边上&#xff0c;相对来说比较安全&#xff0c;宾馆的条件已经不如以前了&#xff0c;稍微显得有点陈旧&#…

krc 编辑 linux,Linux网络编程

6 berkeley - 145 -struct in_addr {unsigned long s_addr;};ina struct sockaddr_instruct in_addrina.sin_addr.s_addr 4 IP4 IP #defines6.5.21IP2h to nto Network Short Hosts H-to-n-s htons()68000n h to s l IPstolh() Short to Long Host?l htons() “Host to Networ…

iphone查看删除的短信_手机资讯:iPhone手机可以批量删除短信吗如何操作

如今使用IT数码设备的小伙伴们是越来越多了&#xff0c;那么IT数码设备当中是有很多知识的&#xff0c;这些知识很多小伙伴一般都是不知道的&#xff0c;就好比最近就有很多小伙伴们想要知道iPhone手机可以批量删除短信吗如何操作&#xff0c;那么既然现在大家对于iPhone手机可…

如何在 C# 8 中使用 模式匹配

模式匹配 是在 C# 7 中引入的一个非常????的特性&#xff0c;你可以在任何类型上使用 模式匹配&#xff0c;甚至是自定义类型&#xff0c;而且在 C# 8 中得到了增强&#xff0c;引入了大量的新模式类型&#xff0c;这篇文章就来讨论如何在 C# 8 中使用模式匹配。C# 8 中的表…

linux下找不到libc 库,Linux-覆盖libc open()库函数

我在库&中有glibc提供的相同的覆盖open().我首先在库中设置了LD_PRELOAD,因此当进程调用open()时,将调用库中定义的open.问题&#xff1a;-glibc中还有其他几个函数,一旦示例为getpt(),就会调用open(),当getpt()调用open()时,将调用glibc中定义的open(),我将如何使getpt ()…

Hadoop 中zoo_0基础如何入门HADOOP

原标题&#xff1a;0基础如何入门HADOOP学习一样东西&#xff0c;肯定先要了解这个东西是什么&#xff0c;那什么是HADOOP呢&#xff1f;我们就来看看什么是HADOOP和如何学习HADOOP及学习内容。一&#xff0c;什么是HADOOPHADOOP是apache旗下的一套开源软件平台HADOOP提供的功能…

.NET 5 程序高级调试-WinDbg

上周和大家分享了.NET 5开源工作流框架elsa&#xff0c;程序跑起来后&#xff0c;想看一下后台线程的执行情况。抓了个进程Dump后&#xff0c;使用WinDbg调试&#xff0c;加载SOS调试器扩展&#xff0c;结果无法正常使用了&#xff1a;0:000> .loadby sos clrUnable to find…