1. 程式人生 > >使用 C# 9 的records作為強型別ID - 路由和查詢引數

使用 C# 9 的records作為強型別ID - 路由和查詢引數

![](https://blog-1259586045.cos.ap-shanghai.myqcloud.com/clipboard_20210117_120835.png) 上一篇文章,我介紹了使用 C# 9 的record型別作為強型別id,非常簡潔 ```csharp public record ProductId(int Value); ``` 但是在強型別id真正可用之前,還有一些問題需要解決,比如,ASP.NET Core並不知道如何在路由引數或查詢字串引數中正確的處理它們,在這篇文章中,我將展示如何解決這個問題。 ### 路由和查詢字串引數的模型繫結 假設我們有一個這樣的實體: ```csharp public record ProductId(int Value); public class Product { public ProductId Id { get; set; } public string Name { get; set; } public decimal UnitPrice { get; set; } } ``` 和這樣的API介面: ```csharp [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`: ```csharp { "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,很簡單: ```csharp 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特性將該轉換器與記錄相關聯: ```csharp [TypeConverter(typeof(ProductIdConverter))] public record ProductId(int Value); ``` 現在,讓我們嘗試再次訪問這個介面: ```csharp { "id": { "value": 1 }, "name": "Apple", "unitPrice": 0.8 } ``` 現在是返回了,但是還有點問題,id 在json中顯示了一個物件,如何在json中處理,是我們下一篇文章給大家介紹的,現在還有一點是,我上面寫了一個ProductId的轉換器,但是如果我們的型別足夠多,那也有很多工作量,所以需要一個公共的通用轉換器。 ### 通用強型別id轉換器 首先,讓我們建立一個Helper - 檢查型別是否為強型別ID,並獲取值的型別 - 獲取值得型別,建立並快取一個委託 ```csharp 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>(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 幫助我們編寫型別轉換器,現在,我們可以編寫通用轉換器了。 ```csharp 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 ```csharp 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中出現的問題。 ```csharp [TypeConverter(typeof(StronglyTypedIdConverter))] public abstract record StronglyTypedId(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/](https://thomaslevesque.com/2020/11/23/csharp-9-records-as-strongly-typed-ids-part-2-aspnet-core-route-and-query-parameters/ "https://thomaslevesque.com/2020/11/23/csharp-9-records-as-strongly-typed-ids-part-2-aspnet-core-route-and-query-parameters/") ### 最後 歡迎掃碼關注我們的公眾號 【全球技術精選】,專注國外優秀部落格的翻譯和開源專案分享,也可以新增QQ群 897216102