From 135c84969c5227ffb158865df4c6e0d23d22dbd0 Mon Sep 17 00:00:00 2001 From: Dave Thieben Date: Mon, 13 Jul 2026 15:59:41 -0400 Subject: [PATCH 1/2] additionally looks for `[Key]` attribute or `[Column]` attribute to include columns when generating SQL for an entity --- Dapper.SimpleCRUD/SimpleCRUD.cs | 49 ++++++++++++++------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/Dapper.SimpleCRUD/SimpleCRUD.cs b/Dapper.SimpleCRUD/SimpleCRUD.cs index 691075c..2ecfa42 100644 --- a/Dapper.SimpleCRUD/SimpleCRUD.cs +++ b/Dapper.SimpleCRUD/SimpleCRUD.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; @@ -857,43 +857,28 @@ private static IEnumerable GetScaffoldableProperties() props = props.Where(p => p.GetCustomAttributes(true).Any(attr => attr.GetType().Name == typeof(EditableAttribute).Name && !IsEditable(p)) == false); - - return props.Where(p => p.PropertyType.IsSimpleType() || IsEditable(p)); + return props.Where(p => + p.PropertyType.IsSimpleType() + || IsEditable(p) + || IsDefinedColumn(p)); } //Determine if the Attribute has an AllowEdit key and return its boolean state - //fake the funk and try to mimic EditableAttribute in System.ComponentModel.DataAnnotations - //This allows use of the DataAnnotations property in the model and have the SimpleCRUD engine just figure it out without a reference private static bool IsEditable(PropertyInfo pi) { - var attributes = pi.GetCustomAttributes(false); - if (attributes.Length > 0) - { - dynamic write = attributes.FirstOrDefault(x => x.GetType().Name == typeof(EditableAttribute).Name); - if (write != null) - { - return write.AllowEdit; - } - } - return false; + return pi.GetAttributeNamed(typeof(Dapper.EditableAttribute).Name)?.AllowEdit ?? false; + } + + private static bool IsDefinedColumn(PropertyInfo pi) + { + return pi.GetAttributeNamed(typeof(Dapper.ColumnAttribute).Name) != null + || pi.GetAttributeNamed(typeof(Dapper.KeyAttribute).Name) != null; } - //Determine if the Attribute has an IsReadOnly key and return its boolean state - //fake the funk and try to mimic ReadOnlyAttribute in System.ComponentModel - //This allows use of the DataAnnotations property in the model and have the SimpleCRUD engine just figure it out without a reference private static bool IsReadOnly(PropertyInfo pi) { - var attributes = pi.GetCustomAttributes(false); - if (attributes.Length > 0) - { - dynamic write = attributes.FirstOrDefault(x => x.GetType().Name == typeof(ReadOnlyAttribute).Name); - if (write != null) - { - return write.IsReadOnly; - } - } - return false; + return pi.GetAttributeNamed(typeof(Dapper.ReadOnlyAttribute).Name)?.IsReadOnly ?? false; } //Get all properties that are: @@ -1261,6 +1246,14 @@ public static bool IsSimpleType(this Type type) }; return simpleTypes.Contains(type) || type.IsEnum; } + + //fake the funk and try to mimic EditableAttribute in System.ComponentModel.DataAnnotations + //This allows use of the DataAnnotations property in the model and have the SimpleCRUD engine just figure it out without a reference + public static dynamic GetAttributeNamed(this PropertyInfo pi, string typeName) + { + return pi.GetCustomAttributes(false)? + .FirstOrDefault(x => x.GetType().Name == typeName); + } public static string CacheKey(this IEnumerable props) { From 3046b6b43e26bc3651f1533eaaf08aa5ef7d1f42 Mon Sep 17 00:00:00 2001 From: Dave Thieben Date: Mon, 20 Jul 2026 17:53:44 -0400 Subject: [PATCH 2/2] add support for custom ID types that are IConvertible with a TypeHandler --- Dapper.SimpleCRUD/SimpleCRUD.cs | 22 +++++-- Dapper.SimpleCRUDTests/Models/OfficeId.cs | 40 +++++++++++++ Dapper.SimpleCRUDTests/Program.cs | 5 ++ Dapper.SimpleCRUDTests/Tests.cs | 73 +++++++++++++++++++++-- 4 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 Dapper.SimpleCRUDTests/Models/OfficeId.cs diff --git a/Dapper.SimpleCRUD/SimpleCRUD.cs b/Dapper.SimpleCRUD/SimpleCRUD.cs index 2ecfa42..054b379 100644 --- a/Dapper.SimpleCRUD/SimpleCRUD.cs +++ b/Dapper.SimpleCRUD/SimpleCRUD.cs @@ -55,7 +55,7 @@ private static void StringBuilderCache(StringBuilder sb, string cacheKey, Action StringBuilderCacheDict.AddOrUpdate(cacheKey, value, (t, v) => value); sb.Append(value); } - + /// /// Returns the current dialect name /// @@ -857,9 +857,10 @@ private static IEnumerable GetScaffoldableProperties() props = props.Where(p => p.GetCustomAttributes(true).Any(attr => attr.GetType().Name == typeof(EditableAttribute).Name && !IsEditable(p)) == false); - return props.Where(p => - p.PropertyType.IsSimpleType() - || IsEditable(p) + return props.Where(p => + p.PropertyType.IsSimpleType() + || IsEditable(p) + || IsConvertible(p) || IsDefinedColumn(p)); } @@ -868,7 +869,16 @@ private static bool IsEditable(PropertyInfo pi) { return pi.GetAttributeNamed(typeof(Dapper.EditableAttribute).Name)?.AllowEdit ?? false; } - + + // a property that is `IConvertible` can be in the SELECT and the value then converted appropriately + private static bool IsConvertible(PropertyInfo pi) + { + var propertyType = Nullable.GetUnderlyingType(pi.PropertyType) ?? pi.PropertyType; + return _convertible.IsAssignableFrom(propertyType); + } + + private static readonly Type _convertible = typeof(IConvertible); + private static bool IsDefinedColumn(PropertyInfo pi) { return pi.GetAttributeNamed(typeof(Dapper.ColumnAttribute).Name) != null @@ -1246,7 +1256,7 @@ public static bool IsSimpleType(this Type type) }; return simpleTypes.Contains(type) || type.IsEnum; } - + //fake the funk and try to mimic EditableAttribute in System.ComponentModel.DataAnnotations //This allows use of the DataAnnotations property in the model and have the SimpleCRUD engine just figure it out without a reference public static dynamic GetAttributeNamed(this PropertyInfo pi, string typeName) diff --git a/Dapper.SimpleCRUDTests/Models/OfficeId.cs b/Dapper.SimpleCRUDTests/Models/OfficeId.cs new file mode 100644 index 0000000..248b9aa --- /dev/null +++ b/Dapper.SimpleCRUDTests/Models/OfficeId.cs @@ -0,0 +1,40 @@ +using System; +using System.Data; + +namespace Dapper.SimpleCRUDTests; + +public readonly record struct OfficeId(int Id) : IConvertible +{ + TypeCode IConvertible.GetTypeCode() => TypeCode.Int32; + bool IConvertible.ToBoolean(IFormatProvider provider) => throw new InvalidCastException(); + byte IConvertible.ToByte(IFormatProvider provider) => throw new InvalidCastException(); + char IConvertible.ToChar(IFormatProvider provider) => throw new InvalidCastException(); + DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw new InvalidCastException(); + decimal IConvertible.ToDecimal(IFormatProvider provider) => Id; + double IConvertible.ToDouble(IFormatProvider provider) => Id; + short IConvertible.ToInt16(IFormatProvider provider) => throw new InvalidCastException(); + int IConvertible.ToInt32(IFormatProvider provider) => Id; + long IConvertible.ToInt64(IFormatProvider provider) => Id; + sbyte IConvertible.ToSByte(IFormatProvider provider) => throw new InvalidCastException(); + float IConvertible.ToSingle(IFormatProvider provider) => Id; + string IConvertible.ToString(IFormatProvider provider) => Id.ToString(); + object IConvertible.ToType(Type conversionType, IFormatProvider provider) => throw new NotImplementedException(); + ushort IConvertible.ToUInt16(IFormatProvider provider) => throw new InvalidCastException(); + uint IConvertible.ToUInt32(IFormatProvider provider) => (uint)Id; + ulong IConvertible.ToUInt64(IFormatProvider provider) => (ulong)Id; + + public class MapperTypeHandler : Dapper.SqlMapper.TypeHandler + { + public override OfficeId Parse(object value) + { + if (value is null || value is DBNull) throw new InvalidOperationException($"Cannot parse a null value as OfficeId"); + return new OfficeId(Convert.ToInt32(value)); + } + + public override void SetValue(IDbDataParameter parameter, OfficeId entityId) + { + parameter.DbType = DbType.Int32; + parameter.Value = entityId.Id; + } + } +} diff --git a/Dapper.SimpleCRUDTests/Program.cs b/Dapper.SimpleCRUDTests/Program.cs index e4d15ee..286e8f8 100644 --- a/Dapper.SimpleCRUDTests/Program.cs +++ b/Dapper.SimpleCRUDTests/Program.cs @@ -57,6 +57,7 @@ private static void Setup() { connection.Open(); connection.Execute(@" create table Users (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, Age int not null, ScheduledDayOff int null, CreatedDate datetime DEFAULT(getdate())) "); + connection.Execute(@" create table UsersWithOffice (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, Age int not null, Office int null) "); connection.Execute(@" create table Car (CarId int IDENTITY(1,1) not null, Id int null, Make nvarchar(100) not null, Model nvarchar(100) not null) "); connection.Execute(@" create table BigCar (CarId bigint IDENTITY(2147483650,1) not null, Make nvarchar(100) not null, Model nvarchar(100) not null) "); connection.Execute(@" create table City (Name nvarchar(100) not null, Population int not null) "); @@ -72,6 +73,9 @@ private static void Setup() } Console.WriteLine("Created database"); + + Dapper.SqlMapper.AddTypeHandler(new OfficeId.MapperTypeHandler()); + } private static void SetupPg() @@ -112,6 +116,7 @@ private static void SetupSqLite() { connection.Open(); connection.Execute(@" create table Users (Id INTEGER PRIMARY KEY AUTOINCREMENT, Name nvarchar(100) not null, Age int not null, ScheduledDayOff int null, CreatedDate datetime default current_timestamp ) "); + connection.Execute(@" create table UsersWithOffice (Id INTEGER PRIMARY KEY AUTOINCREMENT, Name nvarchar(100) not null, Age int not null, Office int null) "); connection.Execute(@" create table Car (CarId INTEGER PRIMARY KEY AUTOINCREMENT, Id INTEGER null, Make nvarchar(100) not null, Model nvarchar(100) not null) "); connection.Execute(@" create table BigCar (CarId INTEGER PRIMARY KEY AUTOINCREMENT, Make nvarchar(100) not null, Model nvarchar(100) not null) "); connection.Execute(@" insert into BigCar (CarId,Make,Model) Values (2147483649,'car','car') "); diff --git a/Dapper.SimpleCRUDTests/Tests.cs b/Dapper.SimpleCRUDTests/Tests.cs index e01d033..434f579 100644 --- a/Dapper.SimpleCRUDTests/Tests.cs +++ b/Dapper.SimpleCRUDTests/Tests.cs @@ -1,14 +1,12 @@ -using System.ComponentModel; +using System; +using System.Collections.Generic; using System.Data; using System.Data.SqlClient; -using System.Diagnostics; -using System.Linq; -using System.Collections.Generic; -using System; using System.Data.SQLite; +using System.Linq; +using IBM.Data.DB2.Core; using MySql.Data.MySqlClient; using Npgsql; -using IBM.Data.DB2.Core; namespace Dapper.SimpleCRUDTests { @@ -171,6 +169,17 @@ public class UserWithoutAutoIdentity public string Name { get; set; } public int Age { get; set; } } + + [Table("UsersWithOffice")] + public class UserWithOffice + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public int Age { get; set; } + public OfficeId? Office { get; set; } + } + public class KeyMaster { [Key, Required] @@ -423,6 +432,58 @@ public void TestsGetListWithParameters() connection.Execute("Delete from Users"); } } + + public void Tests_Insert_GetList_WithOfficeId() + { + using (var connection = GetOpenConnection()) + { + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId1", Office = null }); + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId2", Office = new OfficeId(123) }); + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId3", Office = new OfficeId(123) }); + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId4", Office = new OfficeId(456) }); + + var users = connection.GetList(new { Office = new OfficeId(123) }).ToList(); + users.Count.IsEqualTo(2); + + users[0].Office.HasValue.IsTrue(); + users[0].Office.Value.Id.IsEqualTo(123); + + connection.Execute("Delete from UsersWithOffice"); + } + } + + public void Tests_Update_Get_WithOfficeId() + { + int user1Id = 0; + using (var connection = GetOpenConnection()) + { + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId1", Office = null }); + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId2", Office = new OfficeId(123) }); + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId3", Office = new OfficeId(123) }); + connection.Insert(new UserWithOffice { Name = "TestsGetListWithOfficeId4", Office = new OfficeId(456) }); + + var userCount = connection.RecordCount(); + userCount.IsEqualTo(4); + + var user1 = connection.GetList(new { Name = "TestsGetListWithOfficeId1" }).FirstOrDefault(); + user1.Name.IsEqualTo("TestsGetListWithOfficeId1"); + user1.Office.IsNull(); + user1Id = user1.Id; + + user1.Office = new OfficeId(999); + connection.Update(user1); + } + + using (var connection = GetOpenConnection()) + { + var user1again = connection.Get(user1Id); + user1again.Name.IsEqualTo("TestsGetListWithOfficeId1"); + user1again.Office.HasValue.IsTrue(); + user1again.Office.Value.Id.IsEqualTo(999); + + connection.Execute("Delete from UsersWithOffice"); + } + } public void TestGetWithReadonlyProperty() {