From fa4fb2e02b93c8e78e18f211cae7ade246190569 Mon Sep 17 00:00:00 2001 From: Van Nguyen Date: Thu, 18 Feb 2016 10:23:31 +1100 Subject: [PATCH 1/2] Add TypeMapperBuilder to use Dapper without setting Attributes on Model classes --- .gitignore | 1 + Dapper.Contrib/ReflectionHelper.cs | 31 +++++++ Dapper.Contrib/SqlMapperBuilder.cs | 121 ++++++++++++++++++++++++++ Dapper.Contrib/SqlMapperExtensions.cs | 14 +++ Dapper.Tests.Contrib/TestSuite.cs | 88 ++++++++++++++++++- Dapper.Tests.Contrib/TestSuites.cs | 9 +- Dapper.Tests.Contrib/project.json | 3 +- 7 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 Dapper.Contrib/ReflectionHelper.cs create mode 100644 Dapper.Contrib/SqlMapperBuilder.cs diff --git a/.gitignore b/.gitignore index 0200bf869..f3e2ac518 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .vs/ bin/ obj/ +artifacts/ /*.user _Resharper* .hgtags diff --git a/Dapper.Contrib/ReflectionHelper.cs b/Dapper.Contrib/ReflectionHelper.cs new file mode 100644 index 000000000..db98f536b --- /dev/null +++ b/Dapper.Contrib/ReflectionHelper.cs @@ -0,0 +1,31 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace Dapper.Contrib +{ + public static class ReflectionHelper + { + public static MemberInfo GetProperty(LambdaExpression lambda) + { + Expression expr = lambda; + for (;;) + { + switch (expr.NodeType) + { + case ExpressionType.Lambda: + expr = ((LambdaExpression)expr).Body; + break; + case ExpressionType.Convert: + expr = ((UnaryExpression)expr).Operand; + break; + case ExpressionType.MemberAccess: + MemberExpression memberExpression = (MemberExpression)expr; + MemberInfo mi = memberExpression.Member; + return mi; + default: + return null; + } + } + } + } +} \ No newline at end of file diff --git a/Dapper.Contrib/SqlMapperBuilder.cs b/Dapper.Contrib/SqlMapperBuilder.cs new file mode 100644 index 000000000..215c01233 --- /dev/null +++ b/Dapper.Contrib/SqlMapperBuilder.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Linq.Expressions; +using Dapper.Contrib.Extensions; + +namespace Dapper.Contrib +{ + public static class TypeMappers + { + public static void Register(TypeMapper mapper) where T : class + { + } + + public static ITypeMapperBuilder MapType() where T : class + { + return new CommonTypeMapper(); + } + + public static void ResetTypeMapping() + { + var typeHandle = typeof(T).TypeHandle; + IEnumerable abc; + string tableName; + SqlMapperExtensions.InternalComputedProperties.TryRemove(typeHandle, out abc); + SqlMapperExtensions.InternalExplicitKeyProperties.TryRemove(typeHandle, out abc); + SqlMapperExtensions.InternalKeyProperties.TryRemove(typeHandle, out abc); + SqlMapperExtensions.InternalTypeProperties.TryRemove(typeHandle, out abc); + SqlMapperExtensions.InternalTypeTableName.TryRemove(typeHandle, out tableName); + } + } + + public partial interface ITypeMapperBuilder where T : class + { + ITypeMapperBuilder TableName(string name); + IPropertyMapperBuilder For(Expression> expression); + ITypeMapperBuilder Key(Expression> expression); + } + + public partial interface IPropertyMapperBuilder where T : class + { + IPropertyMapperBuilder For(Expression> expression); + IPropertyMapperBuilder ExplicitKey(); + IPropertyMapperBuilder NotWritable(); + IPropertyMapperBuilder Computed(); + } + + internal class CommonTypeMapper : TypeMapper where T : class + { + } + + public abstract partial class TypeMapper : ITypeMapperBuilder, IPropertyMapperBuilder where T : class + { + private static readonly RuntimeTypeHandle TypeHandle = typeof(T).TypeHandle; + private PropertyInfo _currentProperty; + + private static void TryAdd(IDictionary> dic, PropertyInfo property) + { + if (!dic.ContainsKey(TypeHandle)) + { + dic[TypeHandle] = new List + { + property + }; + } + else + { + var list = new List(dic[TypeHandle]) {property}; + dic[TypeHandle] = list; + } + } + + private static PropertyInfo GetProperty(Expression> expression) + { + return ReflectionHelper.GetProperty(expression) as PropertyInfo; + } + + public ITypeMapperBuilder TableName(string name) + { + SqlMapperExtensions.InternalTypeTableName[typeof(T).TypeHandle] = name; + return this; + } + + public IPropertyMapperBuilder For(Expression> expression) + { + _currentProperty = GetProperty(expression); + return this; + } + + public ITypeMapperBuilder Key(Expression> expression) + { + TryAdd(SqlMapperExtensions.InternalKeyProperties, GetProperty(expression)); + return this; + } + + + public IPropertyMapperBuilder ExplicitKey() + { + TryAdd(SqlMapperExtensions.InternalExplicitKeyProperties, _currentProperty); + return this; + } + + public IPropertyMapperBuilder NotWritable() + { + var properties = SqlMapperExtensions.InternalTypePropertiesCache(typeof (T)); + var matchProperty = properties.FirstOrDefault(p => p.Equals(_currentProperty)); + if (matchProperty != null && properties.Remove(matchProperty)) + { + SqlMapperExtensions.InternalTypeProperties[TypeHandle] = properties; + } + return this; + } + + public IPropertyMapperBuilder Computed() + { + TryAdd(SqlMapperExtensions.InternalComputedProperties, _currentProperty); + return this; + } + } +} \ No newline at end of file diff --git a/Dapper.Contrib/SqlMapperExtensions.cs b/Dapper.Contrib/SqlMapperExtensions.cs index 1362e1a54..31cf3d0b9 100644 --- a/Dapper.Contrib/SqlMapperExtensions.cs +++ b/Dapper.Contrib/SqlMapperExtensions.cs @@ -686,6 +686,20 @@ public WriteAttribute(bool write) public class ComputedAttribute : Attribute { } + + public static partial class SqlMapperExtensions + { + internal static ConcurrentDictionary> InternalKeyProperties { get { return KeyProperties; } } + internal static ConcurrentDictionary> InternalExplicitKeyProperties { get { return ExplicitKeyProperties; } } + internal static ConcurrentDictionary> InternalComputedProperties { get { return ComputedProperties; } } + internal static ConcurrentDictionary InternalTypeTableName { get { return TypeTableName; } } + internal static ConcurrentDictionary> InternalTypeProperties { get { return TypeProperties; } } + + internal static List InternalTypePropertiesCache(Type type) + { + return TypePropertiesCache(type); + } + } } public partial interface ISqlAdapter diff --git a/Dapper.Tests.Contrib/TestSuite.cs b/Dapper.Tests.Contrib/TestSuite.cs index 7fdd7d201..e59b12ad5 100644 --- a/Dapper.Tests.Contrib/TestSuite.cs +++ b/Dapper.Tests.Contrib/TestSuite.cs @@ -2,9 +2,8 @@ using System.Collections.Generic; using System.Data; using System.Linq; - +using Dapper.Contrib; using Dapper.Contrib.Extensions; - #if COREFX using System.Reflection; using IDbConnection = System.Data.Common.DbConnection; @@ -91,10 +90,26 @@ public class Result public int Order { get; set; } } + public class Vehicle + { + public int Id { get; set; } + public string Name { get; set; } + public string Computed { get; set; } + } + + public class ObjectXWithoutAttribute + { + public string ObjectXId { get; set; } + public string Name { get; set; } + public string NotWritable { get; set; } + } + public abstract partial class TestSuite { protected static readonly bool IsAppVeyor = Environment.GetEnvironmentVariable("Appveyor")?.ToUpperInvariant() == "TRUE"; + protected abstract Type SqlClientExceptionType { get; } + public abstract IDbConnection GetConnection(); private IDbConnection GetOpenConnection() @@ -174,7 +189,36 @@ public void InsertGetUpdateDeleteWithExplicitKey() o2.IsNull(); } } - + + [Fact] + public void InsertGetUpdateDeleteWithExplicitKey_UsingBuilder() + { + TypeMappers + .MapType() + .TableName("ObjectX") + .For(x => x.ObjectXId).ExplicitKey() + .For(x => x.NotWritable).NotWritable(); + + using (var connection = GetOpenConnection()) + { + var guid = Guid.NewGuid().ToString(); + var o1 = new ObjectXWithoutAttribute { ObjectXId = guid, Name = "Foo" }; + var originalxCount = connection.Query("Select Count(*) From ObjectX").First(); + connection.Insert(o1); + var list1 = connection.Query("select * from ObjectX").ToList(); + list1.Count.IsEqualTo(originalxCount + 1); + o1 = connection.Get(guid); + o1.ObjectXId.IsEqualTo(guid); + o1.Name = "Bar"; + connection.Update(o1); + o1 = connection.Get(guid); + o1.Name.IsEqualTo("Bar"); + connection.Delete(o1); + o1 = connection.Get(guid); + o1.IsNull(); + } + } + [Fact] public void GetAllWithExplicitKey() { @@ -258,6 +302,44 @@ public void TableName() } } + + public class VehicleMapper : TypeMapper + { + public VehicleMapper() + { + this + .TableName("Automobiles") + .For(x => x.Computed).Computed(); + } + } + + [Fact] + public void TableName_UsingBuilder() + { + TypeMappers.Register(new VehicleMapper()); + using (var connection = GetOpenConnection()) + { + // tests against "Automobiles" table (Table attribute) + var id = connection.Insert(new Vehicle { Name = "Prado" }); + var car = connection.Get(id); + car.IsNotNull(); + car.Name.IsEqualTo("Prado"); + connection.Get(id).Name.IsEqualTo("Prado"); + connection.Update(new Vehicle { Id = (int)id, Name = "Prado Landcruiser" }).IsEqualTo(true); + connection.Get(id).Name.IsEqualTo("Prado Landcruiser"); + connection.Delete(new Vehicle { Id = (int)id }).IsEqualTo(true); + connection.Get(id).IsNull(); + } + TypeMappers.ResetTypeMapping(); + + using (var connection = GetOpenConnection()) + { + var ex = Xunit.Assert.Throws(SqlClientExceptionType, () => { connection.Insert(new Vehicle { Name = "Prado" }); }); + Xunit.Assert.Contains("Vehicles", ex.Message); + } + } + + [Fact] public void TestSimpleGet() { diff --git a/Dapper.Tests.Contrib/TestSuites.cs b/Dapper.Tests.Contrib/TestSuites.cs index a97c8f9bd..b4bb69321 100644 --- a/Dapper.Tests.Contrib/TestSuites.cs +++ b/Dapper.Tests.Contrib/TestSuites.cs @@ -33,7 +33,7 @@ public class SqlServerTestSuite : TestSuite ? @"Server=(local)\SQL2014;Database=tempdb;User ID=sa;Password=Password12!" : $"Data Source=.;Initial Catalog={DbName};Integrated Security=True"; public override IDbConnection GetConnection() => new SqlConnection(ConnectionString); - + protected override Type SqlClientExceptionType { get { return typeof(System.Data.SqlClient.SqlException); } } static SqlServerTestSuite() { using (var connection = new SqlConnection(ConnectionString)) @@ -76,7 +76,7 @@ public override IDbConnection GetConnection() if (_skip) throw new SkipTestException("Skipping MySQL Tests - no server."); return new MySqlConnection(ConnectionString); } - + protected override Type SqlClientExceptionType { get { return typeof(MySql.Data.MySqlClient.MySqlException); } } private static readonly bool _skip; static MySqlServerTestSuite() @@ -127,7 +127,7 @@ public class SQLiteTestSuite : TestSuite const string FileName = "Test.DB.sqlite"; public static string ConnectionString => $"Filename={FileName};"; public override IDbConnection GetConnection() => new SqliteConnection(ConnectionString); - + protected override Type SqlClientExceptionType { get { return typeof(System.Data.SQLite.SQLiteException); } } static SQLiteTestSuite() { if (File.Exists(FileName)) @@ -157,7 +157,8 @@ public class SqlCETestSuite : TestSuite const string FileName = "Test.DB.sdf"; public static string ConnectionString => $"Data Source={FileName};"; public override IDbConnection GetConnection() => new SqlCeConnection(ConnectionString); - + protected override Type SqlClientExceptionType { get { return typeof (System.Data.SqlServerCe.SqlCeException); } } + static SqlCETestSuite() { if (File.Exists(FileName)) diff --git a/Dapper.Tests.Contrib/project.json b/Dapper.Tests.Contrib/project.json index 74faf8c3b..88319f62b 100644 --- a/Dapper.Tests.Contrib/project.json +++ b/Dapper.Tests.Contrib/project.json @@ -59,7 +59,8 @@ "System.Data.Linq": "4.0.0.0", "System.Runtime": "4.0.0.0", "System.Transactions": "4.0.0.0", - "System.Xml": "4.0.0.0" + "System.Xml": "4.0.0.0", + "System.Threading.Tasks": "4.0.0.0" }, "dependencies": { "Microsoft.SqlServer.Compact": "4.0.8876.1", From 18fa813eb78b1da70965f06cc1fff02f11972781 Mon Sep 17 00:00:00 2001 From: Van Nguyen Date: Thu, 18 Feb 2016 10:48:21 +1100 Subject: [PATCH 2/2] Fix broken test for MySQL --- Dapper.Tests.Contrib/TestSuite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dapper.Tests.Contrib/TestSuite.cs b/Dapper.Tests.Contrib/TestSuite.cs index e59b12ad5..4803982e3 100644 --- a/Dapper.Tests.Contrib/TestSuite.cs +++ b/Dapper.Tests.Contrib/TestSuite.cs @@ -335,7 +335,7 @@ public void TableName_UsingBuilder() using (var connection = GetOpenConnection()) { var ex = Xunit.Assert.Throws(SqlClientExceptionType, () => { connection.Insert(new Vehicle { Name = "Prado" }); }); - Xunit.Assert.Contains("Vehicles", ex.Message); + Xunit.Assert.Contains("vehicles", ex.Message.ToLower()); } }