使用 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 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 StronglyTypedIdFactories = new(); public static Func GetFactory(Type stronglyTypedIdType) where TValue : notnull { return (Func)StronglyTypedIdFactories.GetOrAdd( stronglyTypedIdType, CreateFactory); } private static Func CreateFactory(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>(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 : 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(_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)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 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 Value) where TValue : notnull { public override string ToString() => Value.ToString(); }
原文作者: thomas levesque
最后
欢迎扫码关注我们的公众号 【全球技术精选】,专注国外优秀博客的翻译和开源项目分享,也可以添加QQ群 897216102