diff --git a/Dapper.sln b/Dapper.sln index e993c7a4..4aa75f10 100644 --- a/Dapper.sln +++ b/Dapper.sln @@ -16,7 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution version.json = version.json EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Contrib", "Dapper.Contrib\Dapper.Contrib.csproj", "{4E409F8F-CFBB-4332-8B0A-FD5A283051FD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Contrib", "src\Dapper.Contrib\Dapper.Contrib.csproj", "{4E409F8F-CFBB-4332-8B0A-FD5A283051FD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Contrib", "tests\Dapper.Tests.Contrib\Dapper.Tests.Contrib.csproj", "{DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}" EndProject diff --git a/Readme.md b/Readme.md index 9a1fd892..ec042df9 100644 --- a/Readme.md +++ b/Readme.md @@ -167,6 +167,16 @@ Dapper.Contrib makes use of some optional attributes: * `[Write(true/false)]` - this property is (not) writeable * `[Computed]` - this property is computed and should not be part of updates +Support for Snake Casing +------- +When Dapper's `DefaultTypeMap.MatchNamesWithUnderscores` is set to true, +Dapper will match a property named 'EmployeeId' to a +column named 'employee_id'. + +(If using this setting, it's generally best to avoid capitalised acronyms in property +names. For example, a prroperty named 'EmployeeID' would be mapped to a column +named 'employee_i_d', which is unlikely to be the desired behavior.) + Limitations and caveats ------- diff --git a/src/Dapper.Contrib/Dapper.Contrib.csproj b/src/Dapper.Contrib/Dapper.Contrib.csproj index 86a0cbb2..0f99133a 100644 --- a/src/Dapper.Contrib/Dapper.Contrib.csproj +++ b/src/Dapper.Contrib/Dapper.Contrib.csproj @@ -10,7 +10,7 @@ $(NoWarn);CA1050 - + @@ -19,4 +19,12 @@ + + + all + runtime; build; native; contentfiles; analyzers + + + + \ No newline at end of file diff --git a/src/Dapper.Contrib/SqlMapperExtensions.Async.cs b/src/Dapper.Contrib/SqlMapperExtensions.Async.cs index c93e39a4..1e6db3b4 100644 --- a/src/Dapper.Contrib/SqlMapperExtensions.Async.cs +++ b/src/Dapper.Contrib/SqlMapperExtensions.Async.cs @@ -25,13 +25,14 @@ public static partial class SqlMapperExtensions public static async Task GetAsync(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class { var type = typeof(T); - if (!GetQueries.TryGetValue(type.TypeHandle, out string sql)) + var keyType = new GetQueryCacheKey(type.TypeHandle, DefaultTypeMap.MatchNamesWithUnderscores); + if (!GetQueries.TryGetValue(keyType, out string sql)) { var key = GetSingleKey(nameof(GetAsync)); var name = GetTableName(type); - sql = $"SELECT * FROM {name} WHERE {key.Name} = @id"; - GetQueries[type.TypeHandle] = sql; + sql = $"SELECT * FROM {name} WHERE {ColumnMapping.ColumnName(key.Name)} = @id"; + GetQueries[keyType] = sql; } var dynParams = new DynamicParameters(); @@ -49,7 +50,7 @@ public static async Task GetAsync(this IDbConnection connection, dynamic i foreach (var property in TypePropertiesCache(type)) { - var val = res[property.Name]; + var val = res[ColumnMapping.ColumnName(property.Name)]; if (val == null) continue; if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { @@ -83,13 +84,15 @@ public static Task> GetAllAsync(this IDbConnection connection, var type = typeof(T); var cacheType = typeof(List); - if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) + var cacheKeyType = new GetQueryCacheKey(cacheType.TypeHandle, DefaultTypeMap.MatchNamesWithUnderscores); + + if (!GetQueries.TryGetValue(cacheKeyType, out string sql)) { GetSingleKey(nameof(GetAll)); var name = GetTableName(type); sql = "SELECT * FROM " + name; - GetQueries[cacheType.TypeHandle] = sql; + GetQueries[cacheKeyType] = sql; } if (!type.IsInterface) @@ -108,7 +111,7 @@ private static async Task> GetAllAsyncImpl(IDbConnection conne var obj = ProxyGenerator.GetInterfaceProxy(); foreach (var property in TypePropertiesCache(type)) { - var val = res[property.Name]; + var val = res[ColumnMapping.ColumnName(property.Name)]; if (val == null) continue; if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { diff --git a/src/Dapper.Contrib/SqlMapperExtensions.cs b/src/Dapper.Contrib/SqlMapperExtensions.cs index 9a30e805..aefff0b3 100644 --- a/src/Dapper.Contrib/SqlMapperExtensions.cs +++ b/src/Dapper.Contrib/SqlMapperExtensions.cs @@ -41,6 +41,33 @@ public interface ITableNameMapper string GetTableName(Type type); } + /// + /// Key used for the Get Query cache. A query is deterministic for a type and a + /// naming convention. + /// + public struct GetQueryCacheKey { + /// + /// Initialise a Cache Key + /// + /// The Type handle of the returned object + /// The naming convention used + public GetQueryCacheKey(RuntimeTypeHandle typeHandle, bool isUnderscore) + { + TypeHandle = typeHandle; + IsUnderscore = isUnderscore; + } + + /// + /// The Type handle of the returned object + /// + public RuntimeTypeHandle TypeHandle { get; } + + /// + /// The naming convention used + /// + public bool IsUnderscore { get; } + }; + /// /// The function to get a database type from the given . /// @@ -56,8 +83,8 @@ public interface ITableNameMapper private static readonly ConcurrentDictionary> ExplicitKeyProperties = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> TypeProperties = new ConcurrentDictionary>(); private static readonly ConcurrentDictionary> ComputedProperties = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary GetQueries = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary TypeTableName = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary GetQueries = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary TypeTableName = new ConcurrentDictionary(); private static readonly ISqlAdapter DefaultAdapter = new SqlServerAdapter(); private static readonly Dictionary AdapterDictionary @@ -171,13 +198,14 @@ public static T Get(this IDbConnection connection, dynamic id, IDbTransaction { var type = typeof(T); - if (!GetQueries.TryGetValue(type.TypeHandle, out string sql)) + var keyType = new GetQueryCacheKey(type.TypeHandle, DefaultTypeMap.MatchNamesWithUnderscores); + if (!GetQueries.TryGetValue(keyType, out string sql)) { var key = GetSingleKey(nameof(Get)); var name = GetTableName(type); - sql = $"select * from {name} where {key.Name} = @id"; - GetQueries[type.TypeHandle] = sql; + sql = $"select * from {name} where {ColumnMapping.ColumnName(key.Name)} = @id"; + GetQueries[keyType] = sql; } var dynParams = new DynamicParameters(); @@ -196,7 +224,7 @@ public static T Get(this IDbConnection connection, dynamic id, IDbTransaction foreach (var property in TypePropertiesCache(type)) { - var val = res[property.Name]; + var val = res[ColumnMapping.ColumnName(property.Name)]; if (val == null) continue; if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { @@ -234,13 +262,15 @@ public static IEnumerable GetAll(this IDbConnection connection, IDbTransac var type = typeof(T); var cacheType = typeof(List); - if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) + var keyType = new GetQueryCacheKey(cacheType.TypeHandle, DefaultTypeMap.MatchNamesWithUnderscores); + + if (!GetQueries.TryGetValue(keyType, out string sql)) { GetSingleKey(nameof(GetAll)); var name = GetTableName(type); sql = "select * from " + name; - GetQueries[cacheType.TypeHandle] = sql; + GetQueries[keyType] = sql; } if (!type.IsInterface) return connection.Query(sql, null, transaction, commandTimeout: commandTimeout); @@ -252,7 +282,7 @@ public static IEnumerable GetAll(this IDbConnection connection, IDbTransac var obj = ProxyGenerator.GetInterfaceProxy(); foreach (var property in TypePropertiesCache(type)) { - var val = res[property.Name]; + var val = res[ColumnMapping.ColumnName(property.Name)]; if (val == null) continue; if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { @@ -279,7 +309,9 @@ public static IEnumerable GetAll(this IDbConnection connection, IDbTransac private static string GetTableName(Type type) { - if (TypeTableName.TryGetValue(type.TypeHandle, out string name)) return name; + var keyType = new GetQueryCacheKey(type.TypeHandle, DefaultTypeMap.MatchNamesWithUnderscores); + + if (TypeTableName.TryGetValue(keyType, out string name)) return name; if (TableNameMapper != null) { @@ -304,7 +336,7 @@ private static string GetTableName(Type type) } } - TypeTableName[type.TypeHandle] = name; + TypeTableName[keyType] = name; return name; } @@ -791,14 +823,14 @@ public partial interface ISqlAdapter /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - void AppendColumnName(StringBuilder sb, string columnName); + /// The property name. + void AppendColumnName(StringBuilder sb, string propertyName); /// /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - void AppendColumnNameEqualsValue(StringBuilder sb, string columnName); + /// The column name. + void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName); } /// @@ -840,9 +872,10 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnName(StringBuilder sb, string propertyName) { + var columnName = ColumnMapping.ColumnName(propertyName); sb.AppendFormat("[{0}]", columnName); } @@ -850,10 +883,11 @@ public void AppendColumnName(StringBuilder sb, string columnName) /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName) { - sb.AppendFormat("[{0}] = @{1}", columnName, columnName); + var columnName = ColumnMapping.ColumnName(propertyName); + sb.AppendFormat("[{0}] = @{1}", columnName, propertyName); } } @@ -896,9 +930,10 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnName(StringBuilder sb, string propertyName) { + var columnName = ColumnMapping.ColumnName(propertyName); sb.AppendFormat("[{0}]", columnName); } @@ -906,10 +941,11 @@ public void AppendColumnName(StringBuilder sb, string columnName) /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName) { - sb.AppendFormat("[{0}] = @{1}", columnName, columnName); + var columnName = ColumnMapping.ColumnName(propertyName); + sb.AppendFormat("[{0}] = @{1}", columnName, propertyName); } } @@ -951,9 +987,10 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnName(StringBuilder sb, string propertyName) { + var columnName = ColumnMapping.ColumnName(propertyName); sb.AppendFormat("`{0}`", columnName); } @@ -961,10 +998,11 @@ public void AppendColumnName(StringBuilder sb, string columnName) /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName) { - sb.AppendFormat("`{0}` = @{1}", columnName, columnName); + var columnName = ColumnMapping.ColumnName(propertyName); + sb.AppendFormat("`{0}` = @{1}", columnName, propertyName); } } @@ -1027,9 +1065,10 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnName(StringBuilder sb, string propertyName) { + var columnName = ColumnMapping.ColumnName(propertyName); sb.AppendFormat("\"{0}\"", columnName); } @@ -1037,10 +1076,11 @@ public void AppendColumnName(StringBuilder sb, string columnName) /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName) { - sb.AppendFormat("\"{0}\" = @{1}", columnName, columnName); + var columnName = ColumnMapping.ColumnName(propertyName); + sb.AppendFormat("\"{0}\" = @{1}", columnName, propertyName); } } @@ -1080,9 +1120,10 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnName(StringBuilder sb, string propertyName) { + var columnName = ColumnMapping.ColumnName(propertyName); sb.AppendFormat("\"{0}\"", columnName); } @@ -1090,10 +1131,11 @@ public void AppendColumnName(StringBuilder sb, string columnName) /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName) { - sb.AppendFormat("\"{0}\" = @{1}", columnName, columnName); + var columnName = ColumnMapping.ColumnName(propertyName); + sb.AppendFormat("\"{0}\" = @{1}", columnName, propertyName); } } @@ -1137,9 +1179,10 @@ public int Insert(IDbConnection connection, IDbTransaction transaction, int? com /// Adds the name of a column. /// /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnName(StringBuilder sb, string propertyName) { + var columnName = ColumnMapping.ColumnName(propertyName); sb.AppendFormat("{0}", columnName); } @@ -1147,9 +1190,48 @@ public void AppendColumnName(StringBuilder sb, string columnName) /// Adds a column equality to a parameter. /// /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) + /// The property name. + public void AppendColumnNameEqualsValue(StringBuilder sb, string propertyName) + { + var columnName = ColumnMapping.ColumnName(propertyName); + sb.AppendFormat("{0} = @{1}", columnName, propertyName); + } +} + +/// +/// Utilities for mapping property names to column names +/// +public static class ColumnMapping +{ + /// + /// Converts the property name to a column name, + /// respecting the setting + /// + /// The property name to evaluate + /// The corresponding column name. + /// + /// Underscore Handling was introduced in https://github.com/DapperLib/Dapper/commit/33090c0218383411c3b25fa6cb4cbee38d0f3270 + /// + public static string ColumnName(string propertyName) { - sb.AppendFormat("{0} = @{1}", columnName, columnName); + return DefaultTypeMap.MatchNamesWithUnderscores + ? PascalCaseToSnakeCase(propertyName) + : propertyName; + } + + /// + /// Converts PascalCase to Snake_Case + /// + /// Text to be transformed + /// The Snake_Cased string + /// + /// Because Dapper itself only uses property-column mappings for data reads, + /// it only needed to strip underscores from column names; it did not have to + /// decide on a casing convention for column names. + /// Typically underscores are used with lower case text. + /// + public static string PascalCaseToSnakeCase(string pascalCaseString) { + return string.Concat(pascalCaseString.Select((character, index) + => index > 0 && char.IsUpper(character) ? "_" + character.ToString() : character.ToString())).ToLower(); } } diff --git a/tests/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj b/tests/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj index d217f0ce..3d137142 100644 --- a/tests/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj +++ b/tests/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj @@ -13,11 +13,29 @@ - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/Dapper.Tests.Contrib/PascalToSnakeCasingTests.cs b/tests/Dapper.Tests.Contrib/PascalToSnakeCasingTests.cs new file mode 100644 index 00000000..9d51dfb7 --- /dev/null +++ b/tests/Dapper.Tests.Contrib/PascalToSnakeCasingTests.cs @@ -0,0 +1,15 @@ +using Xunit; + +namespace Dapper.Tests.Contrib +{ + public class PascalToSnakeCasingTests + { + [Fact] + public void ConvertToSnakeCase() + { + Assert.Equal("full_name", ColumnMapping.PascalCaseToSnakeCase("FullName")); + Assert.Equal("the_id", ColumnMapping.PascalCaseToSnakeCase("TheId")); + Assert.Equal("i_d", ColumnMapping.PascalCaseToSnakeCase("ID")); + } + } +} diff --git a/tests/Dapper.Tests.Contrib/TestSuite.Async.cs b/tests/Dapper.Tests.Contrib/TestSuite.Async.cs index fcd11802..bfb903e9 100644 --- a/tests/Dapper.Tests.Contrib/TestSuite.Async.cs +++ b/tests/Dapper.Tests.Contrib/TestSuite.Async.cs @@ -158,10 +158,10 @@ public async Task TestSimpleGetAsync() { using (var connection = GetOpenConnection()) { - var id = await connection.InsertAsync(new User { Name = "Adama", Age = 10 }).ConfigureAwait(false); + var id = await connection.InsertAsync(new User { UserName = "Adama", Age = 10 }).ConfigureAwait(false); var user = await connection.GetAsync(id).ConfigureAwait(false); Assert.Equal(id, user.Id); - Assert.Equal("Adama", user.Name); + Assert.Equal("Adama", user.UserName); await connection.DeleteAsync(user).ConfigureAwait(false); } } @@ -175,25 +175,25 @@ public async Task InsertGetUpdateAsync() var originalCount = (await connection.QueryAsync("select Count(*) from Users").ConfigureAwait(false)).First(); - var id = await connection.InsertAsync(new User { Name = "Adam", Age = 10 }).ConfigureAwait(false); + var id = await connection.InsertAsync(new User { UserName = "Adam", Age = 10 }).ConfigureAwait(false); //get a user with "isdirty" tracking var user = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal("Adam", user.Name); + Assert.Equal("Adam", user.UserName); Assert.False(await connection.UpdateAsync(user).ConfigureAwait(false)); //returns false if not updated, based on tracking - user.Name = "Bob"; + user.UserName = "Bob"; Assert.True(await connection.UpdateAsync(user).ConfigureAwait(false)); //returns true if updated, based on tracking user = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal("Bob", user.Name); + Assert.Equal("Bob", user.UserName); //get a user with no tracking var notrackedUser = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal("Bob", notrackedUser.Name); + Assert.Equal("Bob", notrackedUser.UserName); Assert.True(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); //returns true, even though user was not changed - notrackedUser.Name = "Cecil"; + notrackedUser.UserName = "Cecil"; Assert.True(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); - Assert.Equal("Cecil", (await connection.GetAsync(id).ConfigureAwait(false)).Name); + Assert.Equal("Cecil", (await connection.GetAsync(id).ConfigureAwait(false)).UserName); Assert.Equal((await connection.QueryAsync("select * from Users").ConfigureAwait(false)).Count(), originalCount + 1); Assert.True(await connection.DeleteAsync(user).ConfigureAwait(false)); @@ -201,7 +201,7 @@ public async Task InsertGetUpdateAsync() Assert.False(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); //returns false, user not found - Assert.True(await connection.InsertAsync(new User { Name = "Adam", Age = 10 }).ConfigureAwait(false) > originalCount + 1); + Assert.True(await connection.InsertAsync(new User { UserName = "Adam", Age = 10 }).ConfigureAwait(false) > originalCount + 1); } } @@ -213,7 +213,7 @@ public async Task InsertCheckKeyAsync() await connection.DeleteAllAsync().ConfigureAwait(false); Assert.Null(await connection.GetAsync(3).ConfigureAwait(false)); - var user = new User { Name = "Adamb", Age = 10 }; + var user = new User { UserName = "Adamb", Age = 10 }; var id = await connection.InsertAsync(user).ConfigureAwait(false); Assert.Equal(user.Id, id); } @@ -230,14 +230,15 @@ public async Task BuilderSelectClauseAsync() var data = new List(100); for (var i = 0; i < 100; i++) { - var nU = new User { Age = rand.Next(70), Id = i, Name = Guid.NewGuid().ToString() }; + var nU = new User { Age = rand.Next(70), Id = i, UserName = Guid.NewGuid().ToString() }; data.Add(nU); nU.Id = await connection.InsertAsync(nU).ConfigureAwait(false); } var builder = new SqlBuilder(); var justId = builder.AddTemplate("SELECT /**select**/ FROM Users"); - var all = builder.AddTemplate("SELECT Name, /**select**/, Age FROM Users"); + var isUnderscored = DefaultTypeMap.MatchNamesWithUnderscores; + var all = builder.AddTemplate($"SELECT User{(isUnderscored ? "_" : "")}Name, /**select**/, Age FROM Users"); builder.Select("Id"); @@ -247,7 +248,7 @@ public async Task BuilderSelectClauseAsync() foreach (var u in data) { if (!ids.Any(i => u.Id == i)) throw new Exception("Missing ids in select"); - if (!users.Any(a => a.Id == u.Id && a.Name == u.Name && a.Age == u.Age)) + if (!users.Any(a => a.Id == u.Id && a.UserName == u.UserName && a.Age == u.Age)) throw new Exception("Missing users in select"); } } @@ -266,7 +267,7 @@ public async Task BuilderTemplateWithoutCompositionAsync() { await connection.DeleteAllAsync().ConfigureAwait(false); - await connection.InsertAsync(new User { Age = 5, Name = "Testy McTestington" }).ConfigureAwait(false); + await connection.InsertAsync(new User { Age = 5, UserName = "Testy McTestington" }).ConfigureAwait(false); if ((await connection.QueryAsync(template.RawSql, template.Parameters).ConfigureAwait(false)).Single() != 1) throw new Exception("Query failed"); @@ -298,7 +299,7 @@ private async Task InsertHelperAsync(Func, T> helper) var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -336,7 +337,7 @@ private async Task UpdateHelperAsync(Func, T> helper) var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -348,10 +349,10 @@ private async Task UpdateHelperAsync(Func, T> helper) Assert.Equal(users.Count, numberOfEntities); foreach (var user in users) { - user.Name += " updated"; + user.UserName += " updated"; } await connection.UpdateAsync(helper(users)).ConfigureAwait(false); - var name = connection.Query("select * from Users").First().Name; + var name = connection.Query("select * from Users").First().UserName; Assert.Contains("updated", name); } } @@ -381,7 +382,7 @@ private async Task DeleteHelperAsync(Func, T> helper) var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -406,7 +407,7 @@ public async Task GetAllAsync() var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -465,8 +466,8 @@ public async Task DeleteAllAsync() { await connection.DeleteAllAsync().ConfigureAwait(false); - var id1 = await connection.InsertAsync(new User { Name = "Alice", Age = 32 }).ConfigureAwait(false); - var id2 = await connection.InsertAsync(new User { Name = "Bob", Age = 33 }).ConfigureAwait(false); + var id1 = await connection.InsertAsync(new User { UserName = "Alice", Age = 32 }).ConfigureAwait(false); + var id2 = await connection.InsertAsync(new User { UserName = "Bob", Age = 33 }).ConfigureAwait(false); await connection.DeleteAllAsync().ConfigureAwait(false); Assert.Null(await connection.GetAsync(id1).ConfigureAwait(false)); Assert.Null(await connection.GetAsync(id2).ConfigureAwait(false)); diff --git a/tests/Dapper.Tests.Contrib/TestSuite.cs b/tests/Dapper.Tests.Contrib/TestSuite.cs index 71dbd8ad..c5a1ed59 100644 --- a/tests/Dapper.Tests.Contrib/TestSuite.cs +++ b/tests/Dapper.Tests.Contrib/TestSuite.cs @@ -37,14 +37,14 @@ public interface IUser { [Key] int Id { get; set; } - string Name { get; set; } + string UserName { get; set; } int Age { get; set; } } public class User : IUser { public int Id { get; set; } - public string Name { get; set; } + public string UserName { get; set; } public int Age { get; set; } } @@ -73,7 +73,7 @@ public class Stuff [Key] public short TheId { get; set; } public string Name { get; set; } - public DateTime? Created { get; set; } + public DateTime? CreatedAt { get; set; } } [Table("Automobiles")] @@ -308,10 +308,10 @@ public void NullDateTime() using (var connection = GetOpenConnection()) { connection.Insert(new Stuff { Name = "First item" }); - connection.Insert(new Stuff { Name = "Second item", Created = DateTime.Now }); + connection.Insert(new Stuff { Name = "Second item", CreatedAt = DateTime.Now }); var stuff = connection.Query("select * from Stuff").ToList(); - Assert.Null(stuff[0].Created); - Assert.NotNull(stuff.Last().Created); + Assert.Null(stuff[0].CreatedAt); + Assert.NotNull(stuff.Last().CreatedAt); } } @@ -338,10 +338,10 @@ public void TestSimpleGet() { using (var connection = GetOpenConnection()) { - var id = connection.Insert(new User { Name = "Adama", Age = 10 }); + var id = connection.Insert(new User { UserName = "Adama", Age = 10 }); var user = connection.Get(id); Assert.Equal(user.Id, (int)id); - Assert.Equal("Adama", user.Name); + Assert.Equal("Adama", user.UserName); connection.Delete(user); } } @@ -351,7 +351,7 @@ public void TestClosedConnection() { using (var connection = GetConnection()) { - Assert.True(connection.Insert(new User { Name = "Adama", Age = 10 }) > 0); + Assert.True(connection.Insert(new User { UserName = "Adama", Age = 10 }) > 0); var users = connection.GetAll(); Assert.NotEmpty(users); } @@ -382,7 +382,7 @@ private void InsertHelper(Func, T> helper) var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -420,7 +420,7 @@ private void UpdateHelper(Func, T> helper) var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -432,10 +432,10 @@ private void UpdateHelper(Func, T> helper) Assert.Equal(users.Count, numberOfEntities); foreach (var user in users) { - user.Name += " updated"; + user.UserName += " updated"; } connection.Update(helper(users)); - var name = connection.Query("select * from Users").First().Name; + var name = connection.Query("select * from Users").First().UserName; Assert.Contains("updated", name); } } @@ -465,7 +465,7 @@ private void DeleteHelper(Func, T> helper) var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -494,24 +494,24 @@ public void InsertGetUpdate() //insert with computed attribute that should be ignored connection.Insert(new Car { Name = "Volvo", Computed = "this property should be ignored" }); - var id = connection.Insert(new User { Name = "Adam", Age = 10 }); + var id = connection.Insert(new User { UserName = "Adam", Age = 10 }); //get a user with "isdirty" tracking var user = connection.Get(id); - Assert.Equal("Adam", user.Name); + Assert.Equal("Adam", user.UserName); Assert.False(connection.Update(user)); //returns false if not updated, based on tracking - user.Name = "Bob"; + user.UserName = "Bob"; Assert.True(connection.Update(user)); //returns true if updated, based on tracking user = connection.Get(id); - Assert.Equal("Bob", user.Name); + Assert.Equal("Bob", user.UserName); //get a user with no tracking var notrackedUser = connection.Get(id); - Assert.Equal("Bob", notrackedUser.Name); + Assert.Equal("Bob", notrackedUser.UserName); Assert.True(connection.Update(notrackedUser)); //returns true, even though user was not changed - notrackedUser.Name = "Cecil"; + notrackedUser.UserName = "Cecil"; Assert.True(connection.Update(notrackedUser)); - Assert.Equal("Cecil", connection.Get(id).Name); + Assert.Equal("Cecil", connection.Get(id).UserName); Assert.Single(connection.Query("select * from Users")); Assert.True(connection.Delete(user)); @@ -590,7 +590,7 @@ public void GetAll() var users = new List(numberOfEntities); for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); + users.Add(new User { UserName = "User " + i, Age = i }); using (var connection = GetOpenConnection()) { @@ -672,7 +672,7 @@ public void InsertCheckKey() using (var connection = GetOpenConnection()) { Assert.Null(connection.Get(3)); - User user = new User { Name = "Adamb", Age = 10 }; + User user = new User { UserName = "Adamb", Age = 10 }; int id = (int)connection.Insert(user); Assert.Equal(user.Id, id); } @@ -687,14 +687,15 @@ public void BuilderSelectClause() var data = new List(100); for (int i = 0; i < 100; i++) { - var nU = new User { Age = rand.Next(70), Id = i, Name = Guid.NewGuid().ToString() }; + var nU = new User { Age = rand.Next(70), Id = i, UserName = Guid.NewGuid().ToString() }; data.Add(nU); nU.Id = (int)connection.Insert(nU); } var builder = new SqlBuilder(); var justId = builder.AddTemplate("SELECT /**select**/ FROM Users"); - var all = builder.AddTemplate("SELECT Name, /**select**/, Age FROM Users"); + var isUnderscored = DefaultTypeMap.MatchNamesWithUnderscores; + var all = builder.AddTemplate($"SELECT User{(isUnderscored ? "_" : "")}Name, /**select**/, Age FROM Users"); builder.Select("Id"); @@ -704,7 +705,7 @@ public void BuilderSelectClause() foreach (var u in data) { if (!ids.Any(i => u.Id == i)) throw new Exception("Missing ids in select"); - if (!users.Any(a => a.Id == u.Id && a.Name == u.Name && a.Age == u.Age)) throw new Exception("Missing users in select"); + if (!users.Any(a => a.Id == u.Id && a.UserName == u.UserName && a.Age == u.Age)) throw new Exception("Missing users in select"); } } } @@ -721,7 +722,7 @@ public void BuilderTemplateWithoutComposition() using (var connection = GetOpenConnection()) { connection.DeleteAll(); - connection.Insert(new User { Age = 5, Name = "Testy McTestington" }); + connection.Insert(new User { Age = 5, UserName = "Testy McTestington" }); if (connection.Query(template.RawSql, template.Parameters).Single() != 1) throw new Exception("Query failed"); @@ -746,8 +747,8 @@ public void DeleteAll() { using (var connection = GetOpenConnection()) { - var id1 = connection.Insert(new User { Name = "Alice", Age = 32 }); - var id2 = connection.Insert(new User { Name = "Bob", Age = 33 }); + var id1 = connection.Insert(new User { UserName = "Alice", Age = 32 }); + var id2 = connection.Insert(new User { UserName = "Bob", Age = 33 }); Assert.True(connection.DeleteAll()); Assert.Null(connection.Get(id1)); Assert.Null(connection.Get(id2)); diff --git a/tests/Dapper.Tests.Contrib/TestSuites.cs b/tests/Dapper.Tests.Contrib/TestSuites.cs index 55d4ef5c..2eba9409 100644 --- a/tests/Dapper.Tests.Contrib/TestSuites.cs +++ b/tests/Dapper.Tests.Contrib/TestSuites.cs @@ -19,6 +19,22 @@ public class SkippableFactAttribute : FactAttribute { } + + /// + /// Disaplce parallel running of test suites. Because of the dependency on + /// the static property + /// parallel running wil break tests using different settings. + /// + /// + /// https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel + /// https://github.com/xunit/visualstudio.xunit/issues/191 + /// + [CollectionDefinition("Sequential", DisableParallelization = true)] + public class NonParallelCollectionDefinitionClass + { + } + + [Collection("Sequential")] public class SqlServerTestSuite : TestSuite { private const string DbName = "tempdb"; @@ -29,17 +45,19 @@ public class SqlServerTestSuite : TestSuite static SqlServerTestSuite() { + Dapper.DefaultTypeMap.MatchNamesWithUnderscores = false; + using (var connection = new SqlConnection(ConnectionString)) { // ReSharper disable once AccessToDisposedClosure void dropTable(string name) => connection.Execute($"IF OBJECT_ID('{name}', 'U') IS NOT NULL DROP TABLE [{name}]; "); connection.Open(); dropTable("Stuff"); - connection.Execute("CREATE TABLE Stuff (TheId int IDENTITY(1,1) not null, Name nvarchar(100) not null, Created DateTime null);"); + connection.Execute("CREATE TABLE Stuff (TheId int IDENTITY(1,1) not null, Name nvarchar(100) not null, CreatedAt DateTime null);"); dropTable("People"); connection.Execute("CREATE TABLE People (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null);"); dropTable("Users"); - connection.Execute("CREATE TABLE Users (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, Age int not null);"); + connection.Execute("CREATE TABLE Users (Id int IDENTITY(1,1) not null, UserName nvarchar(100) not null, Age int not null);"); dropTable("Automobiles"); connection.Execute("CREATE TABLE Automobiles (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null);"); dropTable("Results"); @@ -58,6 +76,49 @@ static SqlServerTestSuite() } } + [Collection("Sequential")] + public class SqlServerUnderscoreTestSuite : TestSuite + { + private const string DbName = "tempdb"; + public static string ConnectionString => + GetConnectionString("SqlServerConnectionString", $"Data Source=.;Initial Catalog={DbName};Integrated Security=True"); + + public override IDbConnection GetConnection() => new SqlConnection(ConnectionString); + + static SqlServerUnderscoreTestSuite() + { + Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; + + using (var connection = new SqlConnection(ConnectionString)) + { + // ReSharper disable once AccessToDisposedClosure + void dropTable(string name) => connection.Execute($"IF OBJECT_ID('{name}', 'U') IS NOT NULL DROP TABLE [{name}]; "); + connection.Open(); + dropTable("Stuff"); + connection.Execute("CREATE TABLE Stuff (the_id int IDENTITY(1,1) not null, name nvarchar(100) not null, created_at DateTime null);"); + dropTable("People"); + connection.Execute("CREATE TABLE People (id int IDENTITY(1,1) not null, name nvarchar(100) not null);"); + dropTable("Users"); + connection.Execute("CREATE TABLE Users (id int IDENTITY(1,1) not null, user_name nvarchar(100) not null, age int not null);"); + dropTable("Automobiles"); + connection.Execute("CREATE TABLE Automobiles (id int IDENTITY(1,1) not null, name nvarchar(100) not null);"); + dropTable("Results"); + connection.Execute("CREATE TABLE Results (id int IDENTITY(1,1) not null, name nvarchar(100) not null, [order] int not null);"); + dropTable("ObjectX"); + connection.Execute("CREATE TABLE ObjectX (object_x_Id nvarchar(100) not null, name nvarchar(100) not null);"); + dropTable("ObjectY"); + connection.Execute("CREATE TABLE ObjectY (object_y_Id int not null, name nvarchar(100) not null);"); + dropTable("ObjectZ"); + connection.Execute("CREATE TABLE ObjectZ (id int not null, name nvarchar(100) not null);"); + dropTable("GenericType"); + connection.Execute("CREATE TABLE GenericType (id nvarchar(100) not null, name nvarchar(100) not null);"); + dropTable("NullableDates"); + connection.Execute("CREATE TABLE NullableDates (id int IDENTITY(1,1) not null, date_value DateTime null);"); + } + } + } + + [Collection("Sequential")] public class MySqlServerTestSuite : TestSuite { public static string ConnectionString { get; } = @@ -73,6 +134,8 @@ public override IDbConnection GetConnection() static MySqlServerTestSuite() { + Dapper.DefaultTypeMap.MatchNamesWithUnderscores = false; + try { using (var connection = new MySqlConnection(ConnectionString)) @@ -85,7 +148,7 @@ static MySqlServerTestSuite() dropTable("People"); connection.Execute("CREATE TABLE People (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null);"); dropTable("Users"); - connection.Execute("CREATE TABLE Users (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null, Age int not null);"); + connection.Execute("CREATE TABLE Users (Id int not null AUTO_INCREMENT PRIMARY KEY, UserName nvarchar(100) not null, Age int not null);"); dropTable("Automobiles"); connection.Execute("CREATE TABLE Automobiles (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null);"); dropTable("Results"); @@ -112,6 +175,40 @@ static MySqlServerTestSuite() } } + [Collection("Sequential")] + public class SQLiteUnderscoreTestSuite : TestSuite + { + // Use a different file name to the SQLiteTestSuite to avoid the test runner attempting to interact with the same file. + private const string FileName = "Test.DB.Undescore.sqlite"; + public static string ConnectionString => $"Filename=./{FileName};Mode=ReadWriteCreate;"; + public override IDbConnection GetConnection() => new SqliteConnection(ConnectionString); + + static SQLiteUnderscoreTestSuite() + { + Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true; + + if (File.Exists(FileName)) + { + File.Delete(FileName); + } + using (var connection = new SqliteConnection(ConnectionString)) + { + connection.Open(); + connection.Execute("CREATE TABLE Stuff (the_id integer primary key autoincrement not null, Name nvarchar(100) not null, created_at DateTime null) "); + connection.Execute("CREATE TABLE People (id integer primary key autoincrement not null, name nvarchar(100) not null) "); + connection.Execute("CREATE TABLE Users (id integer primary key autoincrement not null, user_name nvarchar(100) not null, age int not null) "); + connection.Execute("CREATE TABLE Automobiles (id integer primary key autoincrement not null, name nvarchar(100) not null) "); + connection.Execute("CREATE TABLE Results (id integer primary key autoincrement not null, name nvarchar(100) not null, [order] int not null) "); + connection.Execute("CREATE TABLE ObjectX (object_x_id nvarchar(100) not null, name nvarchar(100) not null) "); + connection.Execute("CREATE TABLE ObjectY (object_y_id integer not null, name nvarchar(100) not null) "); + connection.Execute("CREATE TABLE ObjectZ (id integer not null, name nvarchar(100) not null) "); + connection.Execute("CREATE TABLE GenericType (id nvarchar(100) not null, name nvarchar(100) not null) "); + connection.Execute("CREATE TABLE NullableDates (id integer primary key autoincrement not null, date_value DateTime) "); + } + } + } + + [Collection("Sequential")] public class SQLiteTestSuite : TestSuite { private const string FileName = "Test.DB.sqlite"; @@ -120,6 +217,8 @@ public class SQLiteTestSuite : TestSuite static SQLiteTestSuite() { + Dapper.DefaultTypeMap.MatchNamesWithUnderscores = false; + if (File.Exists(FileName)) { File.Delete(FileName); @@ -127,9 +226,9 @@ static SQLiteTestSuite() using (var connection = new SqliteConnection(ConnectionString)) { connection.Open(); - connection.Execute("CREATE TABLE Stuff (TheId integer primary key autoincrement not null, Name nvarchar(100) not null, Created DateTime null) "); + connection.Execute("CREATE TABLE Stuff (TheId integer primary key autoincrement not null, Name nvarchar(100) not null, CreatedAt DateTime null) "); connection.Execute("CREATE TABLE People (Id integer primary key autoincrement not null, Name nvarchar(100) not null) "); - connection.Execute("CREATE TABLE Users (Id integer primary key autoincrement not null, Name nvarchar(100) not null, Age int not null) "); + connection.Execute("CREATE TABLE Users (Id integer primary key autoincrement not null, UserName nvarchar(100) not null, Age int not null) "); connection.Execute("CREATE TABLE Automobiles (Id integer primary key autoincrement not null, Name nvarchar(100) not null) "); connection.Execute("CREATE TABLE Results (Id integer primary key autoincrement not null, Name nvarchar(100) not null, [Order] int not null) "); connection.Execute("CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null) "); @@ -143,7 +242,8 @@ static SQLiteTestSuite() #if SQLCE - public class SqlCETestSuite : TestSuite + [Collection("Sequential")] + public class SqlCETestSuite : TestSuite { const string FileName = "Test.DB.sdf"; public static string ConnectionString => $"Data Source={FileName};"; @@ -160,9 +260,9 @@ static SqlCETestSuite() using (var connection = new SqlCeConnection(ConnectionString)) { connection.Open(); - connection.Execute(@"CREATE TABLE Stuff (TheId int IDENTITY(1,1) not null, Name nvarchar(100) not null, Created DateTime null) "); + connection.Execute(@"CREATE TABLE Stuff (TheId int IDENTITY(1,1) not null, Name nvarchar(100) not null, CreatedAt DateTime null) "); connection.Execute(@"CREATE TABLE People (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null) "); - connection.Execute(@"CREATE TABLE Users (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, Age int not null) "); + connection.Execute(@"CREATE TABLE Users (Id int IDENTITY(1,1) not null, User_Name nvarchar(100) not null, Age int not null) "); connection.Execute(@"CREATE TABLE Automobiles (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null) "); connection.Execute(@"CREATE TABLE Results (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, [Order] int not null) "); connection.Execute(@"CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null) "); diff --git a/tests/Dapper.Tests.Contrib/xunit.runner.json b/tests/Dapper.Tests.Contrib/xunit.runner.json index f2314377..b1b3aacf 100644 --- a/tests/Dapper.Tests.Contrib/xunit.runner.json +++ b/tests/Dapper.Tests.Contrib/xunit.runner.json @@ -1,4 +1,5 @@ { "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", - "shadowCopy": false + "shadowCopy": false, + "parallelizeTestCollections": false } \ No newline at end of file