From a73afdfe1a141fdc5e1f9cd7b8c1f3c44ab9b0a7 Mon Sep 17 00:00:00 2001 From: Milo Date: Thu, 8 Jan 2026 23:25:22 +0100 Subject: [PATCH] feat: Add IParsable support for strongly-typed IDs This adds support for primary keys that implement IParsable, enabling CoreAdmin to work with strongly-typed ID patterns (DDD-style value objects). Previously, CoreAdmin only supported Guid, int, and long primary keys. Now it will also work with any type that implements IParsable, such as: - record ResightsCvrSyncId(Guid Value) : IdBase The conversion is done via a new ConvertPrimaryKey helper that: 1. Uses fast paths for Guid, int, long, string (existing behavior) 2. Checks for IParsable and calls the static Parse method 3. Falls back to TypeConverter if available This enables Edit/Delete operations in CoreAdmin for entities using strongly-typed IDs that implement IParsable. Co-Authored-By: Claude Opus 4.5 --- .../Controllers/CoreAdminDataController.cs | 83 +++++++++++++------ 1 file changed, 57 insertions(+), 26 deletions(-) diff --git a/src/DotNetEd.CoreAdmin/Controllers/CoreAdminDataController.cs b/src/DotNetEd.CoreAdmin/Controllers/CoreAdminDataController.cs index f2572f3..257054d 100644 --- a/src/DotNetEd.CoreAdmin/Controllers/CoreAdminDataController.cs +++ b/src/DotNetEd.CoreAdmin/Controllers/CoreAdminDataController.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; @@ -22,6 +23,60 @@ public CoreAdminDataController(IEnumerable dbSetEntit this.dbSetEntities = dbSetEntities; } + /// + /// Converts a string ID to the appropriate CLR type for the primary key. + /// Supports built-in types (Guid, int, long), IParsable<T> for strongly-typed IDs, + /// and TypeConverter as a fallback. + /// + private static object ConvertPrimaryKey(string id, Type clrType) + { + // Fast path for common built-in types + if (clrType == typeof(Guid)) + { + return Guid.Parse(id); + } + if (clrType == typeof(int)) + { + return int.Parse(id); + } + if (clrType == typeof(long)) + { + return long.Parse(id); + } + if (clrType == typeof(string)) + { + return id; + } + + // Check for IParsable interface (supports strongly-typed IDs) + var parsableInterface = clrType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IParsable<>)); + + if (parsableInterface != null) + { + // Call the static Parse method: T.Parse(string, IFormatProvider?) + var parseMethod = clrType.GetMethod("Parse", + BindingFlags.Public | BindingFlags.Static, + null, + new[] { typeof(string), typeof(IFormatProvider) }, + null); + + if (parseMethod != null) + { + return parseMethod.Invoke(null, new object[] { id, null }); + } + } + + // Fallback to TypeConverter + var converter = TypeDescriptor.GetConverter(clrType); + if (converter.CanConvertFrom(typeof(string))) + { + return converter.ConvertFromString(id); + } + + // Last resort: return as-is and let EF Core handle it (will likely fail) + return id; + } [HttpGet] public IActionResult Index(string id) @@ -142,19 +197,7 @@ private object GetEntityFromDbSet(string dbSetName, string id, var primaryKey = dbContextObject.Model.FindEntityType(typeOfEntity).FindPrimaryKey(); var clrType = primaryKey.Properties[0].ClrType; - object convertedPrimaryKey = id; - if (clrType == typeof(Guid)) - { - convertedPrimaryKey = Guid.Parse(id); - } - else if (clrType == typeof(int)) - { - convertedPrimaryKey = int.Parse(id); - } - else if (clrType == typeof(long)) - { - convertedPrimaryKey = long.Parse(id); - } + var convertedPrimaryKey = ConvertPrimaryKey(id, clrType); return dbSetValue.GetType().InvokeMember("Find", BindingFlags.InvokeMethod, null, dbSetValue, args: new object[] { convertedPrimaryKey }); @@ -326,19 +369,7 @@ public async Task DeleteEntityPost([FromForm] DataDeleteViewModel var primaryKey = dbContextObject.Model.FindEntityType(dbSetProperty.PropertyType.GetGenericArguments()[0]).FindPrimaryKey(); var clrType = primaryKey.Properties[0].ClrType; - object convertedPrimaryKey = viewModel.Id; - if (clrType == typeof(Guid)) - { - convertedPrimaryKey = Guid.Parse(viewModel.Id); - } - else if(clrType == typeof(int)) - { - convertedPrimaryKey = int.Parse(viewModel.Id); - } - else if (clrType == typeof(Int64)) - { - convertedPrimaryKey = Int64.Parse(viewModel.Id); - } + var convertedPrimaryKey = ConvertPrimaryKey(viewModel.Id, clrType); var entityToDelete = dbSetValue.GetType().InvokeMember("Find", BindingFlags.InvokeMethod, null, dbSetValue, args: new object[] { convertedPrimaryKey }); dbSetValue.GetType().InvokeMember("Remove", BindingFlags.InvokeMethod, null, dbSetValue, args: new object[] {entityToDelete});