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