diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..ab8863289 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [mgravell, dapperlib] +custom: ["https://www.buymeacoffee.com/marcgravell"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..19273c5e6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug, needs-triage +assignees: '' + +--- + +**Check your library version, and try updating** +To help, we're going to need to know your library version. If it isn't the latest: *go do that* - it might +fix the problem, and even if it doesn't: you're going to need to update if we find a problem and fix it, +so you might as well get ready for that now. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected and actual behavior** +A clear and concise description of what you expected to happen, and what actually happens. + +**Additional context** +Add any other context about the problem here: +- what DB backend (and version) are you using, if relevant? +- what ADO.NET provider (and version) are you using, if relevant? +- what OS and .NET runtime (and version) are you using, if relevant? diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 000000000..5e3b83b36 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,43 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # the below token should have repo scope and must be manually added by you in the repository's secret + # This token is required only if you have configured to store the signatures in a remote repository/organization + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://raw.githubusercontent.com/DapperLib/Dapper/main/NonCLA.md' # e.g. a CLA or a DCO document + # branch should not be protected + branch: 'main' + # allowlist: user1,bot* + + # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken + #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) + #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in $owner/$repo#$pullRequestNo' + #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' + #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' + #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..3145873c1 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,57 @@ +name: Main Build + +on: + pull_request: + push: + branches: + - main + paths: + - '*' + - '!/docs/*' # Don't run workflow when files are only in the /docs directory + +jobs: + vm-job: + name: Ubuntu + runs-on: ubuntu-latest + services: + postgres: + image: postgres + ports: + - 5432/tcp + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + ports: + - 1433/tcp + env: + ACCEPT_EULA: Y + SA_PASSWORD: "Password." + mysql: + image: mysql + ports: + - 3306/tcp + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + steps: + - name: Checkout code + uses: actions/checkout@v1 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - name: .NET Build + run: dotnet build Build.csproj -c Release /p:CI=true + - name: Dapper Tests + run: dotnet test tests/Dapper.Tests/Dapper.Tests.csproj -c Release --logger GitHubActions -p:CI=true -p:TestTfmsInParallel=false + env: + MySqlConnectionString: Server=localhost;Port=${{ job.services.mysql.ports[3306] }};Uid=root;Pwd=root;Database=test;Allow User Variables=true + OLEDBConnectionString: Provider=SQLOLEDB;Server=tcp:localhost,${{ job.services.sqlserver.ports[1433] }};Database=tempdb;User Id=sa;Password=Password.; + PostgesConnectionString: Server=localhost;Port=${{ job.services.postgres.ports[5432] }};Database=test;User Id=postgres;Password=postgres; + SqlServerConnectionString: Server=tcp:localhost,${{ job.services.sqlserver.ports[1433] }};Database=tempdb;User Id=sa;Password=Password.; + - name: .NET Lib Pack + run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true diff --git a/.gitignore b/.gitignore index 360431748..02815d7d6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ Test.DB.* TestResults/ Dapper.Tests/*.sdf Dapper.Tests/SqlServerTypes/ -.dotnet/* \ No newline at end of file +.dotnet/* +BenchmarkDotNet.Artifacts/ +.idea/ +.DS_Store \ No newline at end of file diff --git a/Build.csproj b/Build.csproj new file mode 100644 index 000000000..c2ed16c13 --- /dev/null +++ b/Build.csproj @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Dapper.Contrib/Dapper.Contrib.csproj b/Dapper.Contrib/Dapper.Contrib.csproj deleted file mode 100644 index add00a547..000000000 --- a/Dapper.Contrib/Dapper.Contrib.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - Dapper.Contrib - orm;sql;micro-orm;dapper - Dapper.Contrib - The official collection of get, insert, update and delete helpers for Dapper.net. Also handles lists of entities and optional "dirty" tracking of interface-based entities. - Sam Saffron;Johan Danforth - net451;netstandard1.3;netstandard2.0 - - false - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Dapper.Contrib/Readme.md b/Dapper.Contrib/Readme.md deleted file mode 100644 index eeeb0838f..000000000 --- a/Dapper.Contrib/Readme.md +++ /dev/null @@ -1,172 +0,0 @@ -Dapper.Contrib - more extensions for dapper -=========================================== - -Features --------- -Dapper.Contrib contains a number of helper methods for inserting, getting, -updating and deleting records. - -The full list of extension methods in Dapper.Contrib right now are: - -```csharp -T Get(id); -IEnumerable GetAll(); -int Insert(T obj); -int Insert(Enumerable list); -bool Update(T obj); -bool Update(Enumerable list); -bool Delete(T obj); -bool Delete(Enumerable list); -bool DeleteAll(); -``` - -For these extensions to work, the entity in question _MUST_ have a -key property. Dapper will automatically use a property named "`id`" -(case-insensitive) as the key property, if one is present. - -```csharp -public class Car -{ - public int Id { get; set; } // Works by convention - public string Name { get; set; } -} -``` - -If the entity doesn't follow this convention, decorate -a specific property with a `[Key]` or `[ExplicitKey]` attribute. - -```csharp -public class User -{ - [Key] - int TheId { get; set; } - string Name { get; set; } - int Age { get; set; } -} -``` - -`[Key]` should be used for database-generated keys (e.g. autoincrement columns), -while `[ExplicitKey]` should be used for explicit keys generated in code. - -`Get` methods -------- - -Get one specific entity based on id - -```csharp -var car = connection.Get(1); -``` - -or a list of all entities in the table. - -```csharp -var cars = connection.GetAll(); -``` - -`Insert` methods -------- - -Insert one entity - -```csharp -connection.Insert(new Car { Name = "Volvo" }); -``` - -or a list of entities. - -```csharp -connection.Insert(cars); -``` - - - -`Update` methods -------- -Update one specific entity - -```csharp -connection.Update(new Car() { Id = 1, Name = "Saab" }); -``` - -or update a list of entities. - -```csharp -connection.Update(cars); -``` - -`Delete` methods -------- -Delete an entity by the specified `[Key]` property - -```csharp -connection.Delete(new Car() { Id = 1 }); -``` - -a list of entities - -```csharp -connection.Delete(cars); -``` - -or _ALL_ entities in the table. - -```csharp -connection.DeleteAll(); -``` - -Special Attributes ----------- -Dapper.Contrib makes use of some optional attributes: - -* `[Table("Tablename")]` - use another table name instead of the name of the class - - ```csharp - [Table ("emps")] - public class Employee - { - public int Id { get; set; } - public string Name { get; set; } - } - ``` -* `[Key]` - this property represents a database-generated identity/key - - ```csharp - public class Employee - { - [Key] - public int EmployeeId { get; set; } - public string Name { get; set; } - } - ``` -* `[ExplicitKey]` - this property represents an explicit identity/key which is - *not* automatically generated by the database - - ```csharp - public class Employee - { - [ExplicitKey] - public Guid EmployeeId { get; set; } - public string Name { get; set; } - } - ``` -* `[Write(true/false)]` - this property is (not) writeable -* `[Computed]` - this property is computed and should not be part of updates - -Limitations and caveats -------- - -### SQLite - -`SQLiteConnection` exposes an `Update` event that clashes with the `Update` -extension provided by Dapper.Contrib. There are 2 ways to deal with this. - -1. Call the `Update` method explicitly from `SqlMapperExtensions` - - ```Csharp - SqlMapperExtensions.Update(_conn, new Employee { Id = 1, Name = "Mercedes" }); - ``` -2. Make the method signature unique by passing a type parameter to `Update` - - ```Csharp - connection.Update(new Car() { Id = 1, Name = "Maruti" }); - ``` diff --git a/Dapper.Contrib/SqlMapperExtensions.Async.cs b/Dapper.Contrib/SqlMapperExtensions.Async.cs deleted file mode 100644 index 8d283f07a..000000000 --- a/Dapper.Contrib/SqlMapperExtensions.Async.cs +++ /dev/null @@ -1,533 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Dapper; - -namespace Dapper.Contrib.Extensions -{ - public static partial class SqlMapperExtensions - { - /// - /// Returns a single entity by a single id from table "Ts" asynchronously using .NET 4.5 Task. T must be of interface type. - /// Id must be marked with [Key] attribute. - /// Created entity is tracked/intercepted for changes and used by the Update() extension. - /// - /// Interface type to create and populate - /// Open SqlConnection - /// Id of the entity to get, must be marked with [Key] attribute - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// Entity of T - 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 key = GetSingleKey(nameof(GetAsync)); - var name = GetTableName(type); - - sql = $"SELECT * FROM {name} WHERE {key.Name} = @id"; - GetQueries[type.TypeHandle] = sql; - } - - var dynParms = new DynamicParameters(); - dynParms.Add("@id", id); - - if (!type.IsInterface()) - return (await connection.QueryAsync(sql, dynParms, transaction, commandTimeout).ConfigureAwait(false)).FirstOrDefault(); - - var res = (await connection.QueryAsync(sql, dynParms).ConfigureAwait(false)).FirstOrDefault() as IDictionary; - - if (res == null) - return null; - - var obj = ProxyGenerator.GetInterfaceProxy(); - - foreach (var property in TypePropertiesCache(type)) - { - var val = res[property.Name]; - property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); - } - - ((IProxy)obj).IsDirty = false; //reset change tracking and return - - return obj; - } - - /// - /// Returns a list of entites from table "Ts". - /// Id of T must be marked with [Key] attribute. - /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension - /// for optimal performance. - /// - /// Interface or type to create and populate - /// Open SqlConnection - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// Entity of T - public static Task> GetAllAsync(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - var type = typeof(T); - var cacheType = typeof(List); - - if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) - { - GetSingleKey(nameof(GetAll)); - var name = GetTableName(type); - - sql = "SELECT * FROM " + name; - GetQueries[cacheType.TypeHandle] = sql; - } - - if (!type.IsInterface()) - { - return connection.QueryAsync(sql, null, transaction, commandTimeout); - } - return GetAllAsyncImpl(connection, transaction, commandTimeout, sql, type); - } - - private static async Task> GetAllAsyncImpl(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string sql, Type type) where T : class - { - var result = await connection.QueryAsync(sql).ConfigureAwait(false); - var list = new List(); - foreach (IDictionary res in result) - { - var obj = ProxyGenerator.GetInterfaceProxy(); - foreach (var property in TypePropertiesCache(type)) - { - var val = res[property.Name]; - property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); - } - ((IProxy)obj).IsDirty = false; //reset change tracking and return - list.Add(obj); - } - return list; - } - - /// - /// Inserts an entity into table "Ts" asynchronously using .NET 4.5 Task and returns identity id. - /// - /// The type being inserted. - /// Open SqlConnection - /// Entity to insert - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// The specific ISqlAdapter to use, auto-detected based on connection if null - /// Identity of inserted entity - public static Task InsertAsync(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, - int? commandTimeout = null, ISqlAdapter sqlAdapter = null) where T : class - { - var type = typeof(T); - sqlAdapter = sqlAdapter ?? GetFormatter(connection); - - var isList = false; - if (type.IsArray) - { - isList = true; - type = type.GetElementType(); - } - else if (type.IsGenericType() && type.GetTypeInfo().ImplementedInterfaces.Any(ti => ti.IsGenericType() && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - isList = true; - type = type.GetGenericArguments()[0]; - } - - var name = GetTableName(type); - var sbColumnList = new StringBuilder(null); - var allProperties = TypePropertiesCache(type); - var keyProperties = KeyPropertiesCache(type); - var computedProperties = ComputedPropertiesCache(type); - var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); - - for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) - { - var property = allPropertiesExceptKeyAndComputed[i]; - sqlAdapter.AppendColumnName(sbColumnList, property.Name); - if (i < allPropertiesExceptKeyAndComputed.Count - 1) - sbColumnList.Append(", "); - } - - var sbParameterList = new StringBuilder(null); - for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) - { - var property = allPropertiesExceptKeyAndComputed[i]; - sbParameterList.AppendFormat("@{0}", property.Name); - if (i < allPropertiesExceptKeyAndComputed.Count - 1) - sbParameterList.Append(", "); - } - - if (!isList) //single entity - { - return sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(), - sbParameterList.ToString(), keyProperties, entityToInsert); - } - - //insert list of entities - var cmd = $"INSERT INTO {name} ({sbColumnList}) values ({sbParameterList})"; - return connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout); - } - - /// - /// Updates entity in table "Ts" asynchronously using .NET 4.5 Task, checks if the entity is modified if the entity is tracked by the Get() extension. - /// - /// Type to be updated - /// Open SqlConnection - /// Entity to be updated - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// true if updated, false if not found or not modified (tracked entities) - public static async Task UpdateAsync(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - if ((entityToUpdate is IProxy proxy) && !proxy.IsDirty) - { - return false; - } - - var type = typeof(T); - - if (type.IsArray) - { - type = type.GetElementType(); - } - else if (type.IsGenericType()) - { - type = type.GetGenericArguments()[0]; - } - - var keyProperties = KeyPropertiesCache(type); - var explicitKeyProperties = ExplicitKeyPropertiesCache(type); - if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) - throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); - - var name = GetTableName(type); - - var sb = new StringBuilder(); - sb.AppendFormat("update {0} set ", name); - - var allProperties = TypePropertiesCache(type); - keyProperties.AddRange(explicitKeyProperties); - var computedProperties = ComputedPropertiesCache(type); - var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); - - var adapter = GetFormatter(connection); - - for (var i = 0; i < nonIdProps.Count; i++) - { - var property = nonIdProps[i]; - adapter.AppendColumnNameEqualsValue(sb, property.Name); - if (i < nonIdProps.Count - 1) - sb.AppendFormat(", "); - } - sb.Append(" where "); - for (var i = 0; i < keyProperties.Count; i++) - { - var property = keyProperties[i]; - adapter.AppendColumnNameEqualsValue(sb, property.Name); - if (i < keyProperties.Count - 1) - sb.AppendFormat(" and "); - } - var updated = await connection.ExecuteAsync(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction).ConfigureAwait(false); - return updated > 0; - } - - /// - /// Delete entity in table "Ts" asynchronously using .NET 4.5 Task. - /// - /// Type of entity - /// Open SqlConnection - /// Entity to delete - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// true if deleted, false if not found - public static async Task DeleteAsync(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - if (entityToDelete == null) - throw new ArgumentException("Cannot Delete null Object", nameof(entityToDelete)); - - var type = typeof(T); - - if (type.IsArray) - { - type = type.GetElementType(); - } - else if (type.IsGenericType()) - { - type = type.GetGenericArguments()[0]; - } - - var keyProperties = KeyPropertiesCache(type); - var explicitKeyProperties = ExplicitKeyPropertiesCache(type); - if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) - throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); - - var name = GetTableName(type); - keyProperties.AddRange(explicitKeyProperties); - - var sb = new StringBuilder(); - sb.AppendFormat("DELETE FROM {0} WHERE ", name); - - for (var i = 0; i < keyProperties.Count; i++) - { - var property = keyProperties[i]; - sb.AppendFormat("{0} = @{1}", property.Name, property.Name); - if (i < keyProperties.Count - 1) - sb.AppendFormat(" AND "); - } - var deleted = await connection.ExecuteAsync(sb.ToString(), entityToDelete, transaction, commandTimeout).ConfigureAwait(false); - return deleted > 0; - } - - /// - /// Delete all entities in the table related to the type T asynchronously using .NET 4.5 Task. - /// - /// Type of entity - /// Open SqlConnection - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// true if deleted, false if none found - public static async Task DeleteAllAsync(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - var type = typeof(T); - var statement = "DELETE FROM " + GetTableName(type); - var deleted = await connection.ExecuteAsync(statement, null, transaction, commandTimeout).ConfigureAwait(false); - return deleted > 0; - } - } -} - -public partial interface ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); -} - -public partial class SqlServerAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"INSERT INTO {tableName} ({columnList}) values ({parameterList}); SELECT SCOPE_IDENTITY() id"; - var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - - var first = multi.Read().FirstOrDefault(); - if (first == null || first.id == null) return 0; - - var id = (int)first.id; - var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (pi.Length == 0) return id; - - var idp = pi[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return id; - } -} - -public partial class SqlCeServerAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})"; - await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - var r = (await connection.QueryAsync("SELECT @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false)).ToList(); - - if (r[0] == null || r[0].id == null) return 0; - var id = (int)r[0].id; - - var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (pi.Length == 0) return id; - - var idp = pi[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return id; - } -} - -public partial class MySqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, - string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})"; - await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - var r = await connection.QueryAsync("SELECT LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); - - var id = r.First().id; - if (id == null) return 0; - var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (pi.Length == 0) return Convert.ToInt32(id); - - var idp = pi[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return Convert.ToInt32(id); - } -} - -public partial class PostgresAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var sb = new StringBuilder(); - sb.AppendFormat("INSERT INTO {0} ({1}) VALUES ({2})", tableName, columnList, parameterList); - - // If no primary key then safe to assume a join table with not too much data to return - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (propertyInfos.Length == 0) - { - sb.Append(" RETURNING *"); - } - else - { - sb.Append(" RETURNING "); - bool first = true; - foreach (var property in propertyInfos) - { - if (!first) - sb.Append(", "); - first = false; - sb.Append(property.Name); - } - } - - var results = await connection.QueryAsync(sb.ToString(), entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - - // Return the key by assinging the corresponding property in the object - by product is that it supports compound primary keys - var id = 0; - foreach (var p in propertyInfos) - { - var value = ((IDictionary)results.First())[p.Name.ToLower()]; - p.SetValue(entityToInsert, value, null); - if (id == 0) - id = Convert.ToInt32(value); - } - return id; - } -} - -public partial class SQLiteAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList}); SELECT last_insert_rowid() id"; - var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - - var id = (int)multi.Read().First().id; - var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (pi.Length == 0) return id; - - var idp = pi[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return id; - } -} - -public partial class FbAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; - await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); - - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - var keyName = propertyInfos[0].Name; - var r = await connection.QueryAsync($"SELECT FIRST 1 {keyName} ID FROM {tableName} ORDER BY {keyName} DESC", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); - - var id = r.First().ID; - if (id == null) return 0; - if (propertyInfos.Length == 0) return Convert.ToInt32(id); - - var idp = propertyInfos[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return Convert.ToInt32(id); - } -} diff --git a/Dapper.Contrib/SqlMapperExtensions.cs b/Dapper.Contrib/SqlMapperExtensions.cs deleted file mode 100644 index 696dd6e9d..000000000 --- a/Dapper.Contrib/SqlMapperExtensions.cs +++ /dev/null @@ -1,1115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Collections.Concurrent; -using System.Reflection.Emit; - -using Dapper; - -#if NETSTANDARD1_3 -using DataException = System.InvalidOperationException; -#else -using System.Threading; -#endif - -namespace Dapper.Contrib.Extensions -{ - /// - /// The Dapper.Contrib extensions for Dapper - /// - public static partial class SqlMapperExtensions - { - /// - /// Defined a proxy object with a possibly dirty state. - /// - public interface IProxy //must be kept public - { - /// - /// Whether the object has been changed. - /// - bool IsDirty { get; set; } - } - - /// - /// Defines a table name mapper for getting table names from types. - /// - public interface ITableNameMapper - { - /// - /// Gets a table name from a given . - /// - /// The to get a name from. - /// The table name for the given . - string GetTableName(Type type); - } - - /// - /// The function to get a database type from the given . - /// - /// The connection to get a database type name from. - public delegate string GetDatabaseTypeDelegate(IDbConnection connection); - /// - /// The function to get a a table name from a given - /// - /// The to get a table name for. - public delegate string TableNameMapperDelegate(Type type); - - private static readonly ConcurrentDictionary> KeyProperties = new ConcurrentDictionary>(); - 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 ISqlAdapter DefaultAdapter = new SqlServerAdapter(); - private static readonly Dictionary AdapterDictionary - = new Dictionary - { - ["sqlconnection"] = new SqlServerAdapter(), - ["sqlceconnection"] = new SqlCeServerAdapter(), - ["npgsqlconnection"] = new PostgresAdapter(), - ["sqliteconnection"] = new SQLiteAdapter(), - ["mysqlconnection"] = new MySqlAdapter(), - ["fbconnection"] = new FbAdapter() - }; - - private static List ComputedPropertiesCache(Type type) - { - if (ComputedProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) - { - return pi.ToList(); - } - - var computedProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ComputedAttribute)).ToList(); - - ComputedProperties[type.TypeHandle] = computedProperties; - return computedProperties; - } - - private static List ExplicitKeyPropertiesCache(Type type) - { - if (ExplicitKeyProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) - { - return pi.ToList(); - } - - var explicitKeyProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)).ToList(); - - ExplicitKeyProperties[type.TypeHandle] = explicitKeyProperties; - return explicitKeyProperties; - } - - private static List KeyPropertiesCache(Type type) - { - if (KeyProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) - { - return pi.ToList(); - } - - var allProperties = TypePropertiesCache(type); - var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute)).ToList(); - - if (keyProperties.Count == 0) - { - var idProp = allProperties.Find(p => string.Equals(p.Name, "id", StringComparison.CurrentCultureIgnoreCase)); - if (idProp != null && !idProp.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)) - { - keyProperties.Add(idProp); - } - } - - KeyProperties[type.TypeHandle] = keyProperties; - return keyProperties; - } - - private static List TypePropertiesCache(Type type) - { - if (TypeProperties.TryGetValue(type.TypeHandle, out IEnumerable pis)) - { - return pis.ToList(); - } - - var properties = type.GetProperties().Where(IsWriteable).ToArray(); - TypeProperties[type.TypeHandle] = properties; - return properties.ToList(); - } - - private static bool IsWriteable(PropertyInfo pi) - { - var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false).AsList(); - if (attributes.Count != 1) return true; - - var writeAttribute = (WriteAttribute)attributes[0]; - return writeAttribute.Write; - } - - private static PropertyInfo GetSingleKey(string method) - { - var type = typeof(T); - var keys = KeyPropertiesCache(type); - var explicitKeys = ExplicitKeyPropertiesCache(type); - var keyCount = keys.Count + explicitKeys.Count; - if (keyCount > 1) - throw new DataException($"{method} only supports an entity with a single [Key] or [ExplicitKey] property"); - if (keyCount == 0) - throw new DataException($"{method} only supports an entity with a [Key] or an [ExplicitKey] property"); - - return keys.Count > 0 ? keys[0] : explicitKeys[0]; - } - - /// - /// Returns a single entity by a single id from table "Ts". - /// Id must be marked with [Key] attribute. - /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension - /// for optimal performance. - /// - /// Interface or type to create and populate - /// Open SqlConnection - /// Id of the entity to get, must be marked with [Key] attribute - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// Entity of T - public static T Get(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 key = GetSingleKey(nameof(Get)); - var name = GetTableName(type); - - sql = $"select * from {name} where {key.Name} = @id"; - GetQueries[type.TypeHandle] = sql; - } - - var dynParms = new DynamicParameters(); - dynParms.Add("@id", id); - - T obj; - - if (type.IsInterface()) - { - var res = connection.Query(sql, dynParms).FirstOrDefault() as IDictionary; - - if (res == null) - return null; - - obj = ProxyGenerator.GetInterfaceProxy(); - - foreach (var property in TypePropertiesCache(type)) - { - var val = res[property.Name]; - property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); - } - - ((IProxy)obj).IsDirty = false; //reset change tracking and return - } - else - { - obj = connection.Query(sql, dynParms, transaction, commandTimeout: commandTimeout).FirstOrDefault(); - } - return obj; - } - - /// - /// Returns a list of entites from table "Ts". - /// Id of T must be marked with [Key] attribute. - /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension - /// for optimal performance. - /// - /// Interface or type to create and populate - /// Open SqlConnection - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// Entity of T - public static IEnumerable GetAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - var type = typeof(T); - var cacheType = typeof(List); - - if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) - { - GetSingleKey(nameof(GetAll)); - var name = GetTableName(type); - - sql = "select * from " + name; - GetQueries[cacheType.TypeHandle] = sql; - } - - if (!type.IsInterface()) return connection.Query(sql, null, transaction, commandTimeout: commandTimeout); - - var result = connection.Query(sql); - var list = new List(); - foreach (IDictionary res in result) - { - var obj = ProxyGenerator.GetInterfaceProxy(); - foreach (var property in TypePropertiesCache(type)) - { - var val = res[property.Name]; - property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); - } - ((IProxy)obj).IsDirty = false; //reset change tracking and return - list.Add(obj); - } - return list; - } - - /// - /// Specify a custom table name mapper based on the POCO type name - /// - public static TableNameMapperDelegate TableNameMapper; - - private static string GetTableName(Type type) - { - if (TypeTableName.TryGetValue(type.TypeHandle, out string name)) return name; - - if (TableNameMapper != null) - { - name = TableNameMapper(type); - } - else - { - //NOTE: This as dynamic trick should be able to handle both our own Table-attribute as well as the one in EntityFramework - var tableAttr = type -#if NETSTANDARD1_3 - .GetTypeInfo() -#endif - .GetCustomAttributes(false).SingleOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic; - if (tableAttr != null) - { - name = tableAttr.Name; - } - else - { - name = type.Name + "s"; - if (type.IsInterface() && name.StartsWith("I")) - name = name.Substring(1); - } - } - - TypeTableName[type.TypeHandle] = name; - return name; - } - - /// - /// Inserts an entity into table "Ts" and returns identity id or number of inserted rows if inserting a list. - /// - /// The type to insert. - /// Open SqlConnection - /// Entity to insert, can be list of entities - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// Identity of inserted entity, or number of inserted rows if inserting a list - public static long Insert(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - var isList = false; - - var type = typeof(T); - - if (type.IsArray) - { - isList = true; - type = type.GetElementType(); - } - else if (type.IsGenericType() && type.GetTypeInfo().ImplementedInterfaces.Any(ti => ti.IsGenericType() && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - isList = true; - type = type.GetGenericArguments()[0]; - } - - var name = GetTableName(type); - var sbColumnList = new StringBuilder(null); - var allProperties = TypePropertiesCache(type); - var keyProperties = KeyPropertiesCache(type); - var computedProperties = ComputedPropertiesCache(type); - var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); - - var adapter = GetFormatter(connection); - - for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) - { - var property = allPropertiesExceptKeyAndComputed[i]; - adapter.AppendColumnName(sbColumnList, property.Name); //fix for issue #336 - if (i < allPropertiesExceptKeyAndComputed.Count - 1) - sbColumnList.Append(", "); - } - - var sbParameterList = new StringBuilder(null); - for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) - { - var property = allPropertiesExceptKeyAndComputed[i]; - sbParameterList.AppendFormat("@{0}", property.Name); - if (i < allPropertiesExceptKeyAndComputed.Count - 1) - sbParameterList.Append(", "); - } - - int returnVal; - var wasClosed = connection.State == ConnectionState.Closed; - if (wasClosed) connection.Open(); - - if (!isList) //single entity - { - returnVal = adapter.Insert(connection, transaction, commandTimeout, name, sbColumnList.ToString(), - sbParameterList.ToString(), keyProperties, entityToInsert); - } - else - { - //insert list of entities - var cmd = $"insert into {name} ({sbColumnList}) values ({sbParameterList})"; - returnVal = connection.Execute(cmd, entityToInsert, transaction, commandTimeout); - } - if (wasClosed) connection.Close(); - return returnVal; - } - - /// - /// Updates entity in table "Ts", checks if the entity is modified if the entity is tracked by the Get() extension. - /// - /// Type to be updated - /// Open SqlConnection - /// Entity to be updated - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// true if updated, false if not found or not modified (tracked entities) - public static bool Update(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - if (entityToUpdate is IProxy proxy && !proxy.IsDirty) - { - return false; - } - - var type = typeof(T); - - if (type.IsArray) - { - type = type.GetElementType(); - } - else if (type.IsGenericType()) - { - type = type.GetGenericArguments()[0]; - } - - var keyProperties = KeyPropertiesCache(type).ToList(); //added ToList() due to issue #418, must work on a list copy - var explicitKeyProperties = ExplicitKeyPropertiesCache(type); - if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) - throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); - - var name = GetTableName(type); - - var sb = new StringBuilder(); - sb.AppendFormat("update {0} set ", name); - - var allProperties = TypePropertiesCache(type); - keyProperties.AddRange(explicitKeyProperties); - var computedProperties = ComputedPropertiesCache(type); - var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); - - var adapter = GetFormatter(connection); - - for (var i = 0; i < nonIdProps.Count; i++) - { - var property = nonIdProps[i]; - adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336 - if (i < nonIdProps.Count - 1) - sb.AppendFormat(", "); - } - sb.Append(" where "); - for (var i = 0; i < keyProperties.Count; i++) - { - var property = keyProperties[i]; - adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336 - if (i < keyProperties.Count - 1) - sb.AppendFormat(" and "); - } - var updated = connection.Execute(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction); - return updated > 0; - } - - /// - /// Delete entity in table "Ts". - /// - /// Type of entity - /// Open SqlConnection - /// Entity to delete - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// true if deleted, false if not found - public static bool Delete(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - if (entityToDelete == null) - throw new ArgumentException("Cannot Delete null Object", nameof(entityToDelete)); - - var type = typeof(T); - - if (type.IsArray) - { - type = type.GetElementType(); - } - else if (type.IsGenericType()) - { - type = type.GetGenericArguments()[0]; - } - - var keyProperties = KeyPropertiesCache(type).ToList(); //added ToList() due to issue #418, must work on a list copy - var explicitKeyProperties = ExplicitKeyPropertiesCache(type); - if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) - throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); - - var name = GetTableName(type); - keyProperties.AddRange(explicitKeyProperties); - - var sb = new StringBuilder(); - sb.AppendFormat("delete from {0} where ", name); - - var adapter = GetFormatter(connection); - - for (var i = 0; i < keyProperties.Count; i++) - { - var property = keyProperties[i]; - adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336 - if (i < keyProperties.Count - 1) - sb.AppendFormat(" and "); - } - var deleted = connection.Execute(sb.ToString(), entityToDelete, transaction, commandTimeout); - return deleted > 0; - } - - /// - /// Delete all entities in the table related to the type T. - /// - /// Type of entity - /// Open SqlConnection - /// The transaction to run under, null (the default) if none - /// Number of seconds before command execution timeout - /// true if deleted, false if none found - public static bool DeleteAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class - { - var type = typeof(T); - var name = GetTableName(type); - var statement = $"delete from {name}"; - var deleted = connection.Execute(statement, null, transaction, commandTimeout); - return deleted > 0; - } - - /// - /// Specifies a custom callback that detects the database type instead of relying on the default strategy (the name of the connection type object). - /// Please note that this callback is global and will be used by all the calls that require a database specific adapter. - /// - public static GetDatabaseTypeDelegate GetDatabaseType; - - private static ISqlAdapter GetFormatter(IDbConnection connection) - { - var name = GetDatabaseType?.Invoke(connection).ToLower() - ?? connection.GetType().Name.ToLower(); - - return !AdapterDictionary.ContainsKey(name) - ? DefaultAdapter - : AdapterDictionary[name]; - } - - private static class ProxyGenerator - { - private static readonly Dictionary TypeCache = new Dictionary(); - - private static AssemblyBuilder GetAsmBuilder(string name) - { -#if NETSTANDARD1_3 || NETSTANDARD2_0 - return AssemblyBuilder.DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run); -#else - return Thread.GetDomain().DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run); -#endif - } - - public static T GetInterfaceProxy() - { - Type typeOfT = typeof(T); - - if (TypeCache.TryGetValue(typeOfT, out Type k)) - { - return (T)Activator.CreateInstance(k); - } - var assemblyBuilder = GetAsmBuilder(typeOfT.Name); - - var moduleBuilder = assemblyBuilder.DefineDynamicModule("SqlMapperExtensions." + typeOfT.Name); //NOTE: to save, add "asdasd.dll" parameter - - var interfaceType = typeof(IProxy); - var typeBuilder = moduleBuilder.DefineType(typeOfT.Name + "_" + Guid.NewGuid(), - TypeAttributes.Public | TypeAttributes.Class); - typeBuilder.AddInterfaceImplementation(typeOfT); - typeBuilder.AddInterfaceImplementation(interfaceType); - - //create our _isDirty field, which implements IProxy - var setIsDirtyMethod = CreateIsDirtyProperty(typeBuilder); - - // Generate a field for each property, which implements the T - foreach (var property in typeof(T).GetProperties()) - { - var isId = property.GetCustomAttributes(true).Any(a => a is KeyAttribute); - CreateProperty(typeBuilder, property.Name, property.PropertyType, setIsDirtyMethod, isId); - } - -#if NETSTANDARD1_3 || NETSTANDARD2_0 - var generatedType = typeBuilder.CreateTypeInfo().AsType(); -#else - var generatedType = typeBuilder.CreateType(); -#endif - - TypeCache.Add(typeOfT, generatedType); - return (T)Activator.CreateInstance(generatedType); - } - - private static MethodInfo CreateIsDirtyProperty(TypeBuilder typeBuilder) - { - var propType = typeof(bool); - var field = typeBuilder.DefineField("_" + nameof(IProxy.IsDirty), propType, FieldAttributes.Private); - var property = typeBuilder.DefineProperty(nameof(IProxy.IsDirty), - System.Reflection.PropertyAttributes.None, - propType, - new[] { propType }); - - const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.SpecialName - | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig; - - // Define the "get" and "set" accessor methods - var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + nameof(IProxy.IsDirty), - getSetAttr, - propType, - Type.EmptyTypes); - var currGetIl = currGetPropMthdBldr.GetILGenerator(); - currGetIl.Emit(OpCodes.Ldarg_0); - currGetIl.Emit(OpCodes.Ldfld, field); - currGetIl.Emit(OpCodes.Ret); - var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + nameof(IProxy.IsDirty), - getSetAttr, - null, - new[] { propType }); - var currSetIl = currSetPropMthdBldr.GetILGenerator(); - currSetIl.Emit(OpCodes.Ldarg_0); - currSetIl.Emit(OpCodes.Ldarg_1); - currSetIl.Emit(OpCodes.Stfld, field); - currSetIl.Emit(OpCodes.Ret); - - property.SetGetMethod(currGetPropMthdBldr); - property.SetSetMethod(currSetPropMthdBldr); - var getMethod = typeof(IProxy).GetMethod("get_" + nameof(IProxy.IsDirty)); - var setMethod = typeof(IProxy).GetMethod("set_" + nameof(IProxy.IsDirty)); - typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod); - typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); - - return currSetPropMthdBldr; - } - - private static void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propType, MethodInfo setIsDirtyMethod, bool isIdentity) - { - //Define the field and the property - var field = typeBuilder.DefineField("_" + propertyName, propType, FieldAttributes.Private); - var property = typeBuilder.DefineProperty(propertyName, - System.Reflection.PropertyAttributes.None, - propType, - new[] { propType }); - - const MethodAttributes getSetAttr = MethodAttributes.Public - | MethodAttributes.Virtual - | MethodAttributes.HideBySig; - - // Define the "get" and "set" accessor methods - var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, - getSetAttr, - propType, - Type.EmptyTypes); - - var currGetIl = currGetPropMthdBldr.GetILGenerator(); - currGetIl.Emit(OpCodes.Ldarg_0); - currGetIl.Emit(OpCodes.Ldfld, field); - currGetIl.Emit(OpCodes.Ret); - - var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName, - getSetAttr, - null, - new[] { propType }); - - //store value in private field and set the isdirty flag - var currSetIl = currSetPropMthdBldr.GetILGenerator(); - currSetIl.Emit(OpCodes.Ldarg_0); - currSetIl.Emit(OpCodes.Ldarg_1); - currSetIl.Emit(OpCodes.Stfld, field); - currSetIl.Emit(OpCodes.Ldarg_0); - currSetIl.Emit(OpCodes.Ldc_I4_1); - currSetIl.Emit(OpCodes.Call, setIsDirtyMethod); - currSetIl.Emit(OpCodes.Ret); - - //TODO: Should copy all attributes defined by the interface? - if (isIdentity) - { - var keyAttribute = typeof(KeyAttribute); - var myConstructorInfo = keyAttribute.GetConstructor(new Type[] { }); - var attributeBuilder = new CustomAttributeBuilder(myConstructorInfo, new object[] { }); - property.SetCustomAttribute(attributeBuilder); - } - - property.SetGetMethod(currGetPropMthdBldr); - property.SetSetMethod(currSetPropMthdBldr); - var getMethod = typeof(T).GetMethod("get_" + propertyName); - var setMethod = typeof(T).GetMethod("set_" + propertyName); - typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod); - typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); - } - } - } - - /// - /// Defines the name of a table to use in Dapper.Contrib commands. - /// - [AttributeUsage(AttributeTargets.Class)] - public class TableAttribute : Attribute - { - /// - /// Creates a table mapping to a specific name for Dapper.Contrib commands - /// - /// The name of this table in the database. - public TableAttribute(string tableName) - { - Name = tableName; - } - - /// - /// The name of the table in the database - /// - public string Name { get; set; } - } - - /// - /// Specifies that this field is a primary key in the database - /// - [AttributeUsage(AttributeTargets.Property)] - public class KeyAttribute : Attribute - { - } - - /// - /// Specifies that this field is a explicitly set primary key in the database - /// - [AttributeUsage(AttributeTargets.Property)] - public class ExplicitKeyAttribute : Attribute - { - } - - /// - /// Specifies whether a field is writable in the database. - /// - [AttributeUsage(AttributeTargets.Property)] - public class WriteAttribute : Attribute - { - /// - /// Specifies whether a field is writable in the database. - /// - /// Whether a field is writable in the database. - public WriteAttribute(bool write) - { - Write = write; - } - - /// - /// Whether a field is writable in the database. - /// - public bool Write { get; } - } - - /// - /// Specifies that this is a computed column. - /// - [AttributeUsage(AttributeTargets.Property)] - public class ComputedAttribute : Attribute - { - } -} - -/// -/// The interface for all Dapper.Contrib database operations -/// Implementing this is each provider's model. -/// -public partial interface ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - void AppendColumnName(StringBuilder sb, string columnName); - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - void AppendColumnNameEqualsValue(StringBuilder sb, string columnName); -} - -/// -/// The SQL Server database adapter. -/// -public partial class SqlServerAdapter : ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"insert into {tableName} ({columnList}) values ({parameterList});select SCOPE_IDENTITY() id"; - var multi = connection.QueryMultiple(cmd, entityToInsert, transaction, commandTimeout); - - var first = multi.Read().FirstOrDefault(); - if (first == null || first.id == null) return 0; - - var id = (int)first.id; - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (propertyInfos.Length == 0) return id; - - var idProperty = propertyInfos[0]; - idProperty.SetValue(entityToInsert, Convert.ChangeType(id, idProperty.PropertyType), null); - - return id; - } - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) - { - sb.AppendFormat("[{0}]", columnName); - } - - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) - { - sb.AppendFormat("[{0}] = @{1}", columnName, columnName); - } -} - -/// -/// The SQL Server Compact Edition database adapter. -/// -public partial class SqlCeServerAdapter : ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; - connection.Execute(cmd, entityToInsert, transaction, commandTimeout); - var r = connection.Query("select @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout).ToList(); - - if (r[0].id == null) return 0; - var id = (int)r[0].id; - - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (propertyInfos.Length == 0) return id; - - var idProperty = propertyInfos[0]; - idProperty.SetValue(entityToInsert, Convert.ChangeType(id, idProperty.PropertyType), null); - - return id; - } - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) - { - sb.AppendFormat("[{0}]", columnName); - } - - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) - { - sb.AppendFormat("[{0}] = @{1}", columnName, columnName); - } -} - -/// -/// The MySQL database adapter. -/// -public partial class MySqlAdapter : ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; - connection.Execute(cmd, entityToInsert, transaction, commandTimeout); - var r = connection.Query("Select LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout); - - var id = r.First().id; - if (id == null) return 0; - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (propertyInfos.Length == 0) return Convert.ToInt32(id); - - var idp = propertyInfos[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return Convert.ToInt32(id); - } - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) - { - sb.AppendFormat("`{0}`", columnName); - } - - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) - { - sb.AppendFormat("`{0}` = @{1}", columnName, columnName); - } -} - -/// -/// The Postgres database adapter. -/// -public partial class PostgresAdapter : ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var sb = new StringBuilder(); - sb.AppendFormat("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList); - - // If no primary key then safe to assume a join table with not too much data to return - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (propertyInfos.Length == 0) - { - sb.Append(" RETURNING *"); - } - else - { - sb.Append(" RETURNING "); - var first = true; - foreach (var property in propertyInfos) - { - if (!first) - sb.Append(", "); - first = false; - sb.Append(property.Name); - } - } - - var results = connection.Query(sb.ToString(), entityToInsert, transaction, commandTimeout: commandTimeout).ToList(); - - // Return the key by assinging the corresponding property in the object - by product is that it supports compound primary keys - var id = 0; - foreach (var p in propertyInfos) - { - var value = ((IDictionary)results[0])[p.Name.ToLower()]; - p.SetValue(entityToInsert, value, null); - if (id == 0) - id = Convert.ToInt32(value); - } - return id; - } - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) - { - sb.AppendFormat("\"{0}\"", columnName); - } - - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) - { - sb.AppendFormat("\"{0}\" = @{1}", columnName, columnName); - } -} - -/// -/// The SQLite database adapter. -/// -public partial class SQLiteAdapter : ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList}); SELECT last_insert_rowid() id"; - var multi = connection.QueryMultiple(cmd, entityToInsert, transaction, commandTimeout); - - var id = (int)multi.Read().First().id; - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - if (propertyInfos.Length == 0) return id; - - var idProperty = propertyInfos[0]; - idProperty.SetValue(entityToInsert, Convert.ChangeType(id, idProperty.PropertyType), null); - - return id; - } - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) - { - sb.AppendFormat("\"{0}\"", columnName); - } - - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) - { - sb.AppendFormat("\"{0}\" = @{1}", columnName, columnName); - } -} - -/// -/// The Firebase SQL adapeter. -/// -public partial class FbAdapter : ISqlAdapter -{ - /// - /// Inserts into the database, returning the Id of the row created. - /// - /// The connection to use. - /// The transaction to use. - /// The command timeout to use. - /// The table to insert into. - /// The columns to set with this insert. - /// The parameters to set for this insert. - /// The key columns in this table. - /// The entity to insert. - /// The Id of the row created. - public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) - { - var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; - connection.Execute(cmd, entityToInsert, transaction, commandTimeout); - - var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); - var keyName = propertyInfos[0].Name; - var r = connection.Query($"SELECT FIRST 1 {keyName} ID FROM {tableName} ORDER BY {keyName} DESC", transaction: transaction, commandTimeout: commandTimeout); - - var id = r.First().ID; - if (id == null) return 0; - if (propertyInfos.Length == 0) return Convert.ToInt32(id); - - var idp = propertyInfos[0]; - idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); - - return Convert.ToInt32(id); - } - - /// - /// Adds the name of a column. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnName(StringBuilder sb, string columnName) - { - sb.AppendFormat("{0}", columnName); - } - - /// - /// Adds a column equality to a parameter. - /// - /// The string builder to append to. - /// The column name. - public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) - { - sb.AppendFormat("{0} = @{1}", columnName, columnName); - } -} diff --git a/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj b/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj index fdc7382c0..f016f490a 100644 --- a/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj +++ b/Dapper.EntityFramework.StrongName/Dapper.EntityFramework.StrongName.csproj @@ -4,25 +4,20 @@ Dapper: Entity Framework type handlers (with a strong name) Extension handlers for entity framework Marc Gravell;Nick Craver - net451 + net461 ../Dapper.snk true true Dapper.EntityFramework.StrongName orm;sql;micro-orm + enable - - - - - - - - + + diff --git a/Dapper.EntityFramework/Dapper.EntityFramework.csproj b/Dapper.EntityFramework/Dapper.EntityFramework.csproj index 8d2976e78..0496d9c8f 100644 --- a/Dapper.EntityFramework/Dapper.EntityFramework.csproj +++ b/Dapper.EntityFramework/Dapper.EntityFramework.csproj @@ -5,18 +5,18 @@ Dapper entity framework type handlers 1.50.2 Marc Gravell;Nick Craver - net451 + net461 orm;sql;micro-orm + enable - - - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers + \ No newline at end of file diff --git a/Dapper.EntityFramework/DbGeographyHandler.cs b/Dapper.EntityFramework/DbGeographyHandler.cs index c0355dc91..67893a386 100644 --- a/Dapper.EntityFramework/DbGeographyHandler.cs +++ b/Dapper.EntityFramework/DbGeographyHandler.cs @@ -27,10 +27,10 @@ public class DbGeographyHandler : SqlMapper.TypeHandler /// /// The parameter to configure. /// Parameter value. - public override void SetValue(IDbDataParameter parameter, DbGeography value) + public override void SetValue(IDbDataParameter parameter, DbGeography? value) { - object parsed = null; - if (value != null) + object? parsed = null; + if (value is not null) { parsed = SqlGeography.STGeomFromWKB(new SqlBytes(value.AsBinary()), value.CoordinateSystemId); } @@ -46,12 +46,12 @@ public override void SetValue(IDbDataParameter parameter, DbGeography value) /// /// The value from the database. /// The typed value. - public override DbGeography Parse(object value) + public override DbGeography? Parse(object? value) { - if (value == null || value is DBNull) return null; + if (value is null || value is DBNull) return null; if (value is SqlGeography geo) { - return DbGeography.FromBinary(geo.STAsBinary().Value); + return DbGeography.FromBinary(geo.STAsBinary().Value, geo.STSrid.Value); } return DbGeography.FromText(value.ToString()); } diff --git a/Dapper.EntityFramework/DbGeometryHandler.cs b/Dapper.EntityFramework/DbGeometryHandler.cs index bd8582529..835ecfacb 100644 --- a/Dapper.EntityFramework/DbGeometryHandler.cs +++ b/Dapper.EntityFramework/DbGeometryHandler.cs @@ -27,17 +27,17 @@ public class DbGeometryHandler : SqlMapper.TypeHandler /// /// The parameter to configure. /// Parameter value. - public override void SetValue(IDbDataParameter parameter, DbGeometry value) + public override void SetValue(IDbDataParameter parameter, DbGeometry? value) { - object parsed = null; - if (value != null) + object? parsed = null; + if (value is not null) { parsed = SqlGeometry.STGeomFromWKB(new SqlBytes(value.AsBinary()), value.CoordinateSystemId); } parameter.Value = parsed ?? DBNull.Value; - if (parameter is SqlParameter) + if (parameter is SqlParameter sqlP) { - ((SqlParameter)parameter).UdtTypeName = "geometry"; + sqlP.UdtTypeName = "geometry"; } } @@ -46,12 +46,12 @@ public override void SetValue(IDbDataParameter parameter, DbGeometry value) /// /// The value from the database. /// The typed value. - public override DbGeometry Parse(object value) + public override DbGeometry? Parse(object? value) { - if (value == null || value is DBNull) return null; + if (value is null || value is DBNull) return null; if (value is SqlGeometry geo) { - return DbGeometry.FromBinary(geo.STAsBinary().Value); + return DbGeometry.FromBinary(geo.STAsBinary().Value, geo.STSrid.Value); } return DbGeometry.FromText(value.ToString()); } diff --git a/Dapper.EntityFramework/PublicAPI.Shipped.txt b/Dapper.EntityFramework/PublicAPI.Shipped.txt new file mode 100644 index 000000000..1f97829c4 --- /dev/null +++ b/Dapper.EntityFramework/PublicAPI.Shipped.txt @@ -0,0 +1,13 @@ +#nullable enable +Dapper.EntityFramework.DbGeographyHandler +Dapper.EntityFramework.DbGeographyHandler.DbGeographyHandler() -> void +Dapper.EntityFramework.DbGeometryHandler +Dapper.EntityFramework.DbGeometryHandler.DbGeometryHandler() -> void +Dapper.EntityFramework.Handlers +override Dapper.EntityFramework.DbGeographyHandler.Parse(object? value) -> System.Data.Entity.Spatial.DbGeography? +override Dapper.EntityFramework.DbGeographyHandler.SetValue(System.Data.IDbDataParameter! parameter, System.Data.Entity.Spatial.DbGeography? value) -> void +override Dapper.EntityFramework.DbGeometryHandler.Parse(object? value) -> System.Data.Entity.Spatial.DbGeometry? +override Dapper.EntityFramework.DbGeometryHandler.SetValue(System.Data.IDbDataParameter! parameter, System.Data.Entity.Spatial.DbGeometry? value) -> void +static Dapper.EntityFramework.Handlers.Register() -> void +static readonly Dapper.EntityFramework.DbGeographyHandler.Default -> Dapper.EntityFramework.DbGeographyHandler! +static readonly Dapper.EntityFramework.DbGeometryHandler.Default -> Dapper.EntityFramework.DbGeometryHandler! \ No newline at end of file diff --git a/Dapper.EntityFramework/PublicAPI.Unshipped.txt b/Dapper.EntityFramework/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper.EntityFramework/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper.ProviderTools/BulkCopy.cs b/Dapper.ProviderTools/BulkCopy.cs new file mode 100644 index 000000000..71f1b92c9 --- /dev/null +++ b/Dapper.ProviderTools/BulkCopy.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Concurrent; +using System.Data; +using System.Data.Common; +using System.Linq.Expressions; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Dapper.ProviderTools.Internal; + +namespace Dapper.ProviderTools +{ + /// + /// Provides provider-agnostic access to bulk-copy services + /// + public abstract class BulkCopy : IDisposable + { + /// + /// Attempt to create a BulkCopy instance for the connection provided + /// + public static BulkCopy? TryCreate(DbConnection connection) + { + if (connection is null) return null; + var type = connection.GetType(); + if (!s_bcpFactory.TryGetValue(type, out var func)) + { + s_bcpFactory[type] = func = CreateBcpFactory(type); + } + var obj = func?.Invoke(connection); + return DynamicBulkCopy.Create(obj); + } + + /// + /// Create a BulkCopy instance for the connection provided + /// + public static BulkCopy Create(DbConnection connection) + { + var bcp = TryCreate(connection); + if (bcp is null) + { + if (connection is null) throw new ArgumentNullException(nameof(connection)); + throw new NotSupportedException("Unable to create BulkCopy for " + connection.GetType().FullName); + } + return bcp; + } + + ///// + ///// Provide an external registration for a given connection type + ///// + //public static void Register(Type type, Func factory) + //{ + // throw new NotImplementedException(); + //} + + private static readonly ConcurrentDictionary?> s_bcpFactory + = new ConcurrentDictionary?>(); + + internal static Func? CreateBcpFactory(Type connectionType) + { + try + { + var match = Regex.Match(connectionType.Name, "^(.+)Connection$"); + if (match.Success) + { + var prefix = match.Groups[1].Value; + var bcpType = connectionType.Assembly.GetType($"{connectionType.Namespace}.{prefix}BulkCopy"); + if (bcpType is not null) + { + var ctor = bcpType.GetConstructor(new[] { connectionType }); + if (ctor is null) return null; + + var p = Expression.Parameter(typeof(DbConnection), "conn"); + var body = Expression.New(ctor, Expression.Convert(p, connectionType)); + return Expression.Lambda>(body, p).Compile(); + } + } + } + catch { } + return null; + } + + /// + /// Name of the destination table on the server. + /// + public abstract string DestinationTableName { get; set; } + /// + /// Write a set of data to the server + /// + public abstract void WriteToServer(DataTable source); + /// + /// Write a set of data to the server + /// + public abstract void WriteToServer(DataRow[] source); + /// + /// Write a set of data to the server + /// + public abstract void WriteToServer(IDataReader source); + /// + /// Write a set of data to the server + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public abstract Task WriteToServerAsync(DbDataReader source, CancellationToken cancellationToken = default); + /// + /// Write a set of data to the server + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public abstract Task WriteToServerAsync(DataTable source, CancellationToken cancellationToken = default); + /// + /// Write a set of data to the server + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public abstract Task WriteToServerAsync(DataRow[] source, CancellationToken cancellationToken = default); + /// + /// Add a mapping between two columns by name + /// + public abstract void AddColumnMapping(string sourceColumn, string destinationColumn); + /// + /// Add a mapping between two columns by position + /// + public abstract void AddColumnMapping(int sourceColumn, int destinationColumn); + /// + /// The underlying untyped object providing the bulk-copy service + /// + public abstract object Wrapped { get; } + + /// + /// Enables or disables streaming from a data-reader + /// + public bool EnableStreaming { get; set; } + /// + /// Number of rows in each batch + /// + public int BatchSize { get; set; } + /// + /// Number of seconds for the operation to complete before it times out. + /// + public int BulkCopyTimeout { get; set; } + + /// + /// Release any resources associated with this instance + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Release any resources associated with this instance + /// + protected abstract void Dispose(bool disposing); + } +} diff --git a/Dapper.ProviderTools/Dapper.ProviderTools.csproj b/Dapper.ProviderTools/Dapper.ProviderTools.csproj new file mode 100644 index 000000000..176887317 --- /dev/null +++ b/Dapper.ProviderTools/Dapper.ProviderTools.csproj @@ -0,0 +1,24 @@ + + + Dapper.ProviderTools + orm;sql;micro-orm + Dapper Provider Tools + Provider-agnostic ADO.NET helper utilities + Marc Gravell + net461;netstandard2.0;net8.0 + true + enable + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/Dapper.ProviderTools/DbConnectionExtensions.cs b/Dapper.ProviderTools/DbConnectionExtensions.cs new file mode 100644 index 000000000..5960fdef5 --- /dev/null +++ b/Dapper.ProviderTools/DbConnectionExtensions.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Concurrent; +using System.Data.Common; +using System.Linq.Expressions; +using System.Reflection; + +namespace Dapper.ProviderTools +{ + /// + /// Helper utilities for working with database connections + /// + public static class DbConnectionExtensions + { + /// + /// Attempt to get the client connection id for a given connection + /// + public static bool TryGetClientConnectionId(this DbConnection connection, out Guid clientConnectionId) + { + clientConnectionId = default; + return connection is not null && ByTypeHelpers.Get(connection.GetType()).TryGetClientConnectionId( + connection, out clientConnectionId); + } + + /// + /// Clear all pools associated with the provided connection type + /// + public static bool TryClearAllPools(this DbConnection connection) + => connection is not null && ByTypeHelpers.Get(connection.GetType()).TryClearAllPools(); + + /// + /// Clear the pools associated with the provided connection + /// + public static bool TryClearPool(this DbConnection connection) + => connection is not null && ByTypeHelpers.Get(connection.GetType()).TryClearPool(connection); + + private sealed class ByTypeHelpers + { + private static readonly ConcurrentDictionary s_byType + = new ConcurrentDictionary(); + private readonly Func? _getClientConnectionId; + + private readonly Action? _clearPool; + private readonly Action? _clearAllPools; + + public bool TryGetClientConnectionId(DbConnection connection, out Guid clientConnectionId) + { + if (_getClientConnectionId is null) + { + clientConnectionId = default; + return false; + } + clientConnectionId = _getClientConnectionId(connection); + return true; + } + + public bool TryClearPool(DbConnection connection) + { + if (_clearPool is null) return false; + _clearPool(connection); + return true; + } + + public bool TryClearAllPools() + { + if (_clearAllPools is null) return false; + _clearAllPools(); + return true; + } + + public static ByTypeHelpers Get(Type type) + { + if (!s_byType.TryGetValue(type, out var value)) + { + s_byType[type] = value = new ByTypeHelpers(type); + } + return value; + } + + private ByTypeHelpers(Type type) + { + _getClientConnectionId = TryGetInstanceProperty("ClientConnectionId", type); + + try + { + var clearAllPools = type.GetMethod("ClearAllPools", BindingFlags.Public | BindingFlags.Static, + null, Type.EmptyTypes, null); + if (clearAllPools is not null) + { + _clearAllPools = (Action)Delegate.CreateDelegate(typeof(Action), clearAllPools); + } + } + catch { } + + try + { + var clearPool = type.GetMethod("ClearPool", BindingFlags.Public | BindingFlags.Static, + null, new[] { type }, null); + if (clearPool is not null) + { + var p = Expression.Parameter(typeof(DbConnection), "connection"); + var body = Expression.Call(clearPool, Expression.Convert(p, type)); + var lambda = Expression.Lambda>(body, p); + _clearPool = lambda.Compile(); + } + } + catch { } + } + + private static Func? TryGetInstanceProperty(string name, Type type) + { + try + { + var prop = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + if (prop is null || !prop.CanRead) return null; + if (prop.PropertyType != typeof(T)) return null; + + var p = Expression.Parameter(typeof(DbConnection), "connection"); + var body = Expression.Property(Expression.Convert(p, type), prop); + var lambda = Expression.Lambda>(body, p); + return lambda.Compile(); + } + catch + { + return null; + } + } + } + } +} diff --git a/Dapper.ProviderTools/DbExceptionExtensions.cs b/Dapper.ProviderTools/DbExceptionExtensions.cs new file mode 100644 index 000000000..d1b6d8e2b --- /dev/null +++ b/Dapper.ProviderTools/DbExceptionExtensions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Concurrent; +using System.Data.Common; +using System.Linq.Expressions; +using System.Reflection; + +namespace Dapper.ProviderTools +{ + /// + /// Helper utilities for working with database exceptions + /// + public static class DbExceptionExtensions + { + /// + /// Indicates whether the provided exception has an integer Number property with the supplied value + /// + public static bool IsNumber(this DbException exception, int number) + => exception is not null && ByTypeHelpers.Get(exception.GetType()).IsNumber(exception, number); + + + private sealed class ByTypeHelpers + { + private static readonly ConcurrentDictionary s_byType + = new ConcurrentDictionary(); + private readonly Func? _getNumber; + + public bool IsNumber(DbException exception, int number) + => _getNumber is not null && _getNumber(exception) == number; + + public static ByTypeHelpers Get(Type type) + { + if (!s_byType.TryGetValue(type, out var value)) + { + s_byType[type] = value = new ByTypeHelpers(type); + } + return value; + } + + private ByTypeHelpers(Type type) + { + _getNumber = TryGetInstanceProperty("Number", type); + } + + private static Func? TryGetInstanceProperty(string name, Type type) + { + try + { + var prop = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + if (prop is null || !prop.CanRead) return null; + if (prop.PropertyType != typeof(T)) return null; + + var p = Expression.Parameter(typeof(DbException), "exception"); + var body = Expression.Property(Expression.Convert(p, type), prop); + var lambda = Expression.Lambda>(body, p); + return lambda.Compile(); + } + catch + { + return null; + } + } + } + } +} diff --git a/Dapper.ProviderTools/Internal/DynamicBulkCopy.cs b/Dapper.ProviderTools/Internal/DynamicBulkCopy.cs new file mode 100644 index 000000000..d8fd7b5d8 --- /dev/null +++ b/Dapper.ProviderTools/Internal/DynamicBulkCopy.cs @@ -0,0 +1,60 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +#nullable enable +namespace Dapper.ProviderTools.Internal +{ + internal sealed class DynamicBulkCopy : BulkCopy + { + internal static BulkCopy? Create(object? wrapped) + => wrapped is null ? null : new DynamicBulkCopy(wrapped); + + private DynamicBulkCopy(object wrapped) + => _wrapped = wrapped; + + private readonly dynamic _wrapped; + + public override string DestinationTableName + { + get => _wrapped.DestinationTableName; + set => _wrapped.DestinationTableName = value; + } + + public override object Wrapped => _wrapped; + + public override void AddColumnMapping(string sourceColumn, string destinationColumn) + => _wrapped.ColumnMappings.Add(sourceColumn, destinationColumn); + + public override void AddColumnMapping(int sourceColumn, int destinationColumn) + => _wrapped.ColumnMappings.Add(sourceColumn, destinationColumn); + + public override void WriteToServer(DataTable source) + => _wrapped.WriteToServer(source); + public override void WriteToServer(DataRow[] source) + => _wrapped.WriteToServer(source); + + public override void WriteToServer(IDataReader source) + => _wrapped.WriteToServer(source); + + public override Task WriteToServerAsync(DbDataReader source, CancellationToken cancellationToken) + => _wrapped.WriteToServer(source, cancellationToken); + + public override Task WriteToServerAsync(DataTable source, CancellationToken cancellationToken) + => _wrapped.WriteToServer(source, cancellationToken); + public override Task WriteToServerAsync(DataRow[] source, CancellationToken cancellationToken) + => _wrapped.WriteToServer(source, cancellationToken); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_wrapped is IDisposable d) + { + try { d.Dispose(); } catch { } + } + } + } + } +} diff --git a/Dapper.ProviderTools/PublicAPI.Shipped.txt b/Dapper.ProviderTools/PublicAPI.Shipped.txt new file mode 100644 index 000000000..a8329b713 --- /dev/null +++ b/Dapper.ProviderTools/PublicAPI.Shipped.txt @@ -0,0 +1,30 @@ +#nullable enable +abstract Dapper.ProviderTools.BulkCopy.AddColumnMapping(int sourceColumn, int destinationColumn) -> void +abstract Dapper.ProviderTools.BulkCopy.AddColumnMapping(string! sourceColumn, string! destinationColumn) -> void +abstract Dapper.ProviderTools.BulkCopy.DestinationTableName.get -> string! +abstract Dapper.ProviderTools.BulkCopy.DestinationTableName.set -> void +abstract Dapper.ProviderTools.BulkCopy.Dispose(bool disposing) -> void +abstract Dapper.ProviderTools.BulkCopy.Wrapped.get -> object! +abstract Dapper.ProviderTools.BulkCopy.WriteToServer(System.Data.DataRow![]! source) -> void +abstract Dapper.ProviderTools.BulkCopy.WriteToServer(System.Data.DataTable! source) -> void +abstract Dapper.ProviderTools.BulkCopy.WriteToServer(System.Data.IDataReader! source) -> void +abstract Dapper.ProviderTools.BulkCopy.WriteToServerAsync(System.Data.Common.DbDataReader! source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +abstract Dapper.ProviderTools.BulkCopy.WriteToServerAsync(System.Data.DataRow![]! source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +abstract Dapper.ProviderTools.BulkCopy.WriteToServerAsync(System.Data.DataTable! source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Dapper.ProviderTools.BulkCopy +Dapper.ProviderTools.BulkCopy.BatchSize.get -> int +Dapper.ProviderTools.BulkCopy.BatchSize.set -> void +Dapper.ProviderTools.BulkCopy.BulkCopy() -> void +Dapper.ProviderTools.BulkCopy.BulkCopyTimeout.get -> int +Dapper.ProviderTools.BulkCopy.BulkCopyTimeout.set -> void +Dapper.ProviderTools.BulkCopy.Dispose() -> void +Dapper.ProviderTools.BulkCopy.EnableStreaming.get -> bool +Dapper.ProviderTools.BulkCopy.EnableStreaming.set -> void +Dapper.ProviderTools.DbConnectionExtensions +Dapper.ProviderTools.DbExceptionExtensions +static Dapper.ProviderTools.BulkCopy.Create(System.Data.Common.DbConnection! connection) -> Dapper.ProviderTools.BulkCopy! +static Dapper.ProviderTools.BulkCopy.TryCreate(System.Data.Common.DbConnection! connection) -> Dapper.ProviderTools.BulkCopy? +static Dapper.ProviderTools.DbConnectionExtensions.TryClearAllPools(this System.Data.Common.DbConnection! connection) -> bool +static Dapper.ProviderTools.DbConnectionExtensions.TryClearPool(this System.Data.Common.DbConnection! connection) -> bool +static Dapper.ProviderTools.DbConnectionExtensions.TryGetClientConnectionId(this System.Data.Common.DbConnection! connection, out System.Guid clientConnectionId) -> bool +static Dapper.ProviderTools.DbExceptionExtensions.IsNumber(this System.Data.Common.DbException! exception, int number) -> bool \ No newline at end of file diff --git a/Dapper.ProviderTools/PublicAPI.Unshipped.txt b/Dapper.ProviderTools/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper.ProviderTools/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper.Rainbow/Dapper.Rainbow.csproj b/Dapper.Rainbow/Dapper.Rainbow.csproj index 5712fff9a..7d5f3ff5a 100644 --- a/Dapper.Rainbow/Dapper.Rainbow.csproj +++ b/Dapper.Rainbow/Dapper.Rainbow.csproj @@ -6,24 +6,17 @@ Trivial micro-orm implemented on Dapper, provides with CRUD helpers. Sam Saffron 2017 Sam Saffron - net451;netstandard1.3 + net461;netstandard2.0;net5.0 false - - - - - - - - - + + - - + + \ No newline at end of file diff --git a/Dapper.Rainbow/Database.Async.cs b/Dapper.Rainbow/Database.Async.cs index d247e9e18..e1a78c47b 100644 --- a/Dapper.Rainbow/Database.Async.cs +++ b/Dapper.Rainbow/Database.Async.cs @@ -34,7 +34,7 @@ public partial class Table /// /// The Id of the record to update. /// The new record. - /// The number of affeced rows. + /// The number of affected rows. public Task UpdateAsync(TId id, dynamic data) { List paramNames = GetParamNames((object)data); @@ -88,7 +88,7 @@ public Task> AllAsync() => /// The parameters to use. /// The number of rows affected. public Task ExecuteAsync(string sql, dynamic param = null) => - _connection.ExecuteAsync(sql, param as object, _transaction, _commandTimeout); + _connection.ExecuteAsync(sql, param as object, Transaction, _commandTimeout); /// /// Asynchronously queries the current database. @@ -98,7 +98,7 @@ public Task ExecuteAsync(string sql, dynamic param = null) => /// The parameters to use. /// An enumerable of for the rows fetched. public Task> QueryAsync(string sql, dynamic param = null) => - _connection.QueryAsync(sql, param as object, _transaction, _commandTimeout); + _connection.QueryAsync(sql, param as object, Transaction, _commandTimeout); /// /// Asynchronously queries the current database for a single record. @@ -108,10 +108,10 @@ public Task> QueryAsync(string sql, dynamic param = null) => /// The parameters to use. /// An enumerable of for the rows fetched. public Task QueryFirstOrDefaultAsync(string sql, dynamic param = null) => - _connection.QueryFirstOrDefaultAsync(sql, param as object, _transaction, _commandTimeout); + _connection.QueryFirstOrDefaultAsync(sql, param as object, Transaction, _commandTimeout); /// - /// Perform a asynchronous multi-mapping query with 2 input types. + /// Perform an asynchronous multi-mapping query with 2 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -129,7 +129,7 @@ public Task> QueryAsync(string sq _connection.QueryAsync(sql, map, param as object, transaction, buffered, splitOn, commandTimeout); /// - /// Perform a asynchronous multi-mapping query with 3 input types. + /// Perform an asynchronous multi-mapping query with 3 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -148,7 +148,7 @@ public Task> QueryAsync(s _connection.QueryAsync(sql, map, param as object, transaction, buffered, splitOn, commandTimeout); /// - /// Perform a asynchronous multi-mapping query with 4 input types. + /// Perform an asynchronous multi-mapping query with 4 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -168,7 +168,7 @@ public Task> QueryAsync - /// Perform a asynchronous multi-mapping query with 5 input types. + /// Perform an asynchronous multi-mapping query with 5 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -189,13 +189,13 @@ public Task> QueryAsync - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The SQL to execute. /// The parameters to use. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> public Task> QueryAsync(string sql, dynamic param = null) => - _connection.QueryAsync(sql, param as object, _transaction); + _connection.QueryAsync(sql, param as object, Transaction); /// /// Execute a command that returns multiple result sets, and access each in turn. diff --git a/Dapper.Rainbow/Database.cs b/Dapper.Rainbow/Database.cs index 7bd0f91c4..74ab9ba2f 100644 --- a/Dapper.Rainbow/Database.cs +++ b/Dapper.Rainbow/Database.cs @@ -45,7 +45,7 @@ public string TableName { get { - tableName = tableName ?? database.DetermineTableName(likelyTableName); + tableName ??= database.DetermineTableName(likelyTableName); return tableName; } } @@ -142,7 +142,9 @@ internal static List GetParamNames(object o) foreach (var prop in o.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetGetMethod(false) != null)) { var attribs = prop.GetCustomAttributes(typeof(IgnorePropertyAttribute), true); +#pragma warning disable IDE0019 // Use pattern matching - complex enough here var attr = attribs.FirstOrDefault() as IgnorePropertyAttribute; +#pragma warning restore IDE0019 // Use pattern matching if (attr == null || (!attr.Value)) { paramNames.Add(prop.Name); @@ -173,7 +175,16 @@ public Table(Database database, string likelyTableName) private DbConnection _connection; private int _commandTimeout; - private DbTransaction _transaction; + + /// + /// Get access to the underlying transaction + /// + public DbTransaction Transaction { get; private set; } + + /// + /// Get underlying database connection. + /// + public DbConnection Connection => _connection; /// /// Initializes the database. @@ -194,7 +205,7 @@ internal void InitDatabase(DbConnection connection, int commandTimeout) { _connection = connection; _commandTimeout = commandTimeout; - tableConstructor = tableConstructor ?? CreateTableConstructorForTable(); + tableConstructor ??= CreateTableConstructorForTable(); tableConstructor(this as TDatabase); } @@ -208,9 +219,11 @@ internal virtual Action CreateTableConstructorForTable() /// Begins a transaction in this database. /// /// The isolation level to use. - public void BeginTransaction(IsolationLevel isolation = IsolationLevel.ReadCommitted) + /// The transaction created + public DbTransaction BeginTransaction(IsolationLevel isolation = IsolationLevel.ReadCommitted) { - _transaction = _connection.BeginTransaction(isolation); + Transaction = _connection.BeginTransaction(isolation); + return Transaction; } /// @@ -218,8 +231,8 @@ public void BeginTransaction(IsolationLevel isolation = IsolationLevel.ReadCommi /// public void CommitTransaction() { - _transaction.Commit(); - _transaction = null; + Transaction.Commit(); + Transaction = null; } /// @@ -227,8 +240,8 @@ public void CommitTransaction() /// public void RollbackTransaction() { - _transaction.Rollback(); - _transaction = null; + Transaction.Rollback(); + Transaction = null; } /// @@ -252,7 +265,7 @@ protected Action CreateTableConstructor(params Type[] tableTypes) var il = dm.GetILGenerator(); var setters = GetType().GetProperties() - .Where(p => p.PropertyType.IsGenericType() && tableTypes.Contains(p.PropertyType.GetGenericTypeDefinition())) + .Where(p => p.PropertyType.IsGenericType && tableTypes.Contains(p.PropertyType.GetGenericTypeDefinition())) .Select(p => Tuple.Create( p.GetSetMethod(true), p.PropertyType.GetConstructor(new[] { typeof(TDatabase), typeof(string) }), @@ -315,7 +328,7 @@ private bool TableExists(string name) name = name.Replace("[", ""); name = name.Replace("]", ""); - if (name.Contains(".")) + if (name.IndexOf('.') > 0) { var parts = name.Split('.'); if (parts.Length == 2) @@ -329,7 +342,7 @@ private bool TableExists(string name) if (!string.IsNullOrEmpty(schemaName)) builder.Append("TABLE_SCHEMA = @schemaName AND "); builder.Append("TABLE_NAME = @name"); - return _connection.Query(builder.ToString(), new { schemaName, name }, _transaction).Count() == 1; + return _connection.Query(builder.ToString(), new { schemaName, name }, Transaction).Count() == 1; } /// @@ -339,7 +352,7 @@ private bool TableExists(string name) /// The parameters to use. /// The number of rows affected. public int Execute(string sql, dynamic param = null) => - _connection.Execute(sql, param as object, _transaction, _commandTimeout); + _connection.Execute(sql, param as object, Transaction, _commandTimeout); /// /// Queries the current database. @@ -350,7 +363,7 @@ public int Execute(string sql, dynamic param = null) => /// Whether to buffer the results. /// An enumerable of for the rows fetched. public IEnumerable Query(string sql, dynamic param = null, bool buffered = true) => - _connection.Query(sql, param as object, _transaction, buffered, _commandTimeout); + _connection.Query(sql, param as object, Transaction, buffered, _commandTimeout); /// /// Queries the current database for a single record. @@ -360,7 +373,7 @@ public IEnumerable Query(string sql, dynamic param = null, bool buffered = /// The parameters to use. /// An enumerable of for the rows fetched. public T QueryFirstOrDefault(string sql, dynamic param = null) => - _connection.QueryFirstOrDefault(sql, param as object, _transaction, _commandTimeout); + _connection.QueryFirstOrDefault(sql, param as object, Transaction, _commandTimeout); /// /// Perform a multi-mapping query with 2 input types. @@ -448,7 +461,7 @@ public IEnumerable QueryWhether the results should be buffered in memory. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> public IEnumerable Query(string sql, dynamic param = null, bool buffered = true) => - _connection.Query(sql, param as object, _transaction, buffered); + _connection.Query(sql, param as object, Transaction, buffered); /// /// Execute a command that returns multiple result sets, and access each in turn. @@ -464,15 +477,16 @@ public SqlMapper.GridReader QueryMultiple(string sql, dynamic param = null, IDbT /// /// Disposes the current database, rolling back current transactions. /// - public void Dispose() + public virtual void Dispose() { - if (_connection.State != ConnectionState.Closed) + var connection = _connection; + if (connection.State != ConnectionState.Closed) { - _transaction?.Rollback(); - - _connection.Close(); _connection = null; + Transaction = null; + connection?.Close(); } + GC.SuppressFinalize(this); } } } diff --git a/Dapper.Rainbow/Snapshotter.cs b/Dapper.Rainbow/Snapshotter.cs index ef6f8b0d5..c4e44a2e9 100644 --- a/Dapper.Rainbow/Snapshotter.cs +++ b/Dapper.Rainbow/Snapshotter.cs @@ -12,7 +12,7 @@ namespace Dapper public static class Snapshotter { /// - /// Starts the snapshot of an objec by making a copy of the current state. + /// Starts the snapshot of an object by making a copy of its current state. /// /// The type of object to snapshot. /// The object to snapshot. @@ -69,14 +69,14 @@ public DynamicParameters Diff() private static T Clone(T myObject) { - cloner = cloner ?? GenerateCloner(); + cloner ??= GenerateCloner(); return cloner(myObject); } private static DynamicParameters Diff(T original, T current) { var dm = new DynamicParameters(); - differ = differ ?? GenerateDiffer(); + differ ??= GenerateDiffer(); foreach (var pair in differ(original, current)) { dm.Add(pair.Name, pair.NewValue); @@ -91,15 +91,15 @@ private static List RelevantProperties() p.GetSetMethod(true) != null && p.GetGetMethod(true) != null && (p.PropertyType == typeof(string) - || p.PropertyType.IsValueType() - || (p.PropertyType.IsGenericType() && p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))) + || p.PropertyType.IsValueType + || (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))) ).ToList(); } private static bool AreEqual(U first, U second) { - if (EqualityComparer.Default.Equals(first, default(U)) && EqualityComparer.Default.Equals(second, default(U))) return true; - if (EqualityComparer.Default.Equals(first, default(U))) return false; + if (EqualityComparer.Default.Equals(first, default) && EqualityComparer.Default.Equals(second, default)) return true; + if (EqualityComparer.Default.Equals(first, default)) return false; return first.Equals(second); } @@ -109,13 +109,13 @@ private static Func> GenerateDiffer() var il = dm.GetILGenerator(); // change list - il.DeclareLocal(typeof(List)); - il.DeclareLocal(typeof(Change)); - il.DeclareLocal(typeof(object)); // boxed change + var list = il.DeclareLocal(typeof(List)); + var change = il.DeclareLocal(typeof(Change)); + var boxed = il.DeclareLocal(typeof(object)); // boxed change il.Emit(OpCodes.Newobj, typeof(List).GetConstructor(Type.EmptyTypes)); // [list] - il.Emit(OpCodes.Stloc_0); + il.Emit(OpCodes.Stloc, list); foreach (var prop in RelevantProperties()) { @@ -138,7 +138,7 @@ private static Func> GenerateDiffer() // [original prop val, current prop val, current prop val boxed] } - il.Emit(OpCodes.Stloc_2); + il.Emit(OpCodes.Stloc, boxed); // [original prop val, current prop val] il.EmitCall(OpCodes.Call, typeof(Snapshot).GetMethod(nameof(AreEqual), BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(new Type[] { prop.PropertyType }), null); @@ -153,7 +153,7 @@ private static Func> GenerateDiffer() il.Emit(OpCodes.Dup); // [change,change] - il.Emit(OpCodes.Stloc_1); + il.Emit(OpCodes.Stloc, change); // [change] il.Emit(OpCodes.Ldstr, prop.Name); @@ -161,18 +161,18 @@ private static Func> GenerateDiffer() il.Emit(OpCodes.Callvirt, typeof(Change).GetMethod("set_Name")); // [] - il.Emit(OpCodes.Ldloc_1); + il.Emit(OpCodes.Ldloc, change); // [change] - il.Emit(OpCodes.Ldloc_2); + il.Emit(OpCodes.Ldloc, boxed); // [change, boxed] il.Emit(OpCodes.Callvirt, typeof(Change).GetMethod("set_NewValue")); // [] - il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ldloc, list); // [change list] - il.Emit(OpCodes.Ldloc_1); + il.Emit(OpCodes.Ldloc, change); // [change list, change] il.Emit(OpCodes.Callvirt, typeof(List).GetMethod("Add")); // [] @@ -180,7 +180,7 @@ private static Func> GenerateDiffer() il.MarkLabel(skip); } - il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ldloc, list); // [change list] il.Emit(OpCodes.Ret); @@ -191,18 +191,18 @@ private static Func> GenerateDiffer() private static Func GenerateCloner() { var dm = new DynamicMethod("DoClone", typeof(T), new Type[] { typeof(T) }, true); - var ctor = typeof(T).GetConstructor(new Type[] { }); + var ctor = typeof(T).GetConstructor(Type.EmptyTypes); var il = dm.GetILGenerator(); - il.DeclareLocal(typeof(T)); + var typed = il.DeclareLocal(typeof(T)); il.Emit(OpCodes.Newobj, ctor); - il.Emit(OpCodes.Stloc_0); + il.Emit(OpCodes.Stloc, typed); foreach (var prop in RelevantProperties()) { - il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ldloc, typed); // [clone] il.Emit(OpCodes.Ldarg_0); // [clone, source] @@ -213,7 +213,7 @@ private static Func GenerateCloner() } // Load new constructed obj on eval stack -> 1 item on stack - il.Emit(OpCodes.Ldloc_0); + il.Emit(OpCodes.Ldloc, typed); // Return constructed object. --> 0 items on stack il.Emit(OpCodes.Ret); diff --git a/Dapper.Rainbow/readme.md b/Dapper.Rainbow/readme.md new file mode 100644 index 000000000..c1f34b67a --- /dev/null +++ b/Dapper.Rainbow/readme.md @@ -0,0 +1,118 @@ +# Using Dapper.Rainbow in C# for CRUD Operations + +This guide outlines how to use `Dapper.Rainbow` in C# for CRUD operations. + +## 1. Setting Up + +Add Dapper and Dapper.Rainbow to your project via NuGet: + +```powershell +Install-Package Dapper -Version x.x.x +Install-Package Dapper.Rainbow -Version x.x.x +``` + +*Replace `x.x.x` with the latest version numbers.* + +## 2. Database Setup and Requirements + +For `Dapper.Rainbow` to function correctly, ensure each table has a primary key column named `Id`. + +Example `Users` table schema: + +```sql +CREATE TABLE Users ( + Id INT IDENTITY(1,1) PRIMARY KEY, + Name VARCHAR(100), + Email VARCHAR(100) +); +``` + +## 3. Establishing Database Connection + +Open a connection to your database: + +```csharp +using System.Data.SqlClient; + +var connectionString = "your_connection_string_here"; +using var connection = new SqlConnection(connectionString); +connection.Open(); // Open the connection +``` + +## 4. Defining Your Database Context + +Define a class for your database context: + +```csharp +using Dapper; +using System.Data; + +public class MyDatabase : Database +{ + public Table Users { get; set; } +} + +public class User +{ + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } +} +``` + +## 5. Performing CRUD Operations + +### Insert + +```csharp +var db = new MyDatabase { Connection = connection }; +var newUser = new User { Name = "John Doe", Email = "john.doe@example.com" }; +var insertedUser = db.Users.Insert(newUser); +``` + +### Select + +Fetch users by ID or all users: + +```csharp +var user = db.Users.Get(id); // Single user by ID +var users = connection.Query("SELECT * FROM Users"); // All users +``` + +### Update + +```csharp +var userToUpdate = db.Users.Get(id); +userToUpdate.Email = "new.email@example.com"; +db.Users.Update(userToUpdate); +``` + +### Delete + +```csharp +db.Users.Delete(id); +``` + +## 6. Working with Foreign Keys + +Example schema for a `Posts` table with a foreign key to `Users`: + +```sql +CREATE TABLE Posts ( + Id INT IDENTITY(1,1) PRIMARY KEY, + UserId INT, + Content VARCHAR(255), + FOREIGN KEY (UserId) REFERENCES Users(Id) +); +``` + +Inserting a parent (`User`) and a child (`Post`) row: + +```csharp +var newUser = new User { Name = "Jane Doe", Email = "jane.doe@example.com" }; +var userId = db.Users.Insert(newUser); + +var newPost = new Post { UserId = userId, Content = "Hello, World!" }; +db.Connection.Insert(newPost); // Using Dapper for the child table +``` + diff --git a/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj b/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj index 173c24d0c..a455a5941 100644 --- a/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj +++ b/Dapper.SqlBuilder/Dapper.SqlBuilder.csproj @@ -5,21 +5,24 @@ Dapper SqlBuilder component The Dapper SqlBuilder component, for building SQL queries dynamically. Sam Saffron, Johan Danforth - net451;netstandard1.3 - + net461;netstandard2.0;net8.0 false false + enable + + + all + runtime; build; native; contentfiles; analyzers + - - - - - + + + - - + + \ No newline at end of file diff --git a/Dapper.SqlBuilder/PublicAPI.Shipped.txt b/Dapper.SqlBuilder/PublicAPI.Shipped.txt new file mode 100644 index 000000000..9baefbb74 --- /dev/null +++ b/Dapper.SqlBuilder/PublicAPI.Shipped.txt @@ -0,0 +1,22 @@ +#nullable enable +Dapper.SqlBuilder +Dapper.SqlBuilder.AddClause(string! name, string! sql, object? parameters, string! joiner, string! prefix = "", string! postfix = "", bool isInclusive = false) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.AddParameters(dynamic! parameters) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.AddTemplate(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder.Template! +Dapper.SqlBuilder.GroupBy(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.Having(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.InnerJoin(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.Intersect(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.Join(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.LeftJoin(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.OrderBy(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.OrWhere(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.RightJoin(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.Select(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.Set(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! +Dapper.SqlBuilder.SqlBuilder() -> void +Dapper.SqlBuilder.Template +Dapper.SqlBuilder.Template.Parameters.get -> object? +Dapper.SqlBuilder.Template.RawSql.get -> string! +Dapper.SqlBuilder.Template.Template(Dapper.SqlBuilder! builder, string! sql, dynamic? parameters) -> void +Dapper.SqlBuilder.Where(string! sql, dynamic? parameters = null) -> Dapper.SqlBuilder! \ No newline at end of file diff --git a/Dapper.SqlBuilder/PublicAPI.Unshipped.txt b/Dapper.SqlBuilder/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper.SqlBuilder/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper.SqlBuilder/Readme.md b/Dapper.SqlBuilder/Readme.md new file mode 100644 index 000000000..5a0968f9f --- /dev/null +++ b/Dapper.SqlBuilder/Readme.md @@ -0,0 +1,128 @@ +Dapper.SqlBuilder - a simple sql formatter for .Net +======================================== +[![Build status](https://ci.appveyor.com/api/projects/status/1w448i6nfxd14w75?svg=true)](https://ci.appveyor.com/project/StackExchange/dapper-SqlBuilder) + +Packages +-------- + +MyGet Pre-release feed: https://www.myget.org/gallery/dapper + +| Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| [Dapper.SqlBuilder](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder](https://img.shields.io/nuget/v/Dapper.SqlBuilder.svg)](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder](https://img.shields.io/nuget/vpre/Dapper.SqlBuilder.svg)](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder](https://img.shields.io/nuget/dt/Dapper.SqlBuilder.svg)](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.SqlBuilder.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.SqlBuilder) | + +Features +-------- + +Dapper.SqlBuilder contains a number of helper methods for generating sql. + +The list of extension methods in Dapper.SqlBuilder right now are: + +```csharp +SqlBuilder AddParameters(dynamic parameters); +SqlBuilder Select(string sql, dynamic parameters = null); +SqlBuilder Where(string sql, dynamic parameters = null); +SqlBuilder OrWhere(string sql, dynamic parameters = null); +SqlBuilder OrderBy(string sql, dynamic parameters = null); +SqlBuilder GroupBy(string sql, dynamic parameters = null); +SqlBuilder Having(string sql, dynamic parameters = null); +SqlBuilder Set(string sql, dynamic parameters = null); +SqlBuilder Join(string sql, dynamic parameters = null); +SqlBuilder InnerJoin(string sql, dynamic parameters = null); +SqlBuilder LeftJoin(string sql, dynamic parameters = null); +SqlBuilder RightJoin(string sql, dynamic parameters = null); +SqlBuilder Intersect(string sql, dynamic parameters = null); +``` + + +Template +-------- + +SqlBuilder allows you to generate N SQL templates from a composed query, it can easily format sql when you are attaching parameters and how, e.g: +```csharp +var builder = new SqlBuilder() + .Where("a = @a", new { a = 1 }) + .Where("b = @b", new { b = 2 }) + .OrderBy("a") + .OrderBy("b"); +var counter = builder.AddTemplate("select count(*) from table /**where**/"); +var selector = builder.AddTemplate("select * from table /**where**/ /**orderby**/"); +var count = cnn.Query(counter.RawSql, counter.Parameters).Single(); +var rows = cnn.Query(selector.RawSql, selector.Parameters); +``` + +it's same as +```csharp +var count = cnn.Query("select count(*) from table where a = @a and b = @b", new { a = 1, b = 1 }); +var rows = cnn.Query("select * from table where a = @a and b = @b order by a, b", new { a = 1, b = 1 }); +``` + +Dynamic Filter Paging Example +---------- + +```csharp +var builder = new SqlBuilder(); +var selectTemplate = builder.AddTemplate(@"select X.* from ( + select us.*, ROW_NUMBER() OVER (/**orderby**/) AS RowNumber + from Users us + /**where**/ + ) as X + where RowNumber between @start and @finish", new { start, finish }); +var countTemplate = builder.AddTemplate(@"select count(*) from Users /**where**/"); + +if (userId.HasValue()) + builder.Where($"t.userId = @{nameof(userId)}", new { userId }); +if (isCancel) + builder.Where($"t.isCancel = @{nameof(isCancel)}", new { isCancel }); + +builder.OrderBy(string.Format("t.id {0}", orderDesc ? "desc" : "asc")); + +var users = conn.Query(selectTemplate.RawSql, selectTemplate.Parameters); +var count = conn.ExecuteScalar(countTemplate.RawSql, countTemplate.Parameters); +//..etc.. +``` + +Limitations and caveats +-------- + +### Combining the Where and OrWhere methods + +The OrWhere method currently groups all `and` and `or` clauses by type, +then join the groups with `and` or `or` depending on the first call. +This may result in possibly unexpected outcomes. +See also [issue 647](https://github.com/DapperLib/Dapper/issues/647). + +#### Example Where first + +When providing the following clauses +```csharp +sql.Where("a = @a1"); +sql.OrWhere("b = @b1"); +sql.Where("a = @a2"); +sql.OrWhere("b = @b2"); +``` + +SqlBuilder will generate sql +```sql +a = @a1 AND a = @a2 AND ( b = @b1 OR b = @b2 ) +``` + +and not say +```sql +a = @a1 OR b = @b1 AND a = @a2 OR b = @b2 +``` + +#### Example OrWhere first + +When providing the following clauses +```csharp +sql.OrWhere("b = @b1"); +sql.Where("a = @a1"); +sql.OrWhere("b = @b2"); +sql.Where("a = @a2"); +``` + +SqlBuilder will generate sql +```sql +a = @a1 OR a = @a2 OR ( b = @b1 OR b = @b2 ) +``` diff --git a/Dapper.SqlBuilder/SqlBuilder.cs b/Dapper.SqlBuilder/SqlBuilder.cs index c83097f42..cb45d0582 100644 --- a/Dapper.SqlBuilder/SqlBuilder.cs +++ b/Dapper.SqlBuilder/SqlBuilder.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; @@ -9,14 +10,20 @@ public class SqlBuilder private readonly Dictionary _data = new Dictionary(); private int _seq; - private class Clause + private sealed class Clause { - public string Sql { get; set; } - public object Parameters { get; set; } - public bool IsInclusive { get; set; } + public Clause(string sql, object? parameters, bool isInclusive) + { + Sql = sql; + Parameters = parameters; + IsInclusive = isInclusive; + } + public string Sql { get; } + public object? Parameters { get; } + public bool IsInclusive { get; } } - private class Clauses : List + private sealed class Clauses : List { private readonly string _joiner, _prefix, _postfix; @@ -29,11 +36,9 @@ public Clauses(string joiner, string prefix = "", string postfix = "") public string ResolveClauses(DynamicParameters p) { - foreach (var item in this) - { - p.AddDynamicParams(item.Parameters); - } - return this.Any(a => a.IsInclusive) + ForEach(item => p.AddDynamicParams(item.Parameters)); + + return Exists(a => a.IsInclusive) ? _prefix + string.Join(_joiner, this.Where(a => !a.IsInclusive) @@ -52,10 +57,10 @@ public class Template { private readonly string _sql; private readonly SqlBuilder _builder; - private readonly object _initParams; + private readonly object? _initParams; private int _dataSeq = -1; // Unresolved - public Template(SqlBuilder builder, string sql, dynamic parameters) + public Template(SqlBuilder builder, string sql, dynamic? parameters) { _initParams = parameters; _sql = sql; @@ -85,69 +90,73 @@ private void ResolveSql() } } - private string rawSql; - private object parameters; + private string? rawSql; + private object? parameters; public string RawSql { - get { ResolveSql(); return rawSql; } + get { ResolveSql(); return rawSql!; } } - public object Parameters + public object? Parameters { get { ResolveSql(); return parameters; } } } - public Template AddTemplate(string sql, dynamic parameters = null) => - new Template(this, sql, parameters); + public Template AddTemplate(string sql, dynamic? parameters = null) => + new Template(this, sql, (object?)parameters); - protected SqlBuilder AddClause(string name, string sql, object parameters, string joiner, string prefix = "", string postfix = "", bool isInclusive = false) + protected SqlBuilder AddClause(string name, string sql, object? parameters, string joiner, string prefix = "", string postfix = "", bool isInclusive = false) { - if (!_data.TryGetValue(name, out Clauses clauses)) + if (!_data.TryGetValue(name, out var clauses)) { clauses = new Clauses(joiner, prefix, postfix); _data[name] = clauses; } - clauses.Add(new Clause { Sql = sql, Parameters = parameters, IsInclusive = isInclusive }); + clauses.Add(new Clause(sql, parameters, isInclusive)); _seq++; return this; } - public SqlBuilder Intersect(string sql, dynamic parameters = null) => - AddClause("intersect", sql, parameters, "\nINTERSECT\n ", "\n ", "\n", false); + public SqlBuilder Intersect(string sql, dynamic? parameters = null) => + AddClause("intersect", sql, (object?)parameters, "\nINTERSECT\n ", "\n ", "\n", false); - public SqlBuilder InnerJoin(string sql, dynamic parameters = null) => - AddClause("innerjoin", sql, parameters, "\nINNER JOIN ", "\nINNER JOIN ", "\n", false); + public SqlBuilder InnerJoin(string sql, dynamic? parameters = null) => + AddClause("innerjoin", sql, (object?)parameters, "\nINNER JOIN ", "\nINNER JOIN ", "\n", false); - public SqlBuilder LeftJoin(string sql, dynamic parameters = null) => - AddClause("leftjoin", sql, parameters, "\nLEFT JOIN ", "\nLEFT JOIN ", "\n", false); + public SqlBuilder LeftJoin(string sql, dynamic? parameters = null) => + AddClause("leftjoin", sql, (object?)parameters, "\nLEFT JOIN ", "\nLEFT JOIN ", "\n", false); - public SqlBuilder RightJoin(string sql, dynamic parameters = null) => - AddClause("rightjoin", sql, parameters, "\nRIGHT JOIN ", "\nRIGHT JOIN ", "\n", false); + public SqlBuilder RightJoin(string sql, dynamic? parameters = null) => + AddClause("rightjoin", sql, (object?)parameters, "\nRIGHT JOIN ", "\nRIGHT JOIN ", "\n", false); - public SqlBuilder Where(string sql, dynamic parameters = null) => - AddClause("where", sql, parameters, " AND ", "WHERE ", "\n", false); + public SqlBuilder Where(string sql, dynamic? parameters = null) => + AddClause("where", sql, (object?)parameters, " AND ", "WHERE ", "\n", false); - public SqlBuilder OrWhere(string sql, dynamic parameters = null) => - AddClause("where", sql, parameters, " OR ", "WHERE ", "\n", true); + public SqlBuilder OrWhere(string sql, dynamic? parameters = null) => + AddClause("where", sql, (object?)parameters, " OR ", "WHERE ", "\n", true); - public SqlBuilder OrderBy(string sql, dynamic parameters = null) => - AddClause("orderby", sql, parameters, " , ", "ORDER BY ", "\n", false); + public SqlBuilder OrderBy(string sql, dynamic? parameters = null) => + AddClause("orderby", sql, (object?)parameters, " , ", "ORDER BY ", "\n", false); - public SqlBuilder Select(string sql, dynamic parameters = null) => - AddClause("select", sql, parameters, " , ", "", "\n", false); + public SqlBuilder Select(string sql, dynamic? parameters = null) => + AddClause("select", sql, (object?)parameters, " , ", "", "\n", false); public SqlBuilder AddParameters(dynamic parameters) => - AddClause("--parameters", "", parameters, "", "", "", false); + AddClause("--parameters", "", (object?)parameters, "", "", "", false); + + public SqlBuilder Join(string sql, dynamic? parameters = null) => + AddClause("join", sql, (object?)parameters, "\nJOIN ", "\nJOIN ", "\n", false); + + public SqlBuilder GroupBy(string sql, dynamic? parameters = null) => + AddClause("groupby", sql, (object?)parameters, " , ", "\nGROUP BY ", "\n", false); - public SqlBuilder Join(string sql, dynamic parameters = null) => - AddClause("join", sql, parameters, "\nJOIN ", "\nJOIN ", "\n", false); + public SqlBuilder Having(string sql, dynamic? parameters = null) => + AddClause("having", sql, (object?)parameters, "\nAND ", "HAVING ", "\n", false); - public SqlBuilder GroupBy(string sql, dynamic parameters = null) => - AddClause("groupby", sql, parameters, " , ", "\nGROUP BY ", "\n", false); + public SqlBuilder Set(string sql, dynamic? parameters = null) => + AddClause("set", sql, (object?)parameters, " , ", "SET ", "\n", false); - public SqlBuilder Having(string sql, dynamic parameters = null) => - AddClause("having", sql, parameters, "\nAND ", "HAVING ", "\n", false); } } diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj index 22d0a419e..705385fd6 100644 --- a/Dapper.StrongName/Dapper.StrongName.csproj +++ b/Dapper.StrongName/Dapper.StrongName.csproj @@ -3,33 +3,28 @@ Dapper.StrongName orm;sql;micro-orm Dapper (Strong Named) - A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. + A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net451;netstandard1.3;netstandard2.0 + net461;netstandard2.0;net8.0;net10.0 true true + enable + true + $(DefineConstants);STRONG_NAME + - - - - - - + + + + - - - - - - - - - - - - + + + + + diff --git a/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj b/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj deleted file mode 100644 index ea2efb865..000000000 --- a/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - Dapper.Tests.Contrib - Dapper.Tests.Contrib - Dapper Contrib Test Suite - portable - Exe - false - netcoreapp1.0;netcoreapp2.0 - - - - - - - - - - - - - - - - - - - - - diff --git a/Dapper.Tests.Contrib/TestSuite.Async.cs b/Dapper.Tests.Contrib/TestSuite.Async.cs deleted file mode 100644 index 7c57a3634..000000000 --- a/Dapper.Tests.Contrib/TestSuite.Async.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -using Dapper.Contrib.Extensions; -using FactAttribute = Dapper.Tests.Contrib.SkippableFactAttribute; -using Xunit; - -namespace Dapper.Tests.Contrib -{ - public abstract partial class TestSuite - { - [Fact] - public async Task TypeWithGenericParameterCanBeInsertedAsync() - { - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync>(); - var objectToInsert = new GenericType - { - Id = Guid.NewGuid().ToString(), - Name = "something" - }; - await connection.InsertAsync(objectToInsert); - - Assert.Single(connection.GetAll>()); - - var objectsToInsert = new List> - { - new GenericType - { - Id = Guid.NewGuid().ToString(), - Name = "1", - }, - new GenericType - { - Id = Guid.NewGuid().ToString(), - Name = "2", - } - }; - - await connection.InsertAsync(objectsToInsert); - var list = connection.GetAll>(); - Assert.Equal(3, list.Count()); - } - } - - /// - /// Tests for issue #351 - /// - [Fact] - public async Task InsertGetUpdateDeleteWithExplicitKeyAsync() - { - using (var connection = GetOpenConnection()) - { - var guid = Guid.NewGuid().ToString(); - var o1 = new ObjectX { ObjectXId = guid, Name = "Foo" }; - var originalxCount = (await connection.QueryAsync("Select Count(*) From ObjectX").ConfigureAwait(false)).First(); - await connection.InsertAsync(o1).ConfigureAwait(false); - var list1 = (await connection.QueryAsync("select * from ObjectX").ConfigureAwait(false)).ToList(); - Assert.Equal(list1.Count, originalxCount + 1); - o1 = await connection.GetAsync(guid).ConfigureAwait(false); - Assert.Equal(o1.ObjectXId, guid); - o1.Name = "Bar"; - await connection.UpdateAsync(o1).ConfigureAwait(false); - o1 = await connection.GetAsync(guid).ConfigureAwait(false); - Assert.Equal("Bar", o1.Name); - await connection.DeleteAsync(o1).ConfigureAwait(false); - o1 = await connection.GetAsync(guid).ConfigureAwait(false); - Assert.Null(o1); - - const int id = 42; - var o2 = new ObjectY { ObjectYId = id, Name = "Foo" }; - var originalyCount = connection.Query("Select Count(*) From ObjectY").First(); - await connection.InsertAsync(o2).ConfigureAwait(false); - var list2 = (await connection.QueryAsync("select * from ObjectY").ConfigureAwait(false)).ToList(); - Assert.Equal(list2.Count, originalyCount + 1); - o2 = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal(o2.ObjectYId, id); - o2.Name = "Bar"; - await connection.UpdateAsync(o2).ConfigureAwait(false); - o2 = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal("Bar", o2.Name); - await connection.DeleteAsync(o2).ConfigureAwait(false); - o2 = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Null(o2); - } - } - - [Fact] - public async Task TableNameAsync() - { - using (var connection = GetOpenConnection()) - { - // tests against "Automobiles" table (Table attribute) - var id = await connection.InsertAsync(new Car { Name = "VolvoAsync" }).ConfigureAwait(false); - var car = await connection.GetAsync(id).ConfigureAwait(false); - Assert.NotNull(car); - Assert.Equal("VolvoAsync", car.Name); - Assert.True(await connection.UpdateAsync(new Car { Id = id, Name = "SaabAsync" }).ConfigureAwait(false)); - Assert.Equal("SaabAsync", (await connection.GetAsync(id).ConfigureAwait(false)).Name); - Assert.True(await connection.DeleteAsync(new Car { Id = id }).ConfigureAwait(false)); - Assert.Null(await connection.GetAsync(id).ConfigureAwait(false)); - } - } - - [Fact] - public async Task TestSimpleGetAsync() - { - using (var connection = GetOpenConnection()) - { - var id = await connection.InsertAsync(new User { Name = "Adama", Age = 10 }).ConfigureAwait(false); - var user = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal(id, user.Id); - Assert.Equal("Adama", user.Name); - await connection.DeleteAsync(user).ConfigureAwait(false); - } - } - - [Fact] - public async Task InsertGetUpdateAsync() - { - using (var connection = GetOpenConnection()) - { - Assert.Null(await connection.GetAsync(30).ConfigureAwait(false)); - - 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); - - //get a user with "isdirty" tracking - var user = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal("Adam", user.Name); - Assert.False(await connection.UpdateAsync(user).ConfigureAwait(false)); //returns false if not updated, based on tracking - user.Name = "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); - - //get a user with no tracking - var notrackedUser = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal("Bob", notrackedUser.Name); - Assert.True(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); - //returns true, even though user was not changed - notrackedUser.Name = "Cecil"; - Assert.True(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); - Assert.Equal("Cecil", (await connection.GetAsync(id).ConfigureAwait(false)).Name); - - Assert.Equal((await connection.QueryAsync("select * from Users").ConfigureAwait(false)).Count(), originalCount + 1); - Assert.True(await connection.DeleteAsync(user).ConfigureAwait(false)); - Assert.Equal((await connection.QueryAsync("select * from Users").ConfigureAwait(false)).Count(), originalCount); - - 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); - } - } - - [Fact] - public async Task InsertCheckKeyAsync() - { - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - Assert.Null(await connection.GetAsync(3).ConfigureAwait(false)); - var user = new User { Name = "Adamb", Age = 10 }; - var id = await connection.InsertAsync(user).ConfigureAwait(false); - Assert.Equal(user.Id, id); - } - } - - [Fact] - public async Task BuilderSelectClauseAsync() - { - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - var rand = new Random(8675309); - var data = new List(); - for (var i = 0; i < 100; i++) - { - var nU = new User { Age = rand.Next(70), Id = i, Name = 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"); - - builder.Select("Id"); - - var ids = await connection.QueryAsync(justId.RawSql, justId.Parameters).ConfigureAwait(false); - var users = await connection.QueryAsync(all.RawSql, all.Parameters).ConfigureAwait(false); - - 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"); - } - } - } - - [Fact] - public async Task BuilderTemplateWithoutCompositionAsync() - { - var builder = new SqlBuilder(); - var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new { age = 5 }); - - if (template.RawSql == null) throw new Exception("RawSql null"); - if (template.Parameters == null) throw new Exception("Parameters null"); - - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - await connection.InsertAsync(new User { Age = 5, Name = "Testy McTestington" }).ConfigureAwait(false); - - if ((await connection.QueryAsync(template.RawSql, template.Parameters).ConfigureAwait(false)).Single() != 1) - throw new Exception("Query failed"); - } - } - - [Fact] - public async Task InsertArrayAsync() - { - await InsertHelperAsync(src => src.ToArray()).ConfigureAwait(false); - } - - [Fact] - public async Task InsertListAsync() - { - await InsertHelperAsync(src => src.ToList()).ConfigureAwait(false); - } - - private async Task InsertHelperAsync(Func, T> helper) - where T : class - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - var total = await connection.InsertAsync(helper(users)).ConfigureAwait(false); - Assert.Equal(total, numberOfEntities); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities); - } - } - - [Fact] - public async Task UpdateArrayAsync() - { - await UpdateHelperAsync(src => src.ToArray()).ConfigureAwait(false); - } - - [Fact] - public async Task UpdateListAsync() - { - await UpdateHelperAsync(src => src.ToList()).ConfigureAwait(false); - } - - private async Task UpdateHelperAsync(Func, T> helper) - where T : class - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - var total = await connection.InsertAsync(helper(users)).ConfigureAwait(false); - Assert.Equal(total, numberOfEntities); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities); - foreach (var user in users) - { - user.Name += " updated"; - } - await connection.UpdateAsync(helper(users)).ConfigureAwait(false); - var name = connection.Query("select * from Users").First().Name; - Assert.Contains("updated", name); - } - } - - [Fact] - public async Task DeleteArrayAsync() - { - await DeleteHelperAsync(src => src.ToArray()).ConfigureAwait(false); - } - - [Fact] - public async Task DeleteListAsync() - { - await DeleteHelperAsync(src => src.ToList()).ConfigureAwait(false); - } - - private async Task DeleteHelperAsync(Func, T> helper) - where T : class - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - var total = await connection.InsertAsync(helper(users)).ConfigureAwait(false); - Assert.Equal(total, numberOfEntities); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities); - - var usersToDelete = users.Take(10).ToList(); - await connection.DeleteAsync(helper(usersToDelete)).ConfigureAwait(false); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities - 10); - } - } - - [Fact] - public async Task GetAllAsync() - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - - var total = await connection.InsertAsync(users).ConfigureAwait(false); - Assert.Equal(total, numberOfEntities); - users = (List)await connection.GetAllAsync().ConfigureAwait(false); - Assert.Equal(users.Count, numberOfEntities); - var iusers = await connection.GetAllAsync().ConfigureAwait(false); - Assert.Equal(iusers.ToList().Count, numberOfEntities); - } - } - - [Fact] - public async Task InsertFieldWithReservedNameAsync() - { - using (var connection = GetOpenConnection()) - { - await connection.DeleteAllAsync().ConfigureAwait(false); - var id = await connection.InsertAsync(new Result { Name = "Adam", Order = 1 }).ConfigureAwait(false); - - var result = await connection.GetAsync(id).ConfigureAwait(false); - Assert.Equal(1, result.Order); - } - } - - [Fact] - public async Task DeleteAllAsync() - { - using (var connection = GetOpenConnection()) - { - 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); - await connection.DeleteAllAsync().ConfigureAwait(false); - Assert.Null(await connection.GetAsync(id1).ConfigureAwait(false)); - Assert.Null(await connection.GetAsync(id2).ConfigureAwait(false)); - } - } - } -} diff --git a/Dapper.Tests.Contrib/TestSuite.cs b/Dapper.Tests.Contrib/TestSuite.cs deleted file mode 100644 index 44c2bc280..000000000 --- a/Dapper.Tests.Contrib/TestSuite.cs +++ /dev/null @@ -1,671 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; - -using Dapper.Contrib.Extensions; -using Xunit; - -#if !NETCOREAPP1_0 && !NETCOREAPP2_0 -using System.Transactions; -using System.Data.SqlServerCe; -#endif -using FactAttribute = Dapper.Tests.Contrib.SkippableFactAttribute; - -namespace Dapper.Tests.Contrib -{ - [Table("ObjectX")] - public class ObjectX - { - [ExplicitKey] - public string ObjectXId { get; set; } - public string Name { get; set; } - } - - [Table("ObjectY")] - public class ObjectY - { - [ExplicitKey] - public int ObjectYId { get; set; } - public string Name { get; set; } - } - - [Table("ObjectZ")] - public class ObjectZ - { - [ExplicitKey] - public int Id { get; set; } - public string Name { get; set; } - } - - public interface IUser - { - [Key] - int Id { get; set; } - string Name { get; set; } - int Age { get; set; } - } - - public class User : IUser - { - public int Id { get; set; } - public string Name { get; set; } - public int Age { get; set; } - } - - public class Person - { - public int Id { get; set; } - public string Name { get; set; } - } - - [Table("Stuff")] - public class Stuff - { - [Key] - public short TheId { get; set; } - public string Name { get; set; } - public DateTime? Created { get; set; } - } - - [Table("Automobiles")] - public class Car - { - public int Id { get; set; } - public string Name { get; set; } - [Computed] - public string Computed { get; set; } - } - - [Table("Results")] - public class Result - { - public int Id { get; set; } - public string Name { get; set; } - public int Order { get; set; } - } - - [Table("GenericType")] - public class GenericType - { - [ExplicitKey] - public string Id { get; set; } - public string Name { get; set; } - } - - public abstract partial class TestSuite - { - protected static readonly bool IsAppVeyor = Environment.GetEnvironmentVariable("Appveyor")?.ToUpperInvariant() == "TRUE"; - - public abstract IDbConnection GetConnection(); - - private IDbConnection GetOpenConnection() - { - var connection = GetConnection(); - connection.Open(); - return connection; - } - - [Fact] - public void TypeWithGenericParameterCanBeInserted() - { - using (var connection = GetOpenConnection()) - { - connection.DeleteAll>(); - var objectToInsert = new GenericType - { - Id = Guid.NewGuid().ToString(), - Name = "something" - }; - connection.Insert(objectToInsert); - - Assert.Single(connection.GetAll>()); - - var objectsToInsert = new List> - { - new GenericType - { - Id = Guid.NewGuid().ToString(), - Name = "1", - }, - new GenericType - { - Id = Guid.NewGuid().ToString(), - Name = "2", - } - }; - - connection.Insert(objectsToInsert); - var list = connection.GetAll>(); - Assert.Equal(3, list.Count()); - } - } - - [Fact] - public void Issue418() - { - using (var connection = GetOpenConnection()) - { - //update first (will fail) then insert - //added for bug #418 - var updateObject = new ObjectX - { - ObjectXId = Guid.NewGuid().ToString(), - Name = "Someone" - }; - var updates = connection.Update(updateObject); - Assert.False(updates); - - connection.DeleteAll(); - - var objectXId = Guid.NewGuid().ToString(); - var insertObject = new ObjectX - { - ObjectXId = objectXId, - Name = "Someone else" - }; - connection.Insert(insertObject); - var list = connection.GetAll(); - Assert.Single(list); - } - } - - /// - /// Tests for issue #351 - /// - [Fact] - public void InsertGetUpdateDeleteWithExplicitKey() - { - using (var connection = GetOpenConnection()) - { - var guid = Guid.NewGuid().ToString(); - var o1 = new ObjectX { ObjectXId = guid, Name = "Foo" }; - var originalxCount = connection.Query("Select Count(*) From ObjectX").First(); - connection.Insert(o1); - var list1 = connection.Query("select * from ObjectX").ToList(); - Assert.Equal(list1.Count, originalxCount + 1); - o1 = connection.Get(guid); - Assert.Equal(o1.ObjectXId, guid); - o1.Name = "Bar"; - connection.Update(o1); - o1 = connection.Get(guid); - Assert.Equal("Bar", o1.Name); - connection.Delete(o1); - o1 = connection.Get(guid); - Assert.Null(o1); - - const int id = 42; - var o2 = new ObjectY { ObjectYId = id, Name = "Foo" }; - var originalyCount = connection.Query("Select Count(*) From ObjectY").First(); - connection.Insert(o2); - var list2 = connection.Query("select * from ObjectY").ToList(); - Assert.Equal(list2.Count, originalyCount + 1); - o2 = connection.Get(id); - Assert.Equal(o2.ObjectYId, id); - o2.Name = "Bar"; - connection.Update(o2); - o2 = connection.Get(id); - Assert.Equal("Bar", o2.Name); - connection.Delete(o2); - o2 = connection.Get(id); - Assert.Null(o2); - } - } - - [Fact] - public void GetAllWithExplicitKey() - { - using (var connection = GetOpenConnection()) - { - var guid = Guid.NewGuid().ToString(); - var o1 = new ObjectX { ObjectXId = guid, Name = "Foo" }; - connection.Insert(o1); - - var objectXs = connection.GetAll().ToList(); - Assert.True(objectXs.Count > 0); - Assert.Equal(1, objectXs.Count(x => x.ObjectXId == guid)); - } - } - - [Fact] - public void InsertGetUpdateDeleteWithExplicitKeyNamedId() - { - using (var connection = GetOpenConnection()) - { - const int id = 42; - var o2 = new ObjectZ { Id = id, Name = "Foo" }; - connection.Insert(o2); - var list2 = connection.Query("select * from ObjectZ").ToList(); - Assert.Single(list2); - o2 = connection.Get(id); - Assert.Equal(o2.Id, id); - } - } - - [Fact] - public void ShortIdentity() - { - using (var connection = GetOpenConnection()) - { - const string name = "First item"; - var id = connection.Insert(new Stuff { Name = name }); - Assert.True(id > 0); // 1-n are valid here, due to parallel tests - var item = connection.Get(id); - Assert.Equal(item.TheId, (short)id); - Assert.Equal(item.Name, name); - } - } - - [Fact] - public void NullDateTime() - { - using (var connection = GetOpenConnection()) - { - connection.Insert(new Stuff { Name = "First item" }); - connection.Insert(new Stuff { Name = "Second item", Created = DateTime.Now }); - var stuff = connection.Query("select * from Stuff").ToList(); - Assert.Null(stuff[0].Created); - Assert.NotNull(stuff.Last().Created); - } - } - - [Fact] - public void TableName() - { - using (var connection = GetOpenConnection()) - { - // tests against "Automobiles" table (Table attribute) - var id = connection.Insert(new Car { Name = "Volvo" }); - var car = connection.Get(id); - Assert.NotNull(car); - Assert.Equal("Volvo", car.Name); - Assert.Equal("Volvo", connection.Get(id).Name); - Assert.True(connection.Update(new Car { Id = (int)id, Name = "Saab" })); - Assert.Equal("Saab", connection.Get(id).Name); - Assert.True(connection.Delete(new Car { Id = (int)id })); - Assert.Null(connection.Get(id)); - } - } - - [Fact] - public void TestSimpleGet() - { - using (var connection = GetOpenConnection()) - { - var id = connection.Insert(new User { Name = "Adama", Age = 10 }); - var user = connection.Get(id); - Assert.Equal(user.Id, (int)id); - Assert.Equal("Adama", user.Name); - connection.Delete(user); - } - } - - [Fact] - public void TestClosedConnection() - { - using (var connection = GetConnection()) - { - Assert.True(connection.Insert(new User { Name = "Adama", Age = 10 }) > 0); - var users = connection.GetAll(); - Assert.NotEmpty(users); - } - } - - [Fact] - public void InsertArray() - { - InsertHelper(src => src.ToArray()); - } - - [Fact] - public void InsertList() - { - InsertHelper(src => src.ToList()); - } - - private void InsertHelper(Func, T> helper) - where T : class - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - - var total = connection.Insert(helper(users)); - Assert.Equal(total, numberOfEntities); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities); - } - } - - [Fact] - public void UpdateArray() - { - UpdateHelper(src => src.ToArray()); - } - - [Fact] - public void UpdateList() - { - UpdateHelper(src => src.ToList()); - } - - private void UpdateHelper(Func, T> helper) - where T : class - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - - var total = connection.Insert(helper(users)); - Assert.Equal(total, numberOfEntities); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities); - foreach (var user in users) - { - user.Name += " updated"; - } - connection.Update(helper(users)); - var name = connection.Query("select * from Users").First().Name; - Assert.Contains("updated", name); - } - } - - [Fact] - public void DeleteArray() - { - DeleteHelper(src => src.ToArray()); - } - - [Fact] - public void DeleteList() - { - DeleteHelper(src => src.ToList()); - } - - private void DeleteHelper(Func, T> helper) - where T : class - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - - var total = connection.Insert(helper(users)); - Assert.Equal(total, numberOfEntities); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities); - - var usersToDelete = users.Take(10).ToList(); - connection.Delete(helper(usersToDelete)); - users = connection.Query("select * from Users").ToList(); - Assert.Equal(users.Count, numberOfEntities - 10); - } - } - - [Fact] - public void InsertGetUpdate() - { - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - Assert.Null(connection.Get(3)); - - //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 }); - - //get a user with "isdirty" tracking - var user = connection.Get(id); - Assert.Equal("Adam", user.Name); - Assert.False(connection.Update(user)); //returns false if not updated, based on tracking - user.Name = "Bob"; - Assert.True(connection.Update(user)); //returns true if updated, based on tracking - user = connection.Get(id); - Assert.Equal("Bob", user.Name); - - //get a user with no tracking - var notrackedUser = connection.Get(id); - Assert.Equal("Bob", notrackedUser.Name); - Assert.True(connection.Update(notrackedUser)); //returns true, even though user was not changed - notrackedUser.Name = "Cecil"; - Assert.True(connection.Update(notrackedUser)); - Assert.Equal("Cecil", connection.Get(id).Name); - - Assert.Single(connection.Query("select * from Users")); - Assert.True(connection.Delete(user)); - Assert.Empty(connection.Query("select * from Users")); - - Assert.False(connection.Update(notrackedUser)); //returns false, user not found - } - } - -#if !NETCOREAPP1_0 && !NETCOREAPP2_0 - [Fact(Skip = "Not parallel friendly - thinking about how to test this")] - public void InsertWithCustomDbType() - { - SqlMapperExtensions.GetDatabaseType = conn => "SQLiteConnection"; - - bool sqliteCodeCalled = false; - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - Assert.IsNull(connection.Get(3)); - try - { - connection.Insert(new User { Name = "Adam", Age = 10 }); - } - catch (SqlCeException ex) - { - sqliteCodeCalled = ex.Message.IndexOf("There was an error parsing the query", StringComparison.OrdinalIgnoreCase) >= 0; - } - // ReSharper disable once EmptyGeneralCatchClause - catch (Exception) - { - } - } - SqlMapperExtensions.GetDatabaseType = null; - - if (!sqliteCodeCalled) - { - throw new Exception("Was expecting sqlite code to be called"); - } - } -#endif - - [Fact] - public void InsertWithCustomTableNameMapper() - { - SqlMapperExtensions.TableNameMapper = type => - { - switch (type.Name()) - { - case "Person": - return "People"; - default: - var tableattr = type.GetCustomAttributes(false).SingleOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic; - if (tableattr != null) - return tableattr.Name; - - var name = type.Name + "s"; - if (type.IsInterface() && name.StartsWith("I")) - return name.Substring(1); - return name; - } - }; - - using (var connection = GetOpenConnection()) - { - var id = connection.Insert(new Person { Name = "Mr Mapper" }); - Assert.Equal(1, id); - connection.GetAll(); - } - } - - [Fact] - public void GetAll() - { - const int numberOfEntities = 10; - - var users = new List(); - for (var i = 0; i < numberOfEntities; i++) - users.Add(new User { Name = "User " + i, Age = i }); - - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - - var total = connection.Insert(users); - Assert.Equal(total, numberOfEntities); - users = connection.GetAll().ToList(); - Assert.Equal(users.Count, numberOfEntities); - var iusers = connection.GetAll().ToList(); - Assert.Equal(iusers.Count, numberOfEntities); - for (var i = 0; i < numberOfEntities; i++) - Assert.Equal(iusers[i].Age, i); - } - } - - [Fact] - public void Transactions() - { - using (var connection = GetOpenConnection()) - { - var id = connection.Insert(new Car { Name = "one car" }); //insert outside transaction - - var tran = connection.BeginTransaction(); - var car = connection.Get(id, tran); - var orgName = car.Name; - car.Name = "Another car"; - connection.Update(car, tran); - tran.Rollback(); - - car = connection.Get(id); //updates should have been rolled back - Assert.Equal(car.Name, orgName); - } - } - -#if !NETCOREAPP1_0 && !NETCOREAPP2_0 - [Fact] - public void TransactionScope() - { - using (var txscope = new TransactionScope()) - { - using (var connection = GetOpenConnection()) - { - var id = connection.Insert(new Car { Name = "one car" }); //inser car within transaction - - txscope.Dispose(); //rollback - - Assert.IsNull(connection.Get(id)); //returns null - car with that id should not exist - } - } - } -#endif - - [Fact] - public void InsertCheckKey() - { - using (var connection = GetOpenConnection()) - { - Assert.Null(connection.Get(3)); - User user = new User { Name = "Adamb", Age = 10 }; - int id = (int)connection.Insert(user); - Assert.Equal(user.Id, id); - } - } - - [Fact] - public void BuilderSelectClause() - { - using (var connection = GetOpenConnection()) - { - var rand = new Random(8675309); - var data = new List(); - for (int i = 0; i < 100; i++) - { - var nU = new User { Age = rand.Next(70), Id = i, Name = 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"); - - builder.Select("Id"); - - var ids = connection.Query(justId.RawSql, justId.Parameters); - var users = connection.Query(all.RawSql, all.Parameters); - - 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"); - } - } - } - - [Fact] - public void BuilderTemplateWithoutComposition() - { - var builder = new SqlBuilder(); - var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new { age = 5 }); - - if (template.RawSql == null) throw new Exception("RawSql null"); - if (template.Parameters == null) throw new Exception("Parameters null"); - - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - connection.Insert(new User { Age = 5, Name = "Testy McTestington" }); - - if (connection.Query(template.RawSql, template.Parameters).Single() != 1) - throw new Exception("Query failed"); - } - } - - [Fact] - public void InsertFieldWithReservedName() - { - using (var connection = GetOpenConnection()) - { - connection.DeleteAll(); - var id = connection.Insert(new Result() { Name = "Adam", Order = 1 }); - - var result = connection.Get(id); - Assert.Equal(1, result.Order); - } - } - - [Fact] - 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 }); - Assert.True(connection.DeleteAll()); - Assert.Null(connection.Get(id1)); - Assert.Null(connection.Get(id2)); - } - } - } -} diff --git a/Dapper.Tests.Contrib/TestSuites.cs b/Dapper.Tests.Contrib/TestSuites.cs deleted file mode 100644 index 3949138b7..000000000 --- a/Dapper.Tests.Contrib/TestSuites.cs +++ /dev/null @@ -1,181 +0,0 @@ -using Microsoft.Data.Sqlite; -using MySql.Data.MySqlClient; -using System; -using System.Data; -using System.Data.SqlClient; -using System.IO; -using Xunit; -using Xunit.Sdk; - -#if !NETCOREAPP1_0 && !NETCOREAPP2_0 -using System.Data.SqlServerCe; -#endif - -namespace Dapper.Tests.Contrib -{ - // The test suites here implement TestSuiteBase so that each provider runs - // the entire set of tests without declarations per method - // If we want to support a new provider, they need only be added here - not in multiple places - - [XunitTestCaseDiscoverer("Dapper.Tests.SkippableFactDiscoverer", "Dapper.Tests.Contrib")] - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class SkippableFactAttribute : FactAttribute - { - } - - public class SqlServerTestSuite : TestSuite - { - private const string DbName = "tempdb"; - public static string ConnectionString => - IsAppVeyor - ? @"Server=(local)\SQL2016;Database=tempdb;User ID=sa;Password=Password12!" - : $"Data Source=.;Initial Catalog={DbName};Integrated Security=True"; - public override IDbConnection GetConnection() => new SqlConnection(ConnectionString); - - static SqlServerTestSuite() - { - using (var connection = new SqlConnection(ConnectionString)) - { - // ReSharper disable once AccessToDisposedClosure - Action dropTable = 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);"); - 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);"); - 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 (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null);"); - dropTable("ObjectY"); - connection.Execute("CREATE TABLE ObjectY (ObjectYId 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);"); - } - } - } - - public class MySqlServerTestSuite : TestSuite - { - private const string DbName = "DapperContribTests"; - - public static string ConnectionString { get; } = - IsAppVeyor - ? "Server=localhost;Uid=root;Pwd=Password12!;" - : "Server=localhost;Uid=test;Pwd=pass;"; - - public override IDbConnection GetConnection() - { - if (_skip) throw new SkipTestException("Skipping MySQL Tests - no server."); - return new MySqlConnection(ConnectionString); - } - - private static readonly bool _skip; - - static MySqlServerTestSuite() - { - try - { - using (var connection = new MySqlConnection(ConnectionString)) - { - // ReSharper disable once AccessToDisposedClosure - Action dropTable = name => connection.Execute($"DROP TABLE IF EXISTS `{name}`;"); - connection.Open(); - connection.Execute($"DROP DATABASE IF EXISTS {DbName}; CREATE DATABASE {DbName}; USE {DbName};"); - dropTable("Stuff"); - connection.Execute("CREATE TABLE Stuff (TheId int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null, Created DateTime null);"); - 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);"); - dropTable("Automobiles"); - connection.Execute("CREATE TABLE Automobiles (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null);"); - dropTable("Results"); - connection.Execute("CREATE TABLE Results (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null, `Order` int not null);"); - dropTable("ObjectX"); - connection.Execute("CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null);"); - dropTable("ObjectY"); - connection.Execute("CREATE TABLE ObjectY (ObjectYId 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);"); - } - } - catch (MySqlException e) - { - if (e.Message.Contains("Unable to connect")) - _skip = true; - else - throw; - } - } - } - - public class SQLiteTestSuite : TestSuite - { - private const string FileName = "Test.DB.sqlite"; - public static string ConnectionString => $"Filename=./{FileName};Mode=ReadWriteCreate;"; - public override IDbConnection GetConnection() => new SqliteConnection(ConnectionString); - - static SQLiteTestSuite() - { - if (File.Exists(FileName)) - { - File.Delete(FileName); - } - 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 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 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) "); - connection.Execute("CREATE TABLE ObjectY (ObjectYId 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) "); - } - } - } - -#if !NETCOREAPP1_0 && !NETCOREAPP2_0 - public class SqlCETestSuite : TestSuite - { - const string FileName = "Test.DB.sdf"; - public static string ConnectionString => $"Data Source={FileName};"; - public override IDbConnection GetConnection() => new SqlCeConnection(ConnectionString); - - static SqlCETestSuite() - { - if (File.Exists(FileName)) - { - File.Delete(FileName); - } - var engine = new SqlCeEngine(ConnectionString); - engine.CreateDatabase(); - 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 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 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) "); - connection.Execute(@"CREATE TABLE ObjectY (ObjectYId int not null, Name nvarchar(100) not null) "); - connection.Execute(@"CREATE TABLE ObjectZ (Id int not null, Name nvarchar(100) not null) "); - connection.Execute(@"CREATE TABLE GenericType (Id nvarchar(100) not null, Name nvarchar(100) not null) "); - } - Console.WriteLine("Created database"); - } - } -#endif -} diff --git a/Dapper.Tests.Performance/Benchmarks.Belgrade.cs b/Dapper.Tests.Performance/Benchmarks.Belgrade.cs deleted file mode 100644 index 8813912e9..000000000 --- a/Dapper.Tests.Performance/Benchmarks.Belgrade.cs +++ /dev/null @@ -1,44 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Belgrade.SqlClient.SqlDb; -using System.Threading.Tasks; - -namespace Dapper.Tests.Performance -{ - public class BelgradeBenchmarks : BenchmarkBase - { - private QueryMapper _mapper; - - [GlobalSetup] - public void Setup() - { - BaseSetup(); - _mapper = new QueryMapper(ConnectionString); - } - - [Benchmark(Description = "ExecuteReader")] - public async Task ExecuteReader() - { - Step(); - // TODO: How do you get a Post out of this thing? - await _mapper.ExecuteReader("SELECT TOP 1 * FROM Posts WHERE Id = " + i, - reader => - { - var post = new Post(); - post.Id = reader.GetInt32(0); - post.Text = reader.GetString(1); - post.CreationDate = reader.GetDateTime(2); - post.LastChangeDate = reader.GetDateTime(3); - - post.Counter1 = reader.IsDBNull(4) ? null : (int?)reader.GetInt32(4); - post.Counter2 = reader.IsDBNull(5) ? null : (int?)reader.GetInt32(5); - post.Counter3 = reader.IsDBNull(6) ? null : (int?)reader.GetInt32(6); - post.Counter4 = reader.IsDBNull(7) ? null : (int?)reader.GetInt32(7); - post.Counter5 = reader.IsDBNull(8) ? null : (int?)reader.GetInt32(8); - post.Counter6 = reader.IsDBNull(9) ? null : (int?)reader.GetInt32(9); - post.Counter7 = reader.IsDBNull(10) ? null : (int?)reader.GetInt32(10); - post.Counter8 = reader.IsDBNull(11) ? null : (int?)reader.GetInt32(11); - post.Counter9 = reader.IsDBNull(12) ? null : (int?)reader.GetInt32(12); - }); - } - } -} \ No newline at end of file diff --git a/Dapper.Tests.Performance/Benchmarks.EntityFrameworkCore.cs b/Dapper.Tests.Performance/Benchmarks.EntityFrameworkCore.cs deleted file mode 100644 index 1425493c3..000000000 --- a/Dapper.Tests.Performance/Benchmarks.EntityFrameworkCore.cs +++ /dev/null @@ -1,44 +0,0 @@ -using BenchmarkDotNet.Attributes; -using Dapper.Tests.Performance.Linq2Sql; -using Microsoft.EntityFrameworkCore; -using System; -using System.Data.Linq; -using System.Linq; - -namespace Dapper.Tests.Performance -{ - public class EFCoreBenchmarks : BenchmarkBase - { - private EntityFrameworkCore.EFCoreContext Context; - private static readonly Func compiledQuery = - CompiledQuery.Compile((DataClassesDataContext ctx, int id) => ctx.Posts.First(p => p.Id == id)); - - [GlobalSetup] - public void Setup() - { - BaseSetup(); - Context = new EntityFrameworkCore.EFCoreContext(_connection.ConnectionString); - } - - [Benchmark(Description = "Normal")] - public Post Normal() - { - Step(); - return Context.Posts.First(p => p.Id == i); - } - - [Benchmark(Description = "SqlQuery")] - public Post SqlQuery() - { - Step(); - return Context.Posts.FromSql("select * from Posts where Id = {0}", i).First(); - } - - [Benchmark(Description = "No Tracking")] - public Post NoTracking() - { - Step(); - return Context.Posts.AsNoTracking().First(p => p.Id == i); - } - } -} \ No newline at end of file diff --git a/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs b/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs deleted file mode 100644 index 1a86b67ed..000000000 --- a/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs +++ /dev/null @@ -1,38 +0,0 @@ -using BenchmarkDotNet.Attributes; -using PetaPoco; -using System.Linq; - -namespace Dapper.Tests.Performance -{ - public class PetaPocoBenchmarks : BenchmarkBase - { - private Database _db, _dbFast; - - [GlobalSetup] - public void Setup() - { - BaseSetup(); - _db = new Database(ConnectionString, "System.Data.SqlClient"); - _db.OpenSharedConnection(); - _dbFast = new Database(ConnectionString, "System.Data.SqlClient"); - _dbFast.OpenSharedConnection(); - _dbFast.EnableAutoSelect = false; - _dbFast.EnableNamedParams = false; - _dbFast.ForceDateTimesToUtc = false; - } - - [Benchmark(Description = "Fetch")] - public Post Fetch() - { - Step(); - return _db.Fetch("SELECT * from Posts where Id=@0", i).First(); - } - - [Benchmark(Description = "Fetch (Fast)")] - public Post FetchFast() - { - Step(); - return _dbFast.Fetch("SELECT * from Posts where Id=@0", i).First(); - } - } -} \ No newline at end of file diff --git a/Dapper.Tests.Performance/Benchmarks.Soma.cs b/Dapper.Tests.Performance/Benchmarks.Soma.cs deleted file mode 100644 index ae7db5c9b..000000000 --- a/Dapper.Tests.Performance/Benchmarks.Soma.cs +++ /dev/null @@ -1,23 +0,0 @@ -using BenchmarkDotNet.Attributes; - -namespace Dapper.Tests.Performance -{ - public class SomaBenchmarks : BenchmarkBase - { - private dynamic _sdb; - - [GlobalSetup] - public void Setup() - { - BaseSetup(); - _sdb = Simple.Data.Database.OpenConnection(ConnectionString); - } - - [Benchmark(Description = "FindById")] - public dynamic QueryDynamic() - { - Step(); - return _sdb.Posts.FindById(i).FirstOrDefault(); - } - } -} \ No newline at end of file diff --git a/Dapper.Tests.Performance/Benchmarks.cs b/Dapper.Tests.Performance/Benchmarks.cs deleted file mode 100644 index 9518318aa..000000000 --- a/Dapper.Tests.Performance/Benchmarks.cs +++ /dev/null @@ -1,58 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Attributes.Columns; -using BenchmarkDotNet.Columns; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Horology; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Order; -using Dapper.Tests.Performance.Helpers; -using System; -using System.Configuration; -using System.Data.SqlClient; - -namespace Dapper.Tests.Performance -{ - [OrderProvider(SummaryOrderPolicy.FastestToSlowest)] - [RankColumn] - [Config(typeof(Config))] - public abstract class BenchmarkBase - { - public const int Iterations = 50; - protected static readonly Random _rand = new Random(); - protected SqlConnection _connection; - public static string ConnectionString { get; } = ConfigurationManager.ConnectionStrings["Main"].ConnectionString; - protected int i; - - protected void BaseSetup() - { - i = 0; - _connection = new SqlConnection(ConnectionString); - _connection.Open(); - } - - protected void Step() - { - i++; - if (i > 5000) i = 1; - } - } - - public class Config : ManualConfig - { - public Config() - { - Add(new MemoryDiagnoser()); - Add(new ORMColum()); - Add(new ReturnColum()); - Add(Job.Default - .WithUnrollFactor(BenchmarkBase.Iterations) - //.WithIterationTime(new TimeInterval(500, TimeUnit.Millisecond)) - .WithLaunchCount(1) - .WithWarmupCount(0) - .WithTargetCount(5) - .WithRemoveOutliers(true) - ); - } - } -} \ No newline at end of file diff --git a/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj b/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj deleted file mode 100644 index dd4ec0bdd..000000000 --- a/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj +++ /dev/null @@ -1,51 +0,0 @@ - - - Dapper.Tests.Performance - Dapper.Tests.Performance - Dapper Core Performance Suite - Exe - false - net462 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Dapper.Tests.Performance/Soma/SomaConfig.cs b/Dapper.Tests.Performance/Soma/SomaConfig.cs deleted file mode 100644 index aa2dbcdd6..000000000 --- a/Dapper.Tests.Performance/Soma/SomaConfig.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Soma.Core; -using System; - -namespace Dapper.Tests.Performance.Soma -{ - internal class SomaConfig : MsSqlConfig - { - public override string ConnectionString => BenchmarkBase.ConnectionString; - - public override Action Logger => noOp; - - private static readonly Action noOp = x => { /* nope */ }; - } -} diff --git a/Dapper.Tests/Dapper.Tests.csproj b/Dapper.Tests/Dapper.Tests.csproj deleted file mode 100644 index 197dd121f..000000000 --- a/Dapper.Tests/Dapper.Tests.csproj +++ /dev/null @@ -1,67 +0,0 @@ - - - Dapper.Tests - Dapper.Tests - Dapper Core Test Suite - false - true - true - net452;netcoreapp1.0;netcoreapp2.0 - false - - - - $(DefineConstants);ENTITY_FRAMEWORK;LINQ2SQL;SQL_CE;OLEDB - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if not exist "$(TargetDir)x86" md "$(TargetDir)x86" - xcopy /s /y /q "$(NuGetPackageRoot)\Microsoft.SqlServer.Types\14.0.314.76\NativeBinaries\x86\*.*" "$(TargetDir)x86" - if not exist "$(TargetDir)x64" md "$(TargetDir)x64" - xcopy /s /y /q "$(NuGetPackageRoot)\Microsoft.SqlServer.Types\14.0.314.76\NativeBinaries\x64\*.*" "$(TargetDir)x64" - - - diff --git a/Dapper.Tests/DataReaderTests.cs b/Dapper.Tests/DataReaderTests.cs deleted file mode 100644 index 0d9bd49da..000000000 --- a/Dapper.Tests/DataReaderTests.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Xunit; - -namespace Dapper.Tests -{ - public class DataReaderTests : TestBase - { - [Fact] - public void GetSameReaderForSameShape() - { - var origReader = connection.ExecuteReader("select 'abc' as Name, 123 as Id"); - var origParser = origReader.GetRowParser(typeof(HazNameId)); - - var typedParser = origReader.GetRowParser(); - - Assert.True(ReferenceEquals(origParser, typedParser)); - - var list = origReader.Parse().ToList(); - Assert.Single(list); - Assert.Equal("abc", list[0].Name); - Assert.Equal(123, list[0].Id); - origReader.Dispose(); - - var secondReader = connection.ExecuteReader("select 'abc' as Name, 123 as Id"); - var secondParser = secondReader.GetRowParser(typeof(HazNameId)); - var thirdParser = secondReader.GetRowParser(typeof(HazNameId), 1); - - list = secondReader.Parse().ToList(); - Assert.Single(list); - Assert.Equal("abc", list[0].Name); - Assert.Equal(123, list[0].Id); - secondReader.Dispose(); - - // now: should be different readers, but same parser - Assert.False(ReferenceEquals(origReader, secondReader)); - Assert.True(ReferenceEquals(origParser, secondParser)); - Assert.False(ReferenceEquals(secondParser, thirdParser)); - } - - [Fact] - public void DiscriminatedUnion() - { - List result = new List(); - using (var reader = connection.ExecuteReader(@" -select 'abc' as Name, 1 as Type, 3.0 as Value -union all -select 'def' as Name, 2 as Type, 4.0 as Value")) - { - if (reader.Read()) - { - var toFoo = reader.GetRowParser(typeof(Discriminated_Foo)); - var toBar = reader.GetRowParser(typeof(Discriminated_Bar)); - - var col = reader.GetOrdinal("Type"); - do - { - switch (reader.GetInt32(col)) - { - case 1: - result.Add(toFoo(reader)); - break; - case 2: - result.Add(toBar(reader)); - break; - } - } while (reader.Read()); - } - } - - Assert.Equal(2, result.Count); - Assert.Equal(1, result[0].Type); - Assert.Equal(2, result[1].Type); - var foo = (Discriminated_Foo)result[0]; - Assert.Equal("abc", foo.Name); - var bar = (Discriminated_Bar)result[1]; - Assert.Equal(bar.Value, (float)4.0); - } - - [Fact] - public void DiscriminatedUnionWithMultiMapping() - { - var result = new List(); - using (var reader = connection.ExecuteReader(@" -select 'abc' as Name, 1 as Type, 3.0 as Value, 1 as Id, 'zxc' as Name -union all -select 'def' as Name, 2 as Type, 4.0 as Value, 2 as Id, 'qwe' as Name")) - { - if (reader.Read()) - { - var col = reader.GetOrdinal("Type"); - var splitOn = reader.GetOrdinal("Id"); - - var toFoo = reader.GetRowParser(typeof(DiscriminatedWithMultiMapping_Foo), 0, splitOn); - var toBar = reader.GetRowParser(typeof(DiscriminatedWithMultiMapping_Bar), 0, splitOn); - var toHaz = reader.GetRowParser(typeof(HazNameId), splitOn, reader.FieldCount - splitOn); - - do - { - DiscriminatedWithMultiMapping_BaseType obj = null; - switch (reader.GetInt32(col)) - { - case 1: - obj = toFoo(reader); - break; - case 2: - obj = toBar(reader); - break; - } - - Assert.NotNull(obj); - obj.HazNameIdObject = toHaz(reader); - result.Add(obj); - - } while (reader.Read()); - } - } - - Assert.Equal(2, result.Count); - Assert.Equal(1, result[0].Type); - Assert.Equal(2, result[1].Type); - var foo = (DiscriminatedWithMultiMapping_Foo)result[0]; - Assert.Equal("abc", foo.Name); - Assert.Equal(1, foo.HazNameIdObject.Id); - Assert.Equal("zxc", foo.HazNameIdObject.Name); - var bar = (DiscriminatedWithMultiMapping_Bar)result[1]; - Assert.Equal(bar.Value, (float)4.0); - Assert.Equal(2, bar.HazNameIdObject.Id); - Assert.Equal("qwe", bar.HazNameIdObject.Name); - } - - private abstract class Discriminated_BaseType - { - public abstract int Type { get; } - } - - private class Discriminated_Foo : Discriminated_BaseType - { - public string Name { get; set; } - public override int Type => 1; - } - - private class Discriminated_Bar : Discriminated_BaseType - { - public float Value { get; set; } - public override int Type => 2; - } - - private abstract class DiscriminatedWithMultiMapping_BaseType : Discriminated_BaseType - { - public abstract HazNameId HazNameIdObject { get; set; } - } - - private class DiscriminatedWithMultiMapping_Foo : DiscriminatedWithMultiMapping_BaseType - { - public override HazNameId HazNameIdObject { get; set; } - public string Name { get; set; } - public override int Type => 1; - } - - private class DiscriminatedWithMultiMapping_Bar : DiscriminatedWithMultiMapping_BaseType - { - public override HazNameId HazNameIdObject { get; set; } - public float Value { get; set; } - public override int Type => 2; - } - } -} diff --git a/Dapper.Tests/Helpers/Attributes.cs b/Dapper.Tests/Helpers/Attributes.cs deleted file mode 100644 index d71d932b8..000000000 --- a/Dapper.Tests/Helpers/Attributes.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Data.SqlClient; -using Xunit; - -namespace Dapper.Tests -{ - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public sealed class FactLongRunningAttribute : FactAttribute - { - public FactLongRunningAttribute() - { -#if !LONG_RUNNING - Skip = "Long running"; -#endif - } - - public string Url { get; private set; } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class FactRequiredCompatibilityLevelAttribute : FactAttribute - { - public FactRequiredCompatibilityLevelAttribute(int level) : base() - { - if (DetectedLevel < level) - { - Skip = $"Compatibility level {level} required; detected {DetectedLevel}"; - } - } - - public const int SqlServer2016 = 130; - public static readonly int DetectedLevel; - static FactRequiredCompatibilityLevelAttribute() - { - using (var conn = TestBase.GetOpenConnection()) - { - try - { - DetectedLevel = conn.QuerySingle("SELECT compatibility_level FROM sys.databases where name = DB_NAME()"); - } - catch { /* don't care */ } - } - } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class FactUnlessCaseSensitiveDatabaseAttribute : FactAttribute - { - public FactUnlessCaseSensitiveDatabaseAttribute() : base() - { - if (IsCaseSensitive) - { - Skip = "Case sensitive database"; - } - } - - public static readonly bool IsCaseSensitive; - static FactUnlessCaseSensitiveDatabaseAttribute() - { - using (var conn = TestBase.GetOpenConnection()) - { - try - { - conn.Execute("declare @i int; set @I = 1;"); - } - catch (SqlException s) - { - if (s.Number == 137) - IsCaseSensitive = true; - else - throw; - } - } - } - } -} diff --git a/Dapper.Tests/Helpers/XunitSkippable.cs b/Dapper.Tests/Helpers/XunitSkippable.cs deleted file mode 100644 index ffd3406c9..000000000 --- a/Dapper.Tests/Helpers/XunitSkippable.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; - -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Xunit.Abstractions; -using Xunit.Sdk; - -namespace Dapper.Tests -{ - public class SkipTestException : Exception - { - public SkipTestException(string reason) : base(reason) - { - } - } - - // Most of the below is a direct copy & port from the wonderful examples by Brad Wilson at - // https://github.com/xunit/samples.xunit/tree/master/DynamicSkipExample - public class SkippableFactDiscoverer : IXunitTestCaseDiscoverer - { - private readonly IMessageSink _diagnosticMessageSink; - - public SkippableFactDiscoverer(IMessageSink diagnosticMessageSink) - { - _diagnosticMessageSink = diagnosticMessageSink; - } - - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) - { - yield return new SkippableFactTestCase(_diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod); - } - } - - public class SkippableFactTestCase : XunitTestCase - { - [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] - public SkippableFactTestCase() - { - } - - public SkippableFactTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, ITestMethod testMethod, object[] testMethodArguments = null) - : base(diagnosticMessageSink, defaultMethodDisplay, testMethod, testMethodArguments) - { - } - - public override async Task RunAsync( - IMessageSink diagnosticMessageSink, - IMessageBus messageBus, - object[] constructorArguments, - ExceptionAggregator aggregator, - CancellationTokenSource cancellationTokenSource) - { - var skipMessageBus = new SkippableFactMessageBus(messageBus); - var result = await base.RunAsync( - diagnosticMessageSink, - skipMessageBus, - constructorArguments, - aggregator, - cancellationTokenSource).ConfigureAwait(false); - if (skipMessageBus.DynamicallySkippedTestCount > 0) - { - result.Failed -= skipMessageBus.DynamicallySkippedTestCount; - result.Skipped += skipMessageBus.DynamicallySkippedTestCount; - } - - return result; - } - } - - public class SkippableFactMessageBus : IMessageBus - { - private readonly IMessageBus _innerBus; - public SkippableFactMessageBus(IMessageBus innerBus) - { - _innerBus = innerBus; - } - - public int DynamicallySkippedTestCount { get; private set; } - - public void Dispose() - { - } - - public bool QueueMessage(IMessageSinkMessage message) - { - if (message is ITestFailed testFailed) - { - var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); - if (exceptionType == typeof(SkipTestException).FullName) - { - DynamicallySkippedTestCount++; - return _innerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); - } - } - return _innerBus.QueueMessage(message); - } - } -} diff --git a/Dapper.Tests/Providers/EntityFrameworkTests.cs b/Dapper.Tests/Providers/EntityFrameworkTests.cs deleted file mode 100644 index 1f991b01c..000000000 --- a/Dapper.Tests/Providers/EntityFrameworkTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -#if ENTITY_FRAMEWORK -using System.Data.Entity.Spatial; -using Xunit; - -namespace Dapper.Tests.Providers -{ - [Collection("TypeHandlerTests")] - public class EntityFrameworkTests : TestBase - { - public EntityFrameworkTests() - { - EntityFramework.Handlers.Register(); - } - - [Fact] - public void Issue570_DbGeo_HasValues() - { - EntityFramework.Handlers.Register(); - const string redmond = "POINT (122.1215 47.6740)"; - DbGeography point = DbGeography.PointFromText(redmond, DbGeography.DefaultCoordinateSystemId); - DbGeography orig = point.Buffer(20); - - var fromDb = connection.QuerySingle("declare @geos table(geo geography); insert @geos(geo) values(@val); select * from @geos", - new { val = orig }); - - Assert.NotNull(fromDb.Area); - Assert.Equal(orig.Area, fromDb.Area); - } - - [Fact] - public void Issue22_ExecuteScalar_EntityFramework() - { - var geo = DbGeography.LineFromText("LINESTRING(-122.360 47.656, -122.343 47.656 )", 4326); - var geo2 = connection.ExecuteScalar("select @geo", new { geo }); - Assert.NotNull(geo2); - } - } -} -#endif diff --git a/Dapper.Tests/Providers/FirebirdTests.cs b/Dapper.Tests/Providers/FirebirdTests.cs deleted file mode 100644 index f6974e8b8..000000000 --- a/Dapper.Tests/Providers/FirebirdTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using FirebirdSql.Data.FirebirdClient; -using System.Data; -using System.Linq; -using Xunit; - -namespace Dapper.Tests.Providers -{ - public class FirebirdTests : TestBase - { - [Fact(Skip = "Bug in Firebird; a PR to fix it has been submitted")] - public void Issue178_Firebird() - { - const string cs = "initial catalog=localhost:database;user id=SYSDBA;password=masterkey"; - - using (var connection = new FbConnection(cs)) - { - connection.Open(); - const string sql = "select count(*) from Issue178"; - try { connection.Execute("drop table Issue178"); } - catch { /* don't care */ } - connection.Execute("create table Issue178(id int not null)"); - connection.Execute("insert into Issue178(id) values(42)"); - // raw ADO.net - using (var sqlCmd = new FbCommand(sql, connection)) - using (IDataReader reader1 = sqlCmd.ExecuteReader()) - { - Assert.True(reader1.Read()); - Assert.Equal(1, reader1.GetInt32(0)); - Assert.False(reader1.Read()); - Assert.False(reader1.NextResult()); - } - - // dapper - using (var reader2 = connection.ExecuteReader(sql)) - { - Assert.True(reader2.Read()); - Assert.Equal(1, reader2.GetInt32(0)); - Assert.False(reader2.Read()); - Assert.False(reader2.NextResult()); - } - - var count = connection.Query(sql).Single(); - Assert.Equal(1, count); - } - } - } -} diff --git a/Dapper.Tests/Providers/MySQLTests.cs b/Dapper.Tests/Providers/MySQLTests.cs deleted file mode 100644 index aa39a89ad..000000000 --- a/Dapper.Tests/Providers/MySQLTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Linq; -using Xunit; - -namespace Dapper.Tests -{ - public class MySQLTests : TestBase - { - private static MySql.Data.MySqlClient.MySqlConnection GetMySqlConnection(bool open = true, - bool convertZeroDatetime = false, bool allowZeroDatetime = false) - { - string cs = IsAppVeyor - ? "Server=localhost;Database=test;Uid=root;Pwd=Password12!;" - : "Server=localhost;Database=tests;Uid=test;Pwd=pass;"; - var csb = new MySql.Data.MySqlClient.MySqlConnectionStringBuilder(cs) - { - AllowZeroDateTime = allowZeroDatetime, - ConvertZeroDateTime = convertZeroDatetime - }; - var conn = new MySql.Data.MySqlClient.MySqlConnection(csb.ConnectionString); - if (open) conn.Open(); - return conn; - } - - [FactMySql] - public void DapperEnumValue_Mysql() - { - using (var conn = GetMySqlConnection()) - { - Common.DapperEnumValue(conn); - } - } - - [FactMySql(Skip = "See https://github.com/StackExchange/Dapper/issues/552, not resolved on the MySQL end.")] - public void Issue552_SignedUnsignedBooleans() - { - using (var conn = GetMySqlConnection(true, false, false)) - { - conn.Execute(@" -CREATE TEMPORARY TABLE IF NOT EXISTS `bar` ( - `id` INT NOT NULL, - `bool_val` BOOL NULL, - PRIMARY KEY (`id`)); - - truncate table bar; - insert bar (id, bool_val) values (1, null); - insert bar (id, bool_val) values (2, 0); - insert bar (id, bool_val) values (3, 1); - insert bar (id, bool_val) values (4, null); - insert bar (id, bool_val) values (5, 1); - insert bar (id, bool_val) values (6, 0); - insert bar (id, bool_val) values (7, null); - insert bar (id, bool_val) values (8, 1);"); - - var rows = conn.Query("select * from bar;").ToDictionary(x => x.Id); - - Assert.Null(rows[1].Bool_Val); - Assert.False(rows[2].Bool_Val); - Assert.True(rows[3].Bool_Val); - Assert.Null(rows[4].Bool_Val); - Assert.True(rows[5].Bool_Val); - Assert.False(rows[6].Bool_Val); - Assert.Null(rows[7].Bool_Val); - Assert.True(rows[8].Bool_Val); - } - } - - private class MySqlHasBool - { - public int Id { get; set; } - public bool? Bool_Val { get; set; } - } - - [FactMySql] - public void Issue295_NullableDateTime_MySql_Default() - { - using (var conn = GetMySqlConnection(true, false, false)) - { - Common.TestDateTime(conn); - } - } - - [FactMySql] - public void Issue295_NullableDateTime_MySql_ConvertZeroDatetime() - { - using (var conn = GetMySqlConnection(true, true, false)) - { - Common.TestDateTime(conn); - } - } - - [FactMySql(Skip = "See https://github.com/StackExchange/Dapper/issues/295, AllowZeroDateTime=True is not supported")] - public void Issue295_NullableDateTime_MySql_AllowZeroDatetime() - { - using (var conn = GetMySqlConnection(true, false, true)) - { - Common.TestDateTime(conn); - } - } - - [FactMySql(Skip = "See https://github.com/StackExchange/Dapper/issues/295, AllowZeroDateTime=True is not supported")] - public void Issue295_NullableDateTime_MySql_ConvertAllowZeroDatetime() - { - using (var conn = GetMySqlConnection(true, true, true)) - { - Common.TestDateTime(conn); - } - } - - [FactMySql] - public void Issue426_SO34439033_DateTimeGainsTicks() - { - using (var conn = GetMySqlConnection(true, true, true)) - { - try { conn.Execute("drop table Issue426_Test"); } catch { /* don't care */ } - try { conn.Execute("create table Issue426_Test (Id int not null, Time time not null)"); } catch { /* don't care */ } - const long ticks = 553440000000; - const int Id = 426; - - var localObj = new Issue426_Test - { - Id = Id, - Time = TimeSpan.FromTicks(ticks) // from code example - }; - conn.Execute("replace into Issue426_Test values (@Id,@Time)", localObj); - - var dbObj = conn.Query("select * from Issue426_Test where Id = @id", new { id = Id }).Single(); - Assert.Equal(Id, dbObj.Id); - Assert.Equal(ticks, dbObj.Time.Value.Ticks); - } - } - - [FactMySql] - public void SO36303462_Tinyint_Bools() - { - using (var conn = GetMySqlConnection(true, true, true)) - { - try { conn.Execute("drop table SO36303462_Test"); } catch { /* don't care */ } - conn.Execute("create table SO36303462_Test (Id int not null, IsBold tinyint not null);"); - conn.Execute("insert SO36303462_Test (Id, IsBold) values (1,1);"); - conn.Execute("insert SO36303462_Test (Id, IsBold) values (2,0);"); - conn.Execute("insert SO36303462_Test (Id, IsBold) values (3,1);"); - - var rows = conn.Query("select * from SO36303462_Test").ToDictionary(x => x.Id); - Assert.Equal(3, rows.Count); - Assert.True(rows[1].IsBold); - Assert.False(rows[2].IsBold); - Assert.True(rows[3].IsBold); - } - } - - private class SO36303462 - { - public int Id { get; set; } - public bool IsBold { get; set; } - } - - public class Issue426_Test - { - public long Id { get; set; } - public TimeSpan? Time { get; set; } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class FactMySqlAttribute : FactAttribute - { - public override string Skip - { - get { return unavailable ?? base.Skip; } - set { base.Skip = value; } - } - - private static readonly string unavailable; - - static FactMySqlAttribute() - { - try - { - using (GetMySqlConnection(true)) { /* just trying to see if it works */ } - } - catch (Exception ex) - { - unavailable = $"MySql is unavailable: {ex.Message}"; - } - } - } - } -} diff --git a/Dapper.Tests/Providers/OLDEBTests.cs b/Dapper.Tests/Providers/OLDEBTests.cs deleted file mode 100644 index 932b5bfbf..000000000 --- a/Dapper.Tests/Providers/OLDEBTests.cs +++ /dev/null @@ -1,272 +0,0 @@ -#if OLEDB -using System; -using System.Data.OleDb; -using System.Linq; -using Xunit; - -namespace Dapper.Tests -{ - public class OLDEBTests : TestBase - { - public static string OleDbConnectionString => - IsAppVeyor - ? @"Provider=SQLOLEDB;Data Source=(local)\SQL2016;Initial Catalog=tempdb;User Id=sa;Password=Password12!" - : "Provider=SQLOLEDB;Data Source=.;Initial Catalog=tempdb;Integrated Security=SSPI"; - - public OleDbConnection GetOleDbConnection() - { - var conn = new OleDbConnection(OleDbConnectionString); - conn.Open(); - return conn; - } - - // see https://stackoverflow.com/q/18847510/23354 - [Fact] - public void TestOleDbParameters() - { - using (var conn = GetOleDbConnection()) - { - var row = conn.Query("select Id = ?, Age = ?", - new { foo = 12, bar = 23 } // these names DO NOT MATTER!!! - ).Single(); - int age = row.Age; - int id = row.Id; - Assert.Equal(23, age); - Assert.Equal(12, id); - } - } - - [Fact] - public void PseudoPositionalParameters_Simple() - { - using (var connection = GetOleDbConnection()) - { - int value = connection.Query("select ?x? + ?y_2? + ?z?", new { x = 1, y_2 = 3, z = 5, z2 = 24 }).Single(); - Assert.Equal(9, value); - } - } - - [Fact] - public void Issue601_InternationalParameterNamesWork_OleDb() - { - // pseudo-positional - using (var connection = GetOleDbConnection()) - { - int value = connection.QuerySingle("select ?æøå٦?", new { æøå٦ = 42 }); - } - } - - [Fact] - public void PseudoPositionalParameters_Dynamic() - { - using (var connection = GetOleDbConnection()) - { - var args = new DynamicParameters(); - args.Add("x", 1); - args.Add("y_2", 3); - args.Add("z", 5); - args.Add("z2", 24); - int value = connection.Query("select ?x? + ?y_2? + ?z?", args).Single(); - Assert.Equal(9, value); - } - } - - [Fact] - public void PseudoPositionalParameters_ReusedParameter() - { - using (var connection = GetOleDbConnection()) - { - var ex = Assert.Throws(() => connection.Query("select ?x? + ?y_2? + ?x?", new { x = 1, y_2 = 3 }).Single()); - Assert.Equal("When passing parameters by position, each parameter can only be referenced once", ex.Message); - } - } - - [Fact] - public void Issue569_SO38527197_PseudoPositionalParameters_In() - { - using (var connection = GetOleDbConnection()) - { - int[] ids = { 1, 2, 5, 7 }; - var list = connection.Query("select * from string_split('1,2,3,4,5',',') where value in ?ids?", new { ids }).AsList(); - list.Sort(); - Assert.Equal("1,2,5", string.Join(",", list)); - } - } - - [Fact] - public void PseudoPositional_CanUseVariable() - { - using (var connection = GetOleDbConnection()) - { - const int id = 42; - var row = connection.QuerySingle("declare @id int = ?id?; select @id as [A], @id as [B];", new { id }); - int a = (int)row.A; - int b = (int)row.B; - Assert.Equal(42, a); - Assert.Equal(42, b); - } - } - - [Fact] - public void PseudoPositional_CannotUseParameterMultipleTimes() - { - using (var connection = GetOleDbConnection()) - { - var ex = Assert.Throws(() => - { - const int id = 42; - connection.QuerySingle("select ?id? as [A], ?id? as [B];", new { id }); - }); - Assert.Equal("When passing parameters by position, each parameter can only be referenced once", ex.Message); - } - } - - [Fact] - public void PseudoPositionalParameters_ExecSingle() - { - using (var connection = GetOleDbConnection()) - { - var data = new { x = 6 }; - connection.Execute("create table #named_single(val int not null)"); - int count = connection.Execute("insert #named_single (val) values (?x?)", data); - int sum = (int)connection.ExecuteScalar("select sum(val) from #named_single"); - Assert.Equal(1, count); - Assert.Equal(6, sum); - } - } - - [Fact] - public void PseudoPositionalParameters_ExecMulti() - { - using (var connection = GetOleDbConnection()) - { - var data = new[] - { - new { x = 1, y = 1 }, - new { x = 3, y = 1 }, - new { x = 6, y = 1 }, - }; - connection.Execute("create table #named_multi(val int not null)"); - int count = connection.Execute("insert #named_multi (val) values (?x?)", data); - int sum = (int)connection.ExecuteScalar("select sum(val) from #named_multi"); - Assert.Equal(3, count); - Assert.Equal(10, sum); - } - } - - [Fact] - public void Issue457_NullParameterValues() - { - const string sql = @" -DECLARE @since DATETIME, @customerCode nvarchar(10) -SET @since = ? -- ODBC parameter -SET @customerCode = ? -- ODBC parameter - -SELECT @since as [Since], @customerCode as [Code]"; - - using (var connection = GetOleDbConnection()) - { - DateTime? since = null; // DateTime.Now.Date; - const string code = null; // "abc"; - var row = connection.QuerySingle(sql, new - { - since, - customerCode = code - }); - var a = (DateTime?)row.Since; - var b = (string)row.Code; - - Assert.Equal(since, a); - Assert.Equal(code, b); - } - } - - [Fact] - public void Issue457_NullParameterValues_Named() - { - const string sql = @" -DECLARE @since DATETIME, @customerCode nvarchar(10) -SET @since = ?since? -- ODBC parameter -SET @customerCode = ?customerCode? -- ODBC parameter - -SELECT @since as [Since], @customerCode as [Code]"; - - using (var connection = GetOleDbConnection()) - { - DateTime? since = null; // DateTime.Now.Date; - const string code = null; // "abc"; - var row = connection.QuerySingle(sql, new - { - since, - customerCode = code - }); - var a = (DateTime?)row.Since; - var b = (string)row.Code; - - Assert.Equal(since, a); - Assert.Equal(code, b); - } - } - - [Fact] - public async void Issue457_NullParameterValues_MultiAsync() - { - const string sql = @" -DECLARE @since DATETIME, @customerCode nvarchar(10) -SET @since = ? -- ODBC parameter -SET @customerCode = ? -- ODBC parameter - -SELECT @since as [Since], @customerCode as [Code]"; - - using (var connection = GetOleDbConnection()) - { - DateTime? since = null; // DateTime.Now.Date; - const string code = null; // "abc"; - using (var multi = await connection.QueryMultipleAsync(sql, new - { - since, - customerCode = code - }).ConfigureAwait(false)) - { - var row = await multi.ReadSingleAsync().ConfigureAwait(false); - var a = (DateTime?)row.Since; - var b = (string)row.Code; - - Assert.Equal(a, since); - Assert.Equal(b, code); - } - } - } - - [Fact] - public async void Issue457_NullParameterValues_MultiAsync_Named() - { - const string sql = @" -DECLARE @since DATETIME, @customerCode nvarchar(10) -SET @since = ?since? -- ODBC parameter -SET @customerCode = ?customerCode? -- ODBC parameter - -SELECT @since as [Since], @customerCode as [Code]"; - - using (var connection = GetOleDbConnection()) - { - DateTime? since = null; // DateTime.Now.Date; - const string code = null; // "abc"; - using (var multi = await connection.QueryMultipleAsync(sql, new - { - since, - customerCode = code - }).ConfigureAwait(false)) - { - var row = await multi.ReadSingleAsync().ConfigureAwait(false); - var a = (DateTime?)row.Since; - var b = (string)row.Code; - - Assert.Equal(a, since); - Assert.Equal(b, code); - } - } - } - } -} -#endif diff --git a/Dapper.Tests/Providers/PostgresqlTests.cs b/Dapper.Tests/Providers/PostgresqlTests.cs deleted file mode 100644 index 6ec20c3db..000000000 --- a/Dapper.Tests/Providers/PostgresqlTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Data; -using System.Linq; -using Xunit; - -namespace Dapper.Tests -{ - public class PostcresqlTests : TestBase - { - private static Npgsql.NpgsqlConnection GetOpenNpgsqlConnection() - { - string cs = IsAppVeyor - ? "Server=localhost;Port=5432;User Id=postgres;Password=Password12!;Database=test" - : "Server=localhost;Port=5432;User Id=dappertest;Password=dapperpass;Database=dappertest"; // ;Encoding = UNICODE - var conn = new Npgsql.NpgsqlConnection(cs); - conn.Open(); - return conn; - } - - private class Cat - { - public int Id { get; set; } - public string Breed { get; set; } - public string Name { get; set; } - } - - private readonly Cat[] Cats = - { - new Cat() { Breed = "Abyssinian", Name="KACTUS"}, - new Cat() { Breed = "Aegean cat", Name="KADAFFI"}, - new Cat() { Breed = "American Bobtail", Name="KANJI"}, - new Cat() { Breed = "Balinese", Name="MACARONI"}, - new Cat() { Breed = "Bombay", Name="MACAULAY"}, - new Cat() { Breed = "Burmese", Name="MACBETH"}, - new Cat() { Breed = "Chartreux", Name="MACGYVER"}, - new Cat() { Breed = "German Rex", Name="MACKENZIE"}, - new Cat() { Breed = "Javanese", Name="MADISON"}, - new Cat() { Breed = "Persian", Name="MAGNA"} - }; - - [FactPostgresql] - public void TestPostgresqlArrayParameters() - { - using (var conn = GetOpenNpgsqlConnection()) - { - IDbTransaction transaction = conn.BeginTransaction(); - conn.Execute("create table tcat ( id serial not null, breed character varying(20) not null, name character varying (20) not null);"); - conn.Execute("insert into tcat(breed, name) values(:breed, :name) ", Cats); - - var r = conn.Query("select * from tcat where id=any(:catids)", new { catids = new[] { 1, 3, 5 } }); - Assert.Equal(3, r.Count()); - Assert.Equal(1, r.Count(c => c.Id == 1)); - Assert.Equal(1, r.Count(c => c.Id == 3)); - Assert.Equal(1, r.Count(c => c.Id == 5)); - transaction.Rollback(); - } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class FactPostgresqlAttribute : FactAttribute - { - public override string Skip - { - get { return unavailable ?? base.Skip; } - set { base.Skip = value; } - } - - private static readonly string unavailable; - - static FactPostgresqlAttribute() - { - try - { - using (GetOpenNpgsqlConnection()) { /* just trying to see if it works */ } - } - catch (Exception ex) - { - unavailable = $"Postgresql is unavailable: {ex.Message}"; - } - } - } - } -} \ No newline at end of file diff --git a/Dapper.Tests/Providers/SQLCETests.cs b/Dapper.Tests/Providers/SQLCETests.cs deleted file mode 100644 index f435bd4e8..000000000 --- a/Dapper.Tests/Providers/SQLCETests.cs +++ /dev/null @@ -1,56 +0,0 @@ -#if SQL_CE -using System.Data.SqlServerCe; -using System.IO; -using System.Linq; -using Xunit; - -namespace Dapper.Tests -{ - public class SQLCETests : TestBase - { - [Fact] - public void MultiRSSqlCE() - { - if (File.Exists("Test.DB.sdf")) - File.Delete("Test.DB.sdf"); - - const string cnnStr = "Data Source = Test.DB.sdf;"; - var engine = new SqlCeEngine(cnnStr); - engine.CreateDatabase(); - - using (var cnn = new SqlCeConnection(cnnStr)) - { - cnn.Open(); - - cnn.Execute("create table Posts (ID int, Title nvarchar(50), Body nvarchar(50), AuthorID int)"); - cnn.Execute("create table Authors (ID int, Name nvarchar(50))"); - - cnn.Execute("insert Posts values (1,'title','body',1)"); - cnn.Execute("insert Posts values(2,'title2','body2',null)"); - cnn.Execute("insert Authors values(1,'sam')"); - - var data = cnn.Query("select * from Posts p left join Authors a on a.ID = p.AuthorID", (post, author) => { post.Author = author; return post; }).ToList(); - var firstPost = data[0]; - Assert.Equal("title", firstPost.Title); - Assert.Equal("sam", firstPost.Author.Name); - Assert.Null(data[1].Author); - } - } - - public class PostCE - { - public int ID { get; set; } - public string Title { get; set; } - public string Body { get; set; } - - public AuthorCE Author { get; set; } - } - - public class AuthorCE - { - public int ID { get; set; } - public string Name { get; set; } - } - } -} -#endif diff --git a/Dapper.Tests/Providers/SqliteTests.cs b/Dapper.Tests/Providers/SqliteTests.cs deleted file mode 100644 index 8aa750791..000000000 --- a/Dapper.Tests/Providers/SqliteTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -using Microsoft.Data.Sqlite; -using System; -using System.Linq; -using System.Threading.Tasks; -using Xunit; - -namespace Dapper.Tests -{ - public class SqliteTests : TestBase - { - protected static SqliteConnection GetSQLiteConnection(bool open = true) - { - var connection = new SqliteConnection("Data Source=:memory:"); - if (open) connection.Open(); - return connection; - } - - [FactSqlite] - public void DapperEnumValue_Sqlite() - { - using (var connection = GetSQLiteConnection()) - { - Common.DapperEnumValue(connection); - } - } - - [Collection(NonParallelDefinition.Name)] - public class SqliteTypeHandlerTests : TestBase - { - [FactSqlite] - public void Issue466_SqliteHatesOptimizations() - { - using (var connection = GetSQLiteConnection()) - { - SqlMapper.ResetTypeHandlers(); - var row = connection.Query("select 42 as Id").First(); - Assert.Equal(42, row.Id); - row = connection.Query("select 42 as Id").First(); - Assert.Equal(42, row.Id); - - SqlMapper.ResetTypeHandlers(); - row = connection.QueryFirst("select 42 as Id"); - Assert.Equal(42, row.Id); - row = connection.QueryFirst("select 42 as Id"); - Assert.Equal(42, row.Id); - } - } - - [FactSqlite] - public async Task Issue466_SqliteHatesOptimizations_Async() - { - using (var connection = GetSQLiteConnection()) - { - SqlMapper.ResetTypeHandlers(); - var row = (await connection.QueryAsync("select 42 as Id").ConfigureAwait(false)).First(); - Assert.Equal(42, row.Id); - row = (await connection.QueryAsync("select 42 as Id").ConfigureAwait(false)).First(); - Assert.Equal(42, row.Id); - - SqlMapper.ResetTypeHandlers(); - row = await connection.QueryFirstAsync("select 42 as Id").ConfigureAwait(false); - Assert.Equal(42, row.Id); - row = await connection.QueryFirstAsync("select 42 as Id").ConfigureAwait(false); - Assert.Equal(42, row.Id); - } - } - } - - [FactSqlite] - public void Isse467_SqliteLikesParametersWithPrefix() - { - Isse467_SqliteParameterNaming(true); - } - - [FactSqlite] - public void Isse467_SqliteLikesParametersWithoutPrefix() - { // see issue 375 / 467; note: fixed from RC2 onwards - Isse467_SqliteParameterNaming(false); - } - - private void Isse467_SqliteParameterNaming(bool prefix) - { - using (var connection = GetSQLiteConnection()) - { - var cmd = connection.CreateCommand(); - cmd.CommandText = "select @foo"; - const SqliteType type = SqliteType.Integer; - cmd.Parameters.Add(prefix ? "@foo" : "foo", type).Value = 42; - var i = Convert.ToInt32(cmd.ExecuteScalar()); - Assert.Equal(42, i); - } - } - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class FactSqliteAttribute : FactAttribute - { - public override string Skip - { - get { return unavailable ?? base.Skip; } - set { base.Skip = value; } - } - - private static readonly string unavailable; - - static FactSqliteAttribute() - { - try - { - using (GetSQLiteConnection()) - { - } - } - catch (Exception ex) - { - unavailable = $"Sqlite is unavailable: {ex.Message}"; - } - } - } - } -} \ No newline at end of file diff --git a/Dapper.Tests/SharedTypes/Address.cs b/Dapper.Tests/SharedTypes/Address.cs deleted file mode 100644 index 3aaa5a560..000000000 --- a/Dapper.Tests/SharedTypes/Address.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Dapper.Tests -{ - public class Address - { - public int AddressId { get; set; } - public string Name { get; set; } - public int PersonId { get; set; } - } -} diff --git a/Dapper.Tests/SharedTypes/Person.cs b/Dapper.Tests/SharedTypes/Person.cs deleted file mode 100644 index 269eff0ad..000000000 --- a/Dapper.Tests/SharedTypes/Person.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Dapper.Tests -{ - public class Person - { - public int PersonId { get; set; } - public string Name { get; set; } - public string Occupation { get; private set; } - public int NumberOfLegs = 2; - public Address Address { get; set; } - } -} diff --git a/Dapper.Tests/SharedTypes/Post.cs b/Dapper.Tests/SharedTypes/Post.cs deleted file mode 100644 index 9c017d6d7..000000000 --- a/Dapper.Tests/SharedTypes/Post.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Dapper.Tests -{ - public class Post - { - public int Id { get; set; } - public User Owner { get; set; } - public string Content { get; set; } - public Comment Comment { get; set; } - } -} diff --git a/Dapper.Tests/SharedTypes/ReviewBoard.cs b/Dapper.Tests/SharedTypes/ReviewBoard.cs deleted file mode 100644 index 06a5473a7..000000000 --- a/Dapper.Tests/SharedTypes/ReviewBoard.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dapper.Tests -{ - public class ReviewBoard - { - public int Id { get; set; } - public string Name { get; set; } - public User User1 { get; set; } - public User User2 { get; set; } - public User User3 { get; set; } - public User User4 { get; set; } - public User User5 { get; set; } - public User User6 { get; set; } - public User User7 { get; set; } - public User User8 { get; set; } - public User User9 { get; set; } - } -} diff --git a/Dapper.Tests/TestBase.cs b/Dapper.Tests/TestBase.cs deleted file mode 100644 index 312356206..000000000 --- a/Dapper.Tests/TestBase.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using System.Globalization; -using Xunit; -#if !NETCOREAPP1_0 -using System.Threading; -#endif - -namespace Dapper.Tests -{ - public abstract class TestBase : IDisposable - { - protected static readonly bool IsAppVeyor = Environment.GetEnvironmentVariable("Appveyor")?.ToUpperInvariant() == "TRUE"; - - public static string ConnectionString => - IsAppVeyor - ? @"Server=(local)\SQL2016;Database=tempdb;User ID=sa;Password=Password12!" - : "Data Source=.;Initial Catalog=tempdb;Integrated Security=True"; - - protected SqlConnection _connection; - protected SqlConnection connection => _connection ?? (_connection = GetOpenConnection()); - - public static SqlConnection GetOpenConnection(bool mars = false) - { - var cs = ConnectionString; - if (mars) - { - var scsb = new SqlConnectionStringBuilder(cs) - { - MultipleActiveResultSets = true - }; - cs = scsb.ConnectionString; - } - var connection = new SqlConnection(cs); - connection.Open(); - return connection; - } - - public SqlConnection GetClosedConnection() - { - var conn = new SqlConnection(ConnectionString); - if (conn.State != ConnectionState.Closed) throw new InvalidOperationException("should be closed!"); - return conn; - } - - protected static CultureInfo ActiveCulture - { -#if NETCOREAPP1_0 - get { return CultureInfo.CurrentCulture; } - set { CultureInfo.CurrentCulture = value; } -#else - get { return Thread.CurrentThread.CurrentCulture; } - set { Thread.CurrentThread.CurrentCulture = value; } -#endif - } - - static TestBase() - { - Console.WriteLine("Dapper: " + typeof(SqlMapper).AssemblyQualifiedName); - Console.WriteLine("Using Connectionstring: {0}", ConnectionString); -#if NETCOREAPP1_0 - Console.WriteLine("CoreCLR (netcoreapp1.0)"); -#else - Console.WriteLine(".NET: " + Environment.Version); - Console.Write("Loading native assemblies for SQL types..."); - try - { - SqlServerTypesLoader.LoadNativeAssemblies(AppDomain.CurrentDomain.BaseDirectory); - Console.WriteLine("done."); - } - catch (Exception ex) - { - Console.WriteLine("failed."); - Console.Error.WriteLine(ex.Message); - } -#endif - } - - public void Dispose() - { - _connection?.Dispose(); - } - } - - [CollectionDefinition(Name, DisableParallelization = true)] - public class NonParallelDefinition : TestBase - { - public const string Name = "NonParallel"; - } -} diff --git a/Dapper.Tests/TupleTests.cs b/Dapper.Tests/TupleTests.cs deleted file mode 100644 index 562397b7e..000000000 --- a/Dapper.Tests/TupleTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using Xunit; - -namespace Dapper.Tests -{ - public class TupleTests : TestBase - { - [Fact] - public void TupleStructParameter_Fails_HelpfulMessage() - { - var ex = Assert.Throws(() => connection.QuerySingle("select @id", (id: 42, name: "Fred"))); - Assert.Equal("ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing", ex.Message); - } - - [Fact] - public void TupleClassParameter_Works() - { - Assert.Equal(42, connection.QuerySingle("select @Item1", Tuple.Create(42, "Fred"))); - } - - [Fact] - public void TupleReturnValue_Works_ByPosition() - { - var val = connection.QuerySingle<(int id, string name)>("select 42, 'Fred'"); - Assert.Equal(42, val.id); - Assert.Equal("Fred", val.name); - } - - [Fact] - public void TupleReturnValue_TooManyColumns_Ignored() - { - var val = connection.QuerySingle<(int id, string name)>("select 42, 'Fred', 123"); - Assert.Equal(42, val.id); - Assert.Equal("Fred", val.name); - } - - [Fact] - public void TupleReturnValue_TooFewColumns_Unmapped() - { - // I'm very wary of making this throw, but I can also see some sense in pointing out the oddness - var val = connection.QuerySingle<(int id, string name, int extra)>("select 42, 'Fred'"); - Assert.Equal(42, val.id); - Assert.Equal("Fred", val.name); - Assert.Equal(0, val.extra); - } - - [Fact] - public void TupleReturnValue_Works_NamesIgnored() - { - var val = connection.QuerySingle<(int id, string name)>("select 42 as [Item2], 'Fred' as [Item1]"); - Assert.Equal(42, val.id); - Assert.Equal("Fred", val.name); - } - } -} diff --git a/Dapper.Tests/XmlTests.cs b/Dapper.Tests/XmlTests.cs deleted file mode 100644 index 50aa322fb..000000000 --- a/Dapper.Tests/XmlTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Xml; -using System.Xml.Linq; -using Xunit; - -namespace Dapper.Tests -{ - public class XmlTests : TestBase - { - [Fact] - public void CommonXmlTypesSupported() - { - var xml = new XmlDocument(); - xml.LoadXml(""); - - var foo = new Foo - { - A = xml, - B = XDocument.Parse(""), - C = XElement.Parse("") - }; - var bar = connection.QuerySingle("select @a as [A], @b as [B], @c as [C]", new { a = foo.A, b = foo.B, c = foo.C }); - Assert.Equal("abc", bar.A.DocumentElement.Name); - Assert.Equal("def", bar.B.Root.Name.LocalName); - Assert.Equal("ghi", bar.C.Name.LocalName); - } - - public class Foo - { - public XmlDocument A { get; set; } - public XDocument B { get; set; } - public XElement C { get; set; } - } - } -} diff --git a/Dapper.png b/Dapper.png new file mode 100644 index 000000000..ec2c30fb5 Binary files /dev/null and b/Dapper.png differ diff --git a/Dapper.sln b/Dapper.sln.old similarity index 76% rename from Dapper.sln rename to Dapper.sln.old index adb25c8a1..e324cc009 100644 --- a/Dapper.sln +++ b/Dapper.sln.old @@ -1,110 +1,121 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.8 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A34907DF-958A-4E4C-8491-84CF303FD13E}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - appveyor.yml = appveyor.yml - build.ps1 = build.ps1 - build.sh = build.sh - License.txt = License.txt - nuget.config = nuget.config - Readme.md = Readme.md - semver.txt = semver.txt - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper", "Dapper\Dapper.csproj", "{FAC24C3F-68F9-4247-A4B9-21D487E99275}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.StrongName", "Dapper.StrongName\Dapper.StrongName.csproj", "{549C51A1-222B-4E12-96F1-3AEFF45A7709}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests", "Dapper.Tests\Dapper.Tests.csproj", "{052C0817-DB26-4925-8929-8C5E42D148D5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Contrib", "Dapper.Contrib\Dapper.Contrib.csproj", "{4E409F8F-CFBB-4332-8B0A-FD5A283051FD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework", "Dapper.EntityFramework\Dapper.EntityFramework.csproj", "{BE401F7B-8611-4A1E-AEAA-5CB700128C16}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Contrib", "Dapper.Tests.Contrib\Dapper.Tests.Contrib.csproj", "{DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.SqlBuilder", "Dapper.SqlBuilder\Dapper.SqlBuilder.csproj", "{196928F0-7052-4585-90E8-817BD720F5E3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Rainbow", "Dapper.Rainbow\Dapper.Rainbow.csproj", "{8A74F0B6-188F-45D2-8A4B-51E4F211805A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}" - ProjectSection(SolutionItems) = preProject - Directory.build.props = Directory.build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{568BD46C-1C65-4D44-870C-12CD72563262}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework.StrongName", "Dapper.EntityFramework.StrongName\Dapper.EntityFramework.StrongName.csproj", "{39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Performance", "Dapper.Tests.Performance\Dapper.Tests.Performance.csproj", "{F017075A-2969-4A8E-8971-26F154EB420F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.Build.0 = Release|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.Build.0 = Debug|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.ActiveCfg = Release|Any CPU - {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.Build.0 = Release|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.Build.0 = Release|Any CPU - {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Release|Any CPU.Build.0 = Release|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.Build.0 = Release|Any CPU - {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Release|Any CPU.Build.0 = Release|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.Build.0 = Release|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.Build.0 = Release|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.Build.0 = Release|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {FAC24C3F-68F9-4247-A4B9-21D487E99275} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {549C51A1-222B-4E12-96F1-3AEFF45A7709} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {052C0817-DB26-4925-8929-8C5E42D148D5} = {568BD46C-1C65-4D44-870C-12CD72563262} - {4E409F8F-CFBB-4332-8B0A-FD5A283051FD} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {BE401F7B-8611-4A1E-AEAA-5CB700128C16} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A} = {568BD46C-1C65-4D44-870C-12CD72563262} - {196928F0-7052-4585-90E8-817BD720F5E3} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {8A74F0B6-188F-45D2-8A4B-51E4F211805A} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} - {F017075A-2969-4A8E-8971-26F154EB420F} = {568BD46C-1C65-4D44-870C-12CD72563262} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {928A4226-96F3-409A-8A83-9E7444488710} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.33906.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A34907DF-958A-4E4C-8491-84CF303FD13E}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + appveyor.yml = appveyor.yml + Build.csproj = Build.csproj + build.ps1 = build.ps1 + Dapper.png = Dapper.png + Directory.Build.props = Directory.Build.props + Directory.Packages.props = Directory.Packages.props + global.json = global.json + docs\index.md = docs\index.md + License.txt = License.txt + .github\workflows\main.yml = .github\workflows\main.yml + NonCLA.md = NonCLA.md + nuget.config = nuget.config + Readme.md = Readme.md + version.json = version.json + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper", "Dapper\Dapper.csproj", "{FAC24C3F-68F9-4247-A4B9-21D487E99275}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.StrongName", "Dapper.StrongName\Dapper.StrongName.csproj", "{549C51A1-222B-4E12-96F1-3AEFF45A7709}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests", "tests\Dapper.Tests\Dapper.Tests.csproj", "{052C0817-DB26-4925-8929-8C5E42D148D5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework", "Dapper.EntityFramework\Dapper.EntityFramework.csproj", "{BE401F7B-8611-4A1E-AEAA-5CB700128C16}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.SqlBuilder", "Dapper.SqlBuilder\Dapper.SqlBuilder.csproj", "{196928F0-7052-4585-90E8-817BD720F5E3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Rainbow", "Dapper.Rainbow\Dapper.Rainbow.csproj", "{8A74F0B6-188F-45D2-8A4B-51E4F211805A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{568BD46C-1C65-4D44-870C-12CD72563262}" + ProjectSection(SolutionItems) = preProject + tests\Directory.Build.props = tests\Directory.Build.props + tests\Directory.Build.targets = tests\Directory.Build.targets + tests\docker-compose.yml = tests\docker-compose.yml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.EntityFramework.StrongName", "Dapper.EntityFramework.StrongName\Dapper.EntityFramework.StrongName.csproj", "{39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Performance", "benchmarks\Dapper.Tests.Performance\Dapper.Tests.Performance.csproj", "{F017075A-2969-4A8E-8971-26F154EB420F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.ProviderTools", "Dapper.ProviderTools\Dapper.ProviderTools.csproj", "{B06DB435-0C74-4BD3-BC97-52AF7CF9916B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{9D960D4D-80A2-4DAC-B386-8F4235EC73E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "docs", "docs\docs.csproj", "{C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAC24C3F-68F9-4247-A4B9-21D487E99275}.Release|Any CPU.Build.0 = Release|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Debug|Any CPU.Build.0 = Debug|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.ActiveCfg = Release|Any CPU + {549C51A1-222B-4E12-96F1-3AEFF45A7709}.Release|Any CPU.Build.0 = Release|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {052C0817-DB26-4925-8929-8C5E42D148D5}.Release|Any CPU.Build.0 = Release|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE401F7B-8611-4A1E-AEAA-5CB700128C16}.Release|Any CPU.Build.0 = Release|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {196928F0-7052-4585-90E8-817BD720F5E3}.Release|Any CPU.Build.0 = Release|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A74F0B6-188F-45D2-8A4B-51E4F211805A}.Release|Any CPU.Build.0 = Release|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB}.Release|Any CPU.Build.0 = Release|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F017075A-2969-4A8E-8971-26F154EB420F}.Release|Any CPU.Build.0 = Release|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B}.Release|Any CPU.Build.0 = Release|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FAC24C3F-68F9-4247-A4B9-21D487E99275} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {549C51A1-222B-4E12-96F1-3AEFF45A7709} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {052C0817-DB26-4925-8929-8C5E42D148D5} = {568BD46C-1C65-4D44-870C-12CD72563262} + {BE401F7B-8611-4A1E-AEAA-5CB700128C16} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {196928F0-7052-4585-90E8-817BD720F5E3} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {8A74F0B6-188F-45D2-8A4B-51E4F211805A} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {39D3EEB6-9C05-4F4A-8C01-7B209742A7EB} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {F017075A-2969-4A8E-8971-26F154EB420F} = {568BD46C-1C65-4D44-870C-12CD72563262} + {B06DB435-0C74-4BD3-BC97-52AF7CF9916B} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} + {C2F722AC-B2D4-4E97-AF8E-C036B3D9211F} = {9D960D4D-80A2-4DAC-B386-8F4235EC73E6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {928A4226-96F3-409A-8A83-9E7444488710} + EndGlobalSection +EndGlobal diff --git a/Dapper.slnx b/Dapper.slnx new file mode 100644 index 000000000..674239170 --- /dev/null +++ b/Dapper.slnx @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs index 110cd5b2b..19963ba67 100644 --- a/Dapper/CommandDefinition.cs +++ b/Dapper/CommandDefinition.cs @@ -9,18 +9,11 @@ namespace Dapper /// /// Represents the key aspects of a sql operation /// - public struct CommandDefinition + public readonly struct CommandDefinition { - internal static CommandDefinition ForCallback(object parameters) + internal static CommandDefinition ForCallback(object? parameters, CommandFlags flags) { - if (parameters is DynamicParameters) - { - return new CommandDefinition(parameters); - } - else - { - return default(CommandDefinition); - } + return new CommandDefinition(parameters is DynamicParameters ? parameters : null, flags); } internal void OnCompleted() @@ -36,22 +29,27 @@ internal void OnCompleted() /// /// The parameters associated with the command /// - public object Parameters { get; } + public object? Parameters { get; } /// /// The active transaction for the command /// - public IDbTransaction Transaction { get; } + public IDbTransaction? Transaction { get; } /// /// The effective timeout for the command /// public int? CommandTimeout { get; } + internal readonly CommandType CommandTypeDirect; + /// /// The type of command that the command-text represents /// - public CommandType? CommandType { get; } +#if DEBUG // prevent use in our own code + [Obsolete("Prefer " + nameof(CommandTypeDirect), true)] +#endif + public CommandType? CommandType => CommandTypeDirect; /// /// Should data be buffered before returning? @@ -83,23 +81,36 @@ internal void OnCompleted() /// The for this command. /// The behavior flags for this command. /// The cancellation token for this command. - public CommandDefinition(string commandText, object parameters = null, IDbTransaction transaction = null, int? commandTimeout = null, + public CommandDefinition(string commandText, object? parameters = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null, CommandFlags flags = CommandFlags.Buffered - , CancellationToken cancellationToken = default(CancellationToken) + , CancellationToken cancellationToken = default ) { CommandText = commandText; Parameters = parameters; Transaction = transaction; CommandTimeout = commandTimeout; - CommandType = commandType; + CommandTypeDirect = commandType ?? InferCommandType(commandText); Flags = flags; CancellationToken = cancellationToken; } - private CommandDefinition(object parameters) : this() + internal static CommandType InferCommandType(string sql) + { + // if the sql contains any whitespace character (space/tab/cr/lf/etc - via unicode), + // has operators, comments, semi-colon, or a known exception: interpret as ad-hoc; + // otherwise, simple names like "SomeName" should be treated as a stored-proc + // (note TableDirect would need to be specified explicitly, but in reality providers don't usually support TableDirect anyway) + + if (sql is null || CompiledRegex.WhitespaceOrReserved.IsMatch(sql)) return System.Data.CommandType.Text; + return System.Data.CommandType.StoredProcedure; + } + + private CommandDefinition(object? parameters, CommandFlags flags) : this() { Parameters = parameters; + Flags = flags; + CommandText = ""; } /// @@ -107,12 +118,12 @@ private CommandDefinition(object parameters) : this() /// public CancellationToken CancellationToken { get; } - internal IDbCommand SetupCommand(IDbConnection cnn, Action paramReader) + internal IDbCommand SetupCommand(IDbConnection cnn, Action? paramReader) { var cmd = cnn.CreateCommand(); var init = GetInit(cmd.GetType()); init?.Invoke(cmd); - if (Transaction != null) + if (Transaction is not null) cmd.Transaction = Transaction; cmd.CommandText = CommandText; if (CommandTimeout.HasValue) @@ -123,32 +134,35 @@ internal IDbCommand SetupCommand(IDbConnection cnn, Action p { cmd.CommandTimeout = SqlMapper.Settings.CommandTimeout.Value; } - if (CommandType.HasValue) - cmd.CommandType = CommandType.Value; + cmd.CommandType = CommandTypeDirect; paramReader?.Invoke(cmd, Parameters); return cmd; } - private static SqlMapper.Link> commandInitCache; + private static SqlMapper.Link>? commandInitCache; - private static Action GetInit(Type commandType) + internal static void ResetCommandInitCache() + => SqlMapper.Link>.Clear(ref commandInitCache); + + private static Action? GetInit(Type commandType) { - if (commandType == null) + if (commandType is null) return null; // GIGO - if (SqlMapper.Link>.TryGet(commandInitCache, commandType, out Action action)) + if (SqlMapper.Link>.TryGet(commandInitCache, commandType, out Action? action)) { return action; } var bindByName = GetBasicPropertySetter(commandType, "BindByName", typeof(bool)); var initialLongFetchSize = GetBasicPropertySetter(commandType, "InitialLONGFetchSize", typeof(int)); + var fetchSize = GetBasicPropertySetter(commandType, "FetchSize", typeof(long)); action = null; - if (bindByName != null || initialLongFetchSize != null) + if (bindByName is not null || initialLongFetchSize is not null || fetchSize is not null) { var method = new DynamicMethod(commandType.Name + "_init", null, new Type[] { typeof(IDbCommand) }); var il = method.GetILGenerator(); - if (bindByName != null) + if (bindByName is not null) { // .BindByName = true il.Emit(OpCodes.Ldarg_0); @@ -156,7 +170,7 @@ private static Action GetInit(Type commandType) il.Emit(OpCodes.Ldc_I4_1); il.EmitCall(OpCodes.Callvirt, bindByName, null); } - if (initialLongFetchSize != null) + if (initialLongFetchSize is not null) { // .InitialLONGFetchSize = -1 il.Emit(OpCodes.Ldarg_0); @@ -164,15 +178,27 @@ private static Action GetInit(Type commandType) il.Emit(OpCodes.Ldc_I4_M1); il.EmitCall(OpCodes.Callvirt, initialLongFetchSize, null); } + if (fetchSize is not null) + { + var snapshot = SqlMapper.Settings.FetchSize; + if (snapshot >= 0) + { + // .FetchSize = {withValue} + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, commandType); + il.Emit(OpCodes.Ldc_I8, snapshot); // bake it as a constant + il.EmitCall(OpCodes.Callvirt, fetchSize, null); + } + } il.Emit(OpCodes.Ret); action = (Action)method.CreateDelegate(typeof(Action)); } // cache it - SqlMapper.Link>.TryAdd(ref commandInitCache, commandType, ref action); + SqlMapper.Link>.TryAdd(ref commandInitCache, commandType, ref action!); return action; } - private static MethodInfo GetBasicPropertySetter(Type declaringType, string name, Type expectedType) + private static MethodInfo? GetBasicPropertySetter(Type declaringType, string name, Type expectedType) { var prop = declaringType.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); if (prop?.CanWrite == true && prop.PropertyType == expectedType && prop.GetIndexParameters().Length == 0) diff --git a/Dapper/CompiledRegex.cs b/Dapper/CompiledRegex.cs new file mode 100644 index 000000000..faffb7c9a --- /dev/null +++ b/Dapper/CompiledRegex.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; + +namespace Dapper; + +internal static partial class CompiledRegex +{ +#if DEBUG && NET7_0_OR_GREATER // enables colorization in IDE + [StringSyntax("Regex")] +#endif + private const string + WhitespaceOrReservedPattern = @"[\s;/\-+*]|^vacuum$|^commit$|^rollback$|^revert$", + LegacyParameterPattern = @"(? LegacyParameterGen(); + internal static Regex LiteralTokens => LiteralTokensGen(); + internal static Regex PseudoPositional => PseudoPositionalGen(); + internal static Regex WhitespaceOrReserved => WhitespaceOrReservedGen(); +#else + internal static Regex LegacyParameter { get; } + = new(LegacyParameterPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled); + internal static Regex LiteralTokens { get; } + = new(LiteralTokensPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant | RegexOptions.Compiled); + internal static Regex PseudoPositional { get; } + = new(PseudoPositionalPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled); + internal static Regex WhitespaceOrReserved { get; } + = new(WhitespaceOrReservedPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); +#endif +} diff --git a/Dapper/CustomPropertyTypeMap.cs b/Dapper/CustomPropertyTypeMap.cs index 9228db2b0..0d36f7470 100644 --- a/Dapper/CustomPropertyTypeMap.cs +++ b/Dapper/CustomPropertyTypeMap.cs @@ -28,14 +28,14 @@ public CustomPropertyTypeMap(Type type, Func propert /// DataReader column names /// DataReader column types /// Default constructor - public ConstructorInfo FindConstructor(string[] names, Type[] types) => - _type.GetConstructor(new Type[0]); + public ConstructorInfo? FindConstructor(string[] names, Type[] types) => + _type.GetConstructor(Array.Empty())!; /// /// Always returns null /// /// - public ConstructorInfo FindExplicitConstructor() => null; + public ConstructorInfo? FindExplicitConstructor() => null; /// /// Not implemented as far as default constructor used for all cases @@ -52,11 +52,11 @@ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, /// Returns property based on selector strategy /// /// DataReader column name - /// Poperty member map - public SqlMapper.IMemberMap GetMember(string columnName) + /// Property member map + public SqlMapper.IMemberMap? GetMember(string columnName) { var prop = _propertySelector(_type, columnName); - return prop != null ? new SimpleMemberMap(columnName, prop) : null; + return prop is not null ? new SimpleMemberMap(columnName, prop) : null; } } } diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index c62cb33ad..d9518dc45 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -1,30 +1,37 @@  - Dapper - orm;sql;micro-orm + Dapper Dapper - A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc.. + orm;sql;micro-orm + A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net451;netstandard1.3;netstandard2.0 + net461;netstandard2.0;net8.0;net10.0 + enable + true - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + - - - - + + + + - - - - - - - + + + + + diff --git a/Dapper/DataTableHandler.cs b/Dapper/DataTableHandler.cs index f9b2b5fe6..df4dc07a5 100644 --- a/Dapper/DataTableHandler.cs +++ b/Dapper/DataTableHandler.cs @@ -1,6 +1,5 @@ using System; using System.Data; -#if !NETSTANDARD1_3 namespace Dapper { internal sealed class DataTableHandler : SqlMapper.ITypeHandler @@ -16,4 +15,3 @@ public void SetValue(IDbDataParameter parameter, object value) } } } -#endif \ No newline at end of file diff --git a/Dapper/DbString.cs b/Dapper/DbString.cs index 58a7b80c9..15b97d6bc 100644 --- a/Dapper/DbString.cs +++ b/Dapper/DbString.cs @@ -28,6 +28,17 @@ public DbString() Length = -1; IsAnsi = IsAnsiDefault; } + + /// + /// Create a new DbString + /// + public DbString(string? value, int length = -1) + { + Value = value; + Length = length; + IsAnsi = IsAnsiDefault; + } + /// /// Ansi vs Unicode /// @@ -43,7 +54,15 @@ public DbString() /// /// The value of the string /// - public string Value { get; set; } + public string? Value { get; set; } + + /// + /// Gets a string representation of this DbString. + /// + public override string ToString() => Value is null + ? $"Dapper.DbString (Value: null, Length: {Length}, IsAnsi: {IsAnsi}, IsFixedLength: {IsFixedLength})" + : $"Dapper.DbString (Value: '{Value}', Length: {Length}, IsAnsi: {IsAnsi}, IsFixedLength: {IsFixedLength})"; + /// /// Add the parameter to the command... internal use only /// @@ -69,7 +88,7 @@ public void AddParameter(IDbCommand command, string name) #pragma warning disable 0618 param.Value = SqlMapper.SanitizeParameterValue(Value); #pragma warning restore 0618 - if (Length == -1 && Value != null && Value.Length <= DefaultLength) + if (Length == -1 && Value is not null && Value.Length <= DefaultLength) { param.Size = DefaultLength; } diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs index 88859a08b..c2f691883 100644 --- a/Dapper/DefaultTypeMap.cs +++ b/Dapper/DefaultTypeMap.cs @@ -10,7 +10,7 @@ namespace Dapper /// public sealed class DefaultTypeMap : SqlMapper.ITypeMap { - private readonly List _fields; + private readonly FieldInfo[] _fields; private readonly Type _type; /// @@ -19,55 +19,44 @@ public sealed class DefaultTypeMap : SqlMapper.ITypeMap /// Entity type public DefaultTypeMap(Type type) { - if (type == null) + if (type is null) throw new ArgumentNullException(nameof(type)); _fields = GetSettableFields(type); Properties = GetSettableProps(type); _type = type; } -#if NETSTANDARD1_3 - private static bool IsParameterMatch(ParameterInfo[] x, ParameterInfo[] y) + + internal static MethodInfo GetPropertySetterOrThrow(PropertyInfo propertyInfo, Type type) { - if (ReferenceEquals(x, y)) return true; - if (x == null || y == null) return false; - if (x.Length != y.Length) return false; - for (int i = 0; i < x.Length; i++) - if (x[i].ParameterType != y[i].ParameterType) return false; - return true; + return GetPropertySetter(propertyInfo, type) ?? Throw(propertyInfo); + + static MethodInfo Throw(PropertyInfo propertyInfo) => throw new InvalidOperationException("Property setting not found for: " + propertyInfo?.Name); } -#endif - internal static MethodInfo GetPropertySetter(PropertyInfo propertyInfo, Type type) + internal static MethodInfo? GetPropertySetter(PropertyInfo propertyInfo, Type type) { if (propertyInfo.DeclaringType == type) return propertyInfo.GetSetMethod(true); -#if NETSTANDARD1_3 - return propertyInfo.DeclaringType.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Single(x => x.Name == propertyInfo.Name - && x.PropertyType == propertyInfo.PropertyType - && IsParameterMatch(x.GetIndexParameters(), propertyInfo.GetIndexParameters()) - ).GetSetMethod(true); -#else - return propertyInfo.DeclaringType.GetProperty( + + return propertyInfo.DeclaringType!.GetProperty( propertyInfo.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, propertyInfo.PropertyType, - propertyInfo.GetIndexParameters().Select(p => p.ParameterType).ToArray(), - null).GetSetMethod(true); -#endif + Array.ConvertAll(propertyInfo.GetIndexParameters(), p => p.ParameterType), + null)!.GetSetMethod(true); } internal static List GetSettableProps(Type t) { return t .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(p => GetPropertySetter(p, t) != null) + .Where(p => GetPropertySetter(p, t) is not null) .ToList(); } - internal static List GetSettableFields(Type t) + private static FieldInfo[] GetSettableFields(Type t) { - return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); + return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); } /// @@ -76,7 +65,7 @@ internal static List GetSettableFields(Type t) /// DataReader column names /// DataReader column types /// Matching constructor or default one - public ConstructorInfo FindConstructor(string[] names, Type[] types) + public ConstructorInfo? FindConstructor(string[] names, Type[] types) { var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (ConstructorInfo ctor in constructors.OrderBy(c => c.IsPublic ? 0 : (c.IsPrivate ? 2 : 1)).ThenBy(c => c.GetParameters().Length)) @@ -91,15 +80,23 @@ public ConstructorInfo FindConstructor(string[] names, Type[] types) int i = 0; for (; i < ctorParameters.Length; i++) { - if (!string.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase)) + if (EqualsCI(ctorParameters[i].Name, names[i])) + { } // exact match + else if (MatchNamesWithUnderscores && EqualsCIU(ctorParameters[i].Name, names[i])) + { } // match after applying underscores + else + { + // not a name match break; + } + if (types[i] == typeof(byte[]) && ctorParameters[i].ParameterType.FullName == SqlMapper.LinqBinary) continue; var unboxedType = Nullable.GetUnderlyingType(ctorParameters[i].ParameterType) ?? ctorParameters[i].ParameterType; if ((unboxedType != types[i] && !SqlMapper.HasTypeHandler(unboxedType)) - && !(unboxedType.IsEnum() && Enum.GetUnderlyingType(unboxedType) == types[i]) + && !(unboxedType.IsEnum && Enum.GetUnderlyingType(unboxedType) == types[i]) && !(unboxedType == typeof(char) && types[i] == typeof(string)) - && !(unboxedType.IsEnum() && types[i] == typeof(string))) + && !(unboxedType.IsEnum && types[i] == typeof(string))) { break; } @@ -115,14 +112,10 @@ public ConstructorInfo FindConstructor(string[] names, Type[] types) /// /// Returns the constructor, if any, that has the ExplicitConstructorAttribute on it. /// - public ConstructorInfo FindExplicitConstructor() + public ConstructorInfo? FindExplicitConstructor() { var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -#if NETSTANDARD1_3 - var withAttr = constructors.Where(c => c.CustomAttributes.Any(x => x.AttributeType == typeof(ExplicitConstructorAttribute))).ToList(); -#else var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).ToList(); -#endif if (withAttr.Count == 1) { @@ -140,9 +133,10 @@ public ConstructorInfo FindExplicitConstructor() /// Mapping implementation public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) { - var parameters = constructor.GetParameters(); + var param = MatchFirstOrDefault(constructor.GetParameters(), columnName, static p => p.Name) ?? Throw(columnName); + return new SimpleMemberMap(columnName, param); - return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase))); + static ParameterInfo Throw(string name) => throw new ArgumentException("Constructor parameter not found for " + name); } /// @@ -150,42 +144,35 @@ public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, /// /// DataReader column name /// Mapping implementation - public SqlMapper.IMemberMap GetMember(string columnName) + public SqlMapper.IMemberMap? GetMember(string columnName) { - var property = Properties.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) - ?? Properties.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); - - if (property == null && MatchNamesWithUnderscores) - { - property = Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.Ordinal)) - ?? Properties.Find(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.OrdinalIgnoreCase)); - } + var property = MatchFirstOrDefault(Properties, columnName, static p => p.Name); - if (property != null) + if (property is not null) return new SimpleMemberMap(columnName, property); // roslyn automatically implemented properties, in particular for get-only properties: <{Name}>k__BackingField; var backingFieldName = "<" + columnName + ">k__BackingField"; // preference order is: - // exact match over underscre match, exact case over wrong case, backing fields over regular fields, match-inc-underscores over match-exc-underscores - var field = _fields.Find(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); + // exact match over underscore match, exact case over wrong case, backing fields over regular fields, match-inc-underscores over match-exc-underscores + var field = Array.Find(_fields, p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); - if (field == null && MatchNamesWithUnderscores) + if (field is null && MatchNamesWithUnderscores) { var effectiveColumnName = columnName.Replace("_", ""); backingFieldName = "<" + effectiveColumnName + ">k__BackingField"; - field = _fields.Find(p => string.Equals(p.Name, effectiveColumnName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) - ?? _fields.Find(p => string.Equals(p.Name, effectiveColumnName, StringComparison.OrdinalIgnoreCase)) - ?? _fields.Find(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); + field = Array.Find(_fields, p => string.Equals(p.Name, effectiveColumnName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) + ?? Array.Find(_fields, p => string.Equals(p.Name, effectiveColumnName, StringComparison.OrdinalIgnoreCase)) + ?? Array.Find(_fields, p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); } - if (field != null) + if (field is not null) return new SimpleMemberMap(columnName, field); return null; @@ -195,6 +182,72 @@ public SqlMapper.IMemberMap GetMember(string columnName) /// public static bool MatchNamesWithUnderscores { get; set; } + static T? MatchFirstOrDefault(IList? members, string? name, Func selector) where T : class + { + if (members is { Count: > 0 }) + { + // try exact first + foreach (var member in members) + { + if (string.Equals(name, selector(member), StringComparison.Ordinal)) + { + return member; + } + } + // then exact ignoring case + foreach (var member in members) + { + if (string.Equals(name, selector(member), StringComparison.OrdinalIgnoreCase)) + { + return member; + } + } + if (MatchNamesWithUnderscores) + { + // same again, minus underscore delta + name = name?.Replace("_", ""); + + // match normalized column name vs actual property name + foreach (var member in members) + { + if (string.Equals(name, selector(member), StringComparison.Ordinal)) + { + return member; + } + } + foreach (var member in members) + { + if (string.Equals(name, selector(member), StringComparison.OrdinalIgnoreCase)) + { + return member; + } + } + + // match normalized column name vs normalized property name + foreach (var member in members) + { + if (string.Equals(name, selector(member)?.Replace("_", ""), StringComparison.Ordinal)) + { + return member; + } + } + foreach (var member in members) + { + if (string.Equals(name, selector(member)?.Replace("_", ""), StringComparison.OrdinalIgnoreCase)) + { + return member; + } + } + } + } + return null; + } + + internal static bool EqualsCI(string? x, string? y) + => string.Equals(x, y, StringComparison.OrdinalIgnoreCase); + internal static bool EqualsCIU(string? x, string? y) + => string.Equals(x?.Replace("_", ""), y?.Replace("_", ""), StringComparison.OrdinalIgnoreCase); + /// /// The settable properties for this typemap /// diff --git a/Dapper/DynamicParameters.ParamInfo.cs b/Dapper/DynamicParameters.ParamInfo.cs index 67c9c117e..51c5282b4 100644 --- a/Dapper/DynamicParameters.ParamInfo.cs +++ b/Dapper/DynamicParameters.ParamInfo.cs @@ -7,14 +7,14 @@ public partial class DynamicParameters { private sealed class ParamInfo { - public string Name { get; set; } - public object Value { get; set; } + public string Name { get; set; } = null!; + public object? Value { get; set; } public ParameterDirection ParameterDirection { get; set; } public DbType? DbType { get; set; } public int? Size { get; set; } - public IDbDataParameter AttachedParam { get; set; } - internal Action OutputCallback { get; set; } - internal object OutputTarget { get; set; } + public IDbDataParameter AttachedParam { get; set; } = null!; + internal Action? OutputCallback { get; set; } + internal object OutputTarget { get; set; } = null!; internal bool CameFromTemplate { get; set; } public byte? Precision { get; set; } diff --git a/Dapper/DynamicParameters.cs b/Dapper/DynamicParameters.cs index 83038656c..1feb4d020 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -6,10 +6,6 @@ using System.Reflection; using System.Reflection.Emit; -#if NETSTANDARD1_3 -using ApplicationException = System.InvalidOperationException; -#endif - namespace Dapper { /// @@ -20,10 +16,10 @@ public partial class DynamicParameters : SqlMapper.IDynamicParameters, SqlMapper internal const DbType EnumerableMultiParameter = (DbType)(-1); private static readonly Dictionary> paramReaderCache = new Dictionary>(); private readonly Dictionary parameters = new Dictionary(); - private List templates; + private List? templates; - object SqlMapper.IParameterLookup.this[string name] => - parameters.TryGetValue(name, out ParamInfo param) ? param.Value : null; + object? SqlMapper.IParameterLookup.this[string name] => + parameters.TryGetValue(name, out ParamInfo? param) ? param.Value : null; /// /// construct a dynamic parameter bag @@ -37,7 +33,7 @@ public DynamicParameters() /// construct a dynamic parameter bag /// /// can be an anonymous type or a DynamicParameters bag - public DynamicParameters(object template) + public DynamicParameters(object? template) { RemoveUnused = true; AddDynamicParams(template); @@ -48,45 +44,40 @@ public DynamicParameters(object template) /// EG: AddDynamicParams(new {A = 1, B = 2}) // will add property A and B to the dynamic /// /// - public void AddDynamicParams(object param) + public void AddDynamicParams(object? param) { var obj = param; - if (obj != null) + if (obj is not null) { - var subDynamic = obj as DynamicParameters; - if (subDynamic == null) + if (obj is DynamicParameters subDynamic) { - var dictionary = obj as IEnumerable>; - if (dictionary == null) - { - templates = templates ?? new List(); - templates.Add(obj); - } - else + if (subDynamic.parameters is not null) { - foreach (var kvp in dictionary) + foreach (var kvp in subDynamic.parameters) { - Add(kvp.Key, kvp.Value, null, null, null); + parameters.Add(kvp.Key, kvp.Value); } } + + if (subDynamic.templates is not null) + { + templates ??= new List(); + templates.AddRange(subDynamic.templates); + } } else { - if (subDynamic.parameters != null) + if (obj is IEnumerable> dictionary) { - foreach (var kvp in subDynamic.parameters) + foreach (var kvp in dictionary) { - parameters.Add(kvp.Key, kvp.Value); + Add(kvp.Key, kvp.Value, null, null, null); } } - - if (subDynamic.templates != null) + else { - templates = templates ?? new List(); - foreach (var t in subDynamic.templates) - { - templates.Add(t); - } + templates ??= new List(); + templates.Add(obj); } } } @@ -100,7 +91,7 @@ public void AddDynamicParams(object param) /// The type of the parameter. /// The in or out direction of the parameter. /// The size of the parameter. - public void Add(string name, object value, DbType? dbType, ParameterDirection? direction, int? size) + public void Add(string name, object? value, DbType? dbType, ParameterDirection? direction, int? size) { parameters[Clean(name)] = new ParamInfo { @@ -122,7 +113,7 @@ public void Add(string name, object value, DbType? dbType, ParameterDirection? d /// The size of the parameter. /// The precision of the parameter. /// The scale of the parameter. - public void Add(string name, object value = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null, byte? precision = null, byte? scale = null) + public void Add(string name, object? value = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null, byte? precision = null, byte? scale = null) { parameters[Clean(name)] = new ParamInfo { @@ -161,6 +152,12 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id /// public bool RemoveUnused { get; set; } + internal static bool ShouldSetDbType(DbType? dbType) + => dbType.HasValue && dbType.GetValueOrDefault() != EnumerableMultiParameter; + + internal static bool ShouldSetDbType(DbType dbType) + => dbType != EnumerableMultiParameter; // just in case called with non-nullable + /// /// Add all the parameters needed to the command just before it executes /// @@ -168,9 +165,9 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id /// Information about the query protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { - var literals = SqlMapper.GetLiteralTokens(identity.sql); + var literals = SqlMapper.GetLiteralTokens(identity.Sql); - if (templates != null) + if (templates is not null) { foreach (var template in templates) { @@ -179,7 +176,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) lock (paramReaderCache) { - if (!paramReaderCache.TryGetValue(newIdent, out appender)) + if (!paramReaderCache.TryGetValue(newIdent, out appender!)) { appender = SqlMapper.CreateParamInfoGenerator(newIdent, true, RemoveUnused, literals); paramReaderCache[newIdent] = appender; @@ -212,14 +209,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) } // Now that the parameters are added to the command, let's place our output callbacks - var tmp = outputCallbacks; - if (tmp != null) - { - foreach (var generator in tmp) - { - generator(); - } - } + outputCallbacks?.ForEach(generator => generator()); } foreach (var param in parameters.Values) @@ -231,8 +221,8 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) string name = Clean(param.Name); var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter; - SqlMapper.ITypeHandler handler = null; - if (dbType == null && val != null && !isCustomQueryParameter) + SqlMapper.ITypeHandler? handler = null; + if (dbType is null && val is not null && !isCustomQueryParameter) { #pragma warning disable 618 dbType = SqlMapper.LookupDbType(val.GetType(), name, true, out handler); @@ -240,7 +230,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) } if (isCustomQueryParameter) { - ((SqlMapper.ICustomQueryParameter)val).AddParameter(command, name); + ((SqlMapper.ICustomQueryParameter)val!).AddParameter(command, name); } else if (dbType == EnumerableMultiParameter) { @@ -263,30 +253,30 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) } p.Direction = param.ParameterDirection; - if (handler == null) + if (handler is null) { #pragma warning disable 0618 p.Value = SqlMapper.SanitizeParameterValue(val); #pragma warning restore 0618 - if (dbType != null && p.DbType != dbType) + if (ShouldSetDbType(dbType) && p.DbType != dbType.GetValueOrDefault()) { - p.DbType = dbType.Value; + p.DbType = dbType.GetValueOrDefault(); } var s = val as string; if (s?.Length <= DbString.DefaultLength) { p.Size = DbString.DefaultLength; } - if (param.Size != null) p.Size = param.Size.Value; - if (param.Precision != null) p.Precision = param.Precision.Value; - if (param.Scale != null) p.Scale = param.Scale.Value; + if (param.Size is not null) p.Size = param.Size.Value; + if (param.Precision is not null) p.Precision = param.Precision.Value; + if (param.Scale is not null) p.Scale = param.Scale.Value; } else { - if (dbType != null) p.DbType = dbType.Value; - if (param.Size != null) p.Size = param.Size.Value; - if (param.Precision != null) p.Precision = param.Precision.Value; - if (param.Scale != null) p.Scale = param.Scale.Value; + if (ShouldSetDbType(dbType)) p.DbType = dbType.GetValueOrDefault(); + if (param.Size is not null) p.Size = param.Size.Value; + if (param.Precision is not null) p.Precision = param.Precision.Value; + if (param.Scale is not null) p.Scale = param.Scale.Value; handler.SetValue(p, val ?? DBNull.Value); } @@ -298,7 +288,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) } } - // note: most non-priveleged implementations would use: this.ReplaceLiterals(command); + // note: most non-privileged implementations would use: this.ReplaceLiterals(command); if (literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals); } @@ -317,16 +307,16 @@ public T Get(string name) { var paramInfo = parameters[Clean(name)]; var attachedParam = paramInfo.AttachedParam; - object val = attachedParam == null ? paramInfo.Value : attachedParam.Value; + object? val = attachedParam is null ? paramInfo.Value : attachedParam.Value; if (val == DBNull.Value) { - if (default(T) != null) + if (default(T) is not null) { throw new ApplicationException("Attempting to cast a DBNull to a non nullable type! Note that out/return parameters will not have updated values until the data stream completes (after the 'foreach' for Query(..., buffered: false), or after the GridReader has been disposed for QueryMultiple)"); } - return default(T); + return default!; } - return (T)val; + return (T)val!; } /// @@ -339,37 +329,38 @@ public T Get(string name) /// /// The size to set on the parameter. Defaults to 0, or DbString.DefaultLength in case of strings. /// The DynamicParameters instance - public DynamicParameters Output(T target, Expression> expression, DbType? dbType = null, int? size = null) + public DynamicParameters Output(T target, Expression> expression, DbType? dbType = null, int? size = null) { - var failMessage = "Expression must be a property/field chain off of a(n) {0} instance"; - failMessage = string.Format(failMessage, typeof(T).Name); - Action @throw = () => throw new InvalidOperationException(failMessage); + static void ThrowInvalidChain() + => throw new InvalidOperationException($"Expression must be a property/field chain off of a(n) {typeof(T).Name} instance"); // Is it even a MemberExpression? +#pragma warning disable IDE0019 // Use pattern matching - already complex enough var lastMemberAccess = expression.Body as MemberExpression; +#pragma warning restore IDE0019 // Use pattern matching - if (lastMemberAccess == null + if (lastMemberAccess is null || (!(lastMemberAccess.Member is PropertyInfo) && !(lastMemberAccess.Member is FieldInfo))) { if (expression.Body.NodeType == ExpressionType.Convert && expression.Body.Type == typeof(object) - && ((UnaryExpression)expression.Body).Operand is MemberExpression) + && ((UnaryExpression)expression.Body).Operand is MemberExpression member) { // It's got to be unboxed - lastMemberAccess = (MemberExpression)((UnaryExpression)expression.Body).Operand; + lastMemberAccess = member; } else { - @throw(); + ThrowInvalidChain(); } } // Does the chain consist of MemberExpressions leading to a ParameterExpression of type T? - MemberExpression diving = lastMemberAccess; + MemberExpression? diving = lastMemberAccess; // Retain a list of member names and the member expressions so we can rebuild the chain. - List names = new List(); - List chain = new List(); + List names = new List(); + List chain = new List(); do { @@ -378,21 +369,23 @@ public DynamicParameters Output(T target, Expression> express names.Insert(0, diving?.Member.Name); chain.Insert(0, diving); +#pragma warning disable IDE0019 // use pattern matching; this is fine! var constant = diving?.Expression as ParameterExpression; diving = diving?.Expression as MemberExpression; +#pragma warning restore IDE0019 // use pattern matching - if (constant != null && constant.Type == typeof(T)) + if (constant is not null && constant.Type == typeof(T)) { break; } - else if (diving == null + else if (diving is null || (!(diving.Member is PropertyInfo) && !(diving.Member is FieldInfo))) { - @throw(); + ThrowInvalidChain(); } } - while (diving != null); + while (diving is not null); var dynamicParamName = string.Concat(names.ToArray()); @@ -400,8 +393,8 @@ public DynamicParameters Output(T target, Expression> express var lookup = string.Join("|", names.ToArray()); var cache = CachedOutputSetters.Cache; - var setter = (Action)cache[lookup]; - if (setter != null) goto MAKECALLBACK; + var setter = (Action?)cache[lookup]; + if (setter is not null) goto MAKECALLBACK; // Come on let's build a method, let's build it, let's build it now! var dm = new DynamicMethod("ExpressionParam" + Guid.NewGuid().ToString(), null, new[] { typeof(object), GetType() }, true); @@ -411,23 +404,22 @@ public DynamicParameters Output(T target, Expression> express il.Emit(OpCodes.Castclass, typeof(T)); // [T] // Count - 1 to skip the last member access - var i = 0; - for (; i < (chain.Count - 1); i++) + for (var i = 0; i < chain.Count - 1; i++) { - var member = chain[0].Member; + var member = chain[i]?.Member; - if (member is PropertyInfo) + if (member is PropertyInfo info) { - var get = ((PropertyInfo)member).GetGetMethod(true); - il.Emit(OpCodes.Callvirt, get); // [Member{i}] + var get = info.GetGetMethod(true); + il.Emit(OpCodes.Callvirt, get!); // [Member{i}] } else // Else it must be a field! { - il.Emit(OpCodes.Ldfld, (FieldInfo)member); // [Member{i}] + il.Emit(OpCodes.Ldfld, (FieldInfo)member!); // [Member{i}] } } - var paramGetter = GetType().GetMethod("Get", new Type[] { typeof(string) }).MakeGenericMethod(lastMemberAccess.Type); + var paramGetter = GetType().GetMethod("Get", new Type[] { typeof(string) })!.MakeGenericMethod(lastMemberAccess!.Type); il.Emit(OpCodes.Ldarg_1); // [target] [DynamicParameters] il.Emit(OpCodes.Ldstr, dynamicParamName); // [target] [DynamicParameters] [ParamName] @@ -435,10 +427,10 @@ public DynamicParameters Output(T target, Expression> express // GET READY var lastMember = lastMemberAccess.Member; - if (lastMember is PropertyInfo) + if (lastMember is PropertyInfo property) { - var set = ((PropertyInfo)lastMember).GetSetMethod(true); - il.Emit(OpCodes.Callvirt, set); // SET + var set = property.GetSetMethod(true); + il.Emit(OpCodes.Callvirt, set!); // SET } else { @@ -453,15 +445,15 @@ public DynamicParameters Output(T target, Expression> express cache[lookup] = setter; } - // Queue the preparation to be fired off when adding parameters to the DbCommand - MAKECALLBACK: - (outputCallbacks ?? (outputCallbacks = new List())).Add(() => + // Queue the preparation to be fired off when adding parameters to the DbCommand + MAKECALLBACK: + (outputCallbacks ??= new List()).Add(() => { // Finally, prep the parameter and attach the callback to it var targetMemberType = lastMemberAccess?.Type; int sizeToSet = (!size.HasValue && targetMemberType == typeof(string)) ? DbString.DefaultLength : size ?? 0; - if (parameters.TryGetValue(dynamicParamName, out ParamInfo parameter)) + if (parameters.TryGetValue(dynamicParamName, out ParamInfo? parameter)) { parameter.ParameterDirection = parameter.AttachedParam.Direction = ParameterDirection.InputOutput; @@ -472,26 +464,20 @@ public DynamicParameters Output(T target, Expression> express } else { - dbType = (!dbType.HasValue) -#pragma warning disable 618 - ? SqlMapper.LookupDbType(targetMemberType, targetMemberType?.Name, true, out SqlMapper.ITypeHandler handler) -#pragma warning restore 618 - : dbType; - // CameFromTemplate property would not apply here because this new param // Still needs to be added to the command - Add(dynamicParamName, expression.Compile().Invoke(target), null, ParameterDirection.InputOutput, sizeToSet); + Add(dynamicParamName, expression.Compile().Invoke(target), dbType, ParameterDirection.InputOutput, sizeToSet); } parameter = parameters[dynamicParamName]; parameter.OutputCallback = setter; - parameter.OutputTarget = target; + parameter.OutputTarget = target!; }); return this; } - private List outputCallbacks; + private List? outputCallbacks; void SqlMapper.IParameterCallbacks.OnCompleted() { diff --git a/Dapper/ExplicitConstructorAttribute.cs b/Dapper/ExplicitConstructorAttribute.cs index d801af345..4d5580127 100644 --- a/Dapper/ExplicitConstructorAttribute.cs +++ b/Dapper/ExplicitConstructorAttribute.cs @@ -5,7 +5,10 @@ namespace Dapper /// /// Tell Dapper to use an explicit constructor, passing nulls or 0s for all parameters /// - [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false)] + /// + /// Usage on methods is limited to the usage with Dapper.AOT (https://github.com/DapperLib/DapperAOT) + /// + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method, AllowMultiple = false)] public sealed class ExplicitConstructorAttribute : Attribute { } diff --git a/Dapper/Extensions.cs b/Dapper/Extensions.cs new file mode 100644 index 000000000..778011ae8 --- /dev/null +++ b/Dapper/Extensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; + +namespace Dapper +{ + internal static class Extensions + { + /// + /// Creates a with a less specific generic parameter that perfectly mirrors the + /// state of the specified . + /// + internal static Task CastResult(this Task task) + where TFrom : TTo + { + if (task is null) throw new ArgumentNullException(nameof(task)); + + if (task.Status == TaskStatus.RanToCompletion) + return Task.FromResult((TTo)task.Result); + + var source = new TaskCompletionSource(); + task.ContinueWith(OnTaskCompleted, state: source, TaskContinuationOptions.ExecuteSynchronously); + return source.Task; + } + + private static void OnTaskCompleted(Task completedTask, object? state) + where TFrom : TTo + { + var source = (TaskCompletionSource)state!; + + switch (completedTask.Status) + { + case TaskStatus.RanToCompletion: + source.SetResult(completedTask.Result); + break; + case TaskStatus.Canceled: + source.SetCanceled(); + break; + case TaskStatus.Faulted: + source.SetException(completedTask.Exception!.InnerExceptions); + break; + } + } + } +} diff --git a/Dapper/FeatureSupport.cs b/Dapper/FeatureSupport.cs index 402d86e48..f00c38ed3 100644 --- a/Dapper/FeatureSupport.cs +++ b/Dapper/FeatureSupport.cs @@ -6,20 +6,22 @@ namespace Dapper /// /// Handles variances in features per DBMS /// - internal class FeatureSupport + internal sealed class FeatureSupport { private static readonly FeatureSupport Default = new FeatureSupport(false), - Postgres = new FeatureSupport(true); + Postgres = new FeatureSupport(true), + ClickHouse = new FeatureSupport(true); /// /// Gets the feature set based on the passed connection /// /// The connection to get supported features for. - public static FeatureSupport Get(IDbConnection connection) + public static FeatureSupport Get(IDbConnection? connection) { - string name = connection?.GetType().Name; + string? name = connection?.GetType().Name; if (string.Equals(name, "npgsqlconnection", StringComparison.OrdinalIgnoreCase)) return Postgres; + if (string.Equals(name, "clickhouseconnection", StringComparison.OrdinalIgnoreCase)) return ClickHouse; return Default; } diff --git a/Dapper/Global.cs b/Dapper/Global.cs new file mode 100644 index 000000000..f6b5f4ef4 --- /dev/null +++ b/Dapper/Global.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; +#if !STRONG_NAME +[assembly: InternalsVisibleTo("Dapper.Tests")] +#endif diff --git a/Dapper/NRT.cs b/Dapper/NRT.cs new file mode 100644 index 000000000..47c4366e6 --- /dev/null +++ b/Dapper/NRT.cs @@ -0,0 +1,12 @@ +#if !NET5_0_OR_GREATER +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + public bool ReturnValue { get; } + } +} +#endif diff --git a/Dapper/Properties/AssemblyInfo.cs b/Dapper/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..06b4b135e --- /dev/null +++ b/Dapper/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +[module: System.Runtime.CompilerServices.SkipLocalsInit] + +#if !NET5_0_OR_GREATER +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] + internal sealed class SkipLocalsInitAttribute : Attribute {} +} +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + public MemberNotNullAttribute(string member) {} + public MemberNotNullAttribute(params string[] members) {} + } +} + +#endif diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt new file mode 100644 index 000000000..456aa8bb1 --- /dev/null +++ b/Dapper/PublicAPI.Shipped.txt @@ -0,0 +1,335 @@ +#nullable enable +abstract Dapper.SqlMapper.StringTypeHandler.Format(T xml) -> string! +abstract Dapper.SqlMapper.StringTypeHandler.Parse(string! xml) -> T +abstract Dapper.SqlMapper.TypeHandler.Parse(object! value) -> T? +abstract Dapper.SqlMapper.TypeHandler.SetValue(System.Data.IDbDataParameter! parameter, T? value) -> void +const Dapper.DbString.DefaultLength = 4000 -> int +Dapper.CommandDefinition +Dapper.CommandDefinition.Buffered.get -> bool +Dapper.CommandDefinition.CancellationToken.get -> System.Threading.CancellationToken +Dapper.CommandDefinition.CommandDefinition() -> void +Dapper.CommandDefinition.CommandDefinition(string! commandText, object? parameters = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null, Dapper.CommandFlags flags = Dapper.CommandFlags.Buffered, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void +Dapper.CommandDefinition.CommandText.get -> string! +Dapper.CommandDefinition.CommandTimeout.get -> int? +Dapper.CommandDefinition.CommandType.get -> System.Data.CommandType? +Dapper.CommandDefinition.Flags.get -> Dapper.CommandFlags +Dapper.CommandDefinition.Parameters.get -> object? +Dapper.CommandDefinition.Pipelined.get -> bool +Dapper.CommandDefinition.Transaction.get -> System.Data.IDbTransaction? +Dapper.CommandFlags +Dapper.CommandFlags.Buffered = 1 -> Dapper.CommandFlags +Dapper.CommandFlags.NoCache = 4 -> Dapper.CommandFlags +Dapper.CommandFlags.None = 0 -> Dapper.CommandFlags +Dapper.CommandFlags.Pipelined = 2 -> Dapper.CommandFlags +Dapper.CustomPropertyTypeMap +Dapper.CustomPropertyTypeMap.CustomPropertyTypeMap(System.Type! type, System.Func! propertySelector) -> void +Dapper.CustomPropertyTypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo? +Dapper.CustomPropertyTypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo? +Dapper.CustomPropertyTypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.CustomPropertyTypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap? +Dapper.DbString +Dapper.DbString.AddParameter(System.Data.IDbCommand! command, string! name) -> void +Dapper.DbString.DbString() -> void +Dapper.DbString.DbString(string? value, int length = -1) -> void +Dapper.DbString.IsAnsi.get -> bool +Dapper.DbString.IsAnsi.set -> void +Dapper.DbString.IsFixedLength.get -> bool +Dapper.DbString.IsFixedLength.set -> void +Dapper.DbString.Length.get -> int +Dapper.DbString.Length.set -> void +Dapper.DbString.Value.get -> string? +Dapper.DbString.Value.set -> void +Dapper.DefaultTypeMap +Dapper.DefaultTypeMap.DefaultTypeMap(System.Type! type) -> void +Dapper.DefaultTypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo? +Dapper.DefaultTypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo? +Dapper.DefaultTypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap! +Dapper.DefaultTypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap? +Dapper.DefaultTypeMap.Properties.get -> System.Collections.Generic.List! +Dapper.DynamicParameters +Dapper.DynamicParameters.Add(string! name, object? value = null, System.Data.DbType? dbType = null, System.Data.ParameterDirection? direction = null, int? size = null, byte? precision = null, byte? scale = null) -> void +Dapper.DynamicParameters.Add(string! name, object? value, System.Data.DbType? dbType, System.Data.ParameterDirection? direction, int? size) -> void +Dapper.DynamicParameters.AddDynamicParams(object? param) -> void +Dapper.DynamicParameters.AddParameters(System.Data.IDbCommand! command, Dapper.SqlMapper.Identity! identity) -> void +Dapper.DynamicParameters.DynamicParameters() -> void +Dapper.DynamicParameters.DynamicParameters(object? template) -> void +Dapper.DynamicParameters.Get(string! name) -> T +Dapper.DynamicParameters.Output(T target, System.Linq.Expressions.Expression!>! expression, System.Data.DbType? dbType = null, int? size = null) -> Dapper.DynamicParameters! +Dapper.DynamicParameters.ParameterNames.get -> System.Collections.Generic.IEnumerable! +Dapper.DynamicParameters.RemoveUnused.get -> bool +Dapper.DynamicParameters.RemoveUnused.set -> void +Dapper.ExplicitConstructorAttribute +Dapper.ExplicitConstructorAttribute.ExplicitConstructorAttribute() -> void +Dapper.IWrappedDataReader +Dapper.IWrappedDataReader.Command.get -> System.Data.IDbCommand! +Dapper.IWrappedDataReader.Reader.get -> System.Data.IDataReader! +Dapper.SqlMapper +Dapper.SqlMapper.GridReader +Dapper.SqlMapper.GridReader.CancellationToken.get -> System.Threading.CancellationToken +Dapper.SqlMapper.GridReader.Command.get -> System.Data.IDbCommand! +Dapper.SqlMapper.GridReader.Command.set -> void +Dapper.SqlMapper.GridReader.Dispose() -> void +Dapper.SqlMapper.GridReader.DisposeAsync() -> System.Threading.Tasks.ValueTask +Dapper.SqlMapper.GridReader.GridReader(System.Data.IDbCommand! command, System.Data.Common.DbDataReader! reader, Dapper.SqlMapper.Identity? identity, System.Action? onCompleted = null, object? state = null, bool addToCache = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void +Dapper.SqlMapper.GridReader.IsConsumed.get -> bool +Dapper.SqlMapper.GridReader.OnAfterGrid(int index) -> void +Dapper.SqlMapper.GridReader.OnAfterGridAsync(int index) -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.OnBeforeGrid() -> int +Dapper.SqlMapper.GridReader.Read(bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Type! type, bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Func! func, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Func! func, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Func! func, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Func! func, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Func! func, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Func! func, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.Read(System.Type![]! types, System.Func! map, string! splitOn = "id", bool buffered = true) -> System.Collections.Generic.IEnumerable! +Dapper.SqlMapper.GridReader.ReadAsync(bool buffered = true) -> System.Threading.Tasks.Task!>! +Dapper.SqlMapper.GridReader.ReadAsync(System.Type! type, bool buffered = true) -> System.Threading.Tasks.Task!>! +Dapper.SqlMapper.GridReader.ReadAsync(bool buffered = true) -> System.Threading.Tasks.Task!>! +Dapper.SqlMapper.GridReader.Reader.get -> System.Data.Common.DbDataReader! +Dapper.SqlMapper.GridReader.ReadFirst() -> dynamic! +Dapper.SqlMapper.GridReader.ReadFirst(System.Type! type) -> object! +Dapper.SqlMapper.GridReader.ReadFirst() -> T +Dapper.SqlMapper.GridReader.ReadFirstAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadFirstAsync(System.Type! type) -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadFirstAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadFirstOrDefault() -> dynamic? +Dapper.SqlMapper.GridReader.ReadFirstOrDefault(System.Type! type) -> object? +Dapper.SqlMapper.GridReader.ReadFirstOrDefault() -> T? +Dapper.SqlMapper.GridReader.ReadFirstOrDefaultAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadFirstOrDefaultAsync(System.Type! type) -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadFirstOrDefaultAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadSingle() -> dynamic! +Dapper.SqlMapper.GridReader.ReadSingle(System.Type! type) -> object! +Dapper.SqlMapper.GridReader.ReadSingle() -> T +Dapper.SqlMapper.GridReader.ReadSingleAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadSingleAsync(System.Type! type) -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadSingleAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadSingleOrDefault() -> dynamic? +Dapper.SqlMapper.GridReader.ReadSingleOrDefault(System.Type! type) -> object? +Dapper.SqlMapper.GridReader.ReadSingleOrDefault() -> T? +Dapper.SqlMapper.GridReader.ReadSingleOrDefaultAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadSingleOrDefaultAsync(System.Type! type) -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadSingleOrDefaultAsync() -> System.Threading.Tasks.Task! +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +Dapper.SqlMapper.GridReader.ReadUnbufferedAsync() -> System.Collections.Generic.IAsyncEnumerable! +Dapper.SqlMapper.GridReader.ResultIndex.get -> int +Dapper.SqlMapper.ICustomQueryParameter +Dapper.SqlMapper.ICustomQueryParameter.AddParameter(System.Data.IDbCommand! command, string! name) -> void +Dapper.SqlMapper.Identity +Dapper.SqlMapper.Identity.Equals(Dapper.SqlMapper.Identity? other) -> bool +Dapper.SqlMapper.Identity.ForDynamicParameters(System.Type! type) -> Dapper.SqlMapper.Identity! +Dapper.SqlMapper.IDynamicParameters +Dapper.SqlMapper.IDynamicParameters.AddParameters(System.Data.IDbCommand! command, Dapper.SqlMapper.Identity! identity) -> void +Dapper.SqlMapper.IMemberMap +Dapper.SqlMapper.IMemberMap.ColumnName.get -> string! +Dapper.SqlMapper.IMemberMap.Field.get -> System.Reflection.FieldInfo? +Dapper.SqlMapper.IMemberMap.MemberType.get -> System.Type! +Dapper.SqlMapper.IMemberMap.Parameter.get -> System.Reflection.ParameterInfo? +Dapper.SqlMapper.IMemberMap.Property.get -> System.Reflection.PropertyInfo? +Dapper.SqlMapper.IParameterCallbacks +Dapper.SqlMapper.IParameterCallbacks.OnCompleted() -> void +Dapper.SqlMapper.IParameterLookup +Dapper.SqlMapper.IParameterLookup.this[string! name].get -> object? +Dapper.SqlMapper.ITypeHandler +Dapper.SqlMapper.ITypeHandler.Parse(System.Type! destinationType, object! value) -> object? +Dapper.SqlMapper.ITypeHandler.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void +Dapper.SqlMapper.ITypeMap +Dapper.SqlMapper.ITypeMap.FindConstructor(string![]! names, System.Type![]! types) -> System.Reflection.ConstructorInfo? +Dapper.SqlMapper.ITypeMap.FindExplicitConstructor() -> System.Reflection.ConstructorInfo? +Dapper.SqlMapper.ITypeMap.GetConstructorParameter(System.Reflection.ConstructorInfo! constructor, string! columnName) -> Dapper.SqlMapper.IMemberMap? +Dapper.SqlMapper.ITypeMap.GetMember(string! columnName) -> Dapper.SqlMapper.IMemberMap? +Dapper.SqlMapper.Settings +Dapper.SqlMapper.StringTypeHandler +Dapper.SqlMapper.StringTypeHandler.StringTypeHandler() -> void +Dapper.SqlMapper.TypeHandler +Dapper.SqlMapper.TypeHandler.TypeHandler() -> void +Dapper.SqlMapper.TypeHandlerCache +Dapper.SqlMapper.UdtTypeHandler +Dapper.SqlMapper.UdtTypeHandler.UdtTypeHandler(string! udtTypeName) -> void +override Dapper.DbString.ToString() -> string! +override Dapper.SqlMapper.Identity.Equals(object? obj) -> bool +override Dapper.SqlMapper.Identity.GetHashCode() -> int +override Dapper.SqlMapper.Identity.ToString() -> string! +override Dapper.SqlMapper.StringTypeHandler.Parse(object! value) -> T +override Dapper.SqlMapper.StringTypeHandler.SetValue(System.Data.IDbDataParameter! parameter, T? value) -> void +readonly Dapper.SqlMapper.Identity.commandType -> System.Data.CommandType? +Dapper.SqlMapper.Identity.CommandType.get -> System.Data.CommandType? +readonly Dapper.SqlMapper.Identity.connectionString -> string! +readonly Dapper.SqlMapper.Identity.gridIndex -> int +Dapper.SqlMapper.Identity.GridIndex.get -> int +readonly Dapper.SqlMapper.Identity.hashCode -> int +readonly Dapper.SqlMapper.Identity.parametersType -> System.Type? +Dapper.SqlMapper.Identity.ParametersType.get -> System.Type? +readonly Dapper.SqlMapper.Identity.sql -> string! +Dapper.SqlMapper.Identity.Sql.get -> string! +readonly Dapper.SqlMapper.Identity.type -> System.Type? +Dapper.SqlMapper.Identity.Type.get -> System.Type? +static Dapper.DbString.IsAnsiDefault.get -> bool +static Dapper.DbString.IsAnsiDefault.set -> void +static Dapper.DefaultTypeMap.MatchNamesWithUnderscores.get -> bool +static Dapper.DefaultTypeMap.MatchNamesWithUnderscores.set -> void +static Dapper.SqlMapper.AddTypeHandler(System.Type! type, Dapper.SqlMapper.ITypeHandler! handler) -> void +static Dapper.SqlMapper.AddTypeHandler(Dapper.SqlMapper.TypeHandler! handler) -> void +static Dapper.SqlMapper.AddTypeHandlerImpl(System.Type! type, Dapper.SqlMapper.ITypeHandler? handler, bool clone) -> void +static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType) -> void +static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType, bool useGetFieldValue) -> void +static Dapper.SqlMapper.AsList(this System.Collections.Generic.IEnumerable? source) -> System.Collections.Generic.List! +static Dapper.SqlMapper.AsTableValuedParameter(this System.Data.DataTable! table, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter! +static Dapper.SqlMapper.AsTableValuedParameter(this System.Collections.Generic.IEnumerable! list, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter! +static Dapper.SqlMapper.ConnectionStringComparer.get -> System.Collections.Generic.IEqualityComparer! +static Dapper.SqlMapper.ConnectionStringComparer.set -> void +static Dapper.SqlMapper.CreateParamInfoGenerator(Dapper.SqlMapper.Identity! identity, bool checkForDuplicates, bool removeUnused) -> System.Action! +static Dapper.SqlMapper.Execute(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> int +static Dapper.SqlMapper.Execute(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> int +static Dapper.SqlMapper.ExecuteAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteReader(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Data.IDataReader! +static Dapper.SqlMapper.ExecuteReader(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Data.CommandBehavior commandBehavior) -> System.Data.IDataReader! +static Dapper.SqlMapper.ExecuteReader(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Data.IDataReader! +static Dapper.SqlMapper.ExecuteReaderAsync(this System.Data.Common.DbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteReaderAsync(this System.Data.Common.DbConnection! cnn, Dapper.CommandDefinition command, System.Data.CommandBehavior commandBehavior) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteReaderAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteReaderAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteReaderAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Data.CommandBehavior commandBehavior) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteReaderAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteScalar(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> object? +static Dapper.SqlMapper.ExecuteScalar(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> object? +static Dapper.SqlMapper.ExecuteScalar(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> T? +static Dapper.SqlMapper.ExecuteScalar(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> T? +static Dapper.SqlMapper.ExecuteScalarAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteScalarAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteScalarAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.ExecuteScalarAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.FindOrAddParameter(System.Data.IDataParameterCollection! parameters, System.Data.IDbCommand! command, string! name) -> System.Data.IDbDataParameter! +static Dapper.SqlMapper.Format(object? value) -> string! +static Dapper.SqlMapper.GetCachedSQL(int ignoreHitCountAbove = 2147483647) -> System.Collections.Generic.IEnumerable!>! +static Dapper.SqlMapper.GetCachedSQLCount() -> int +static Dapper.SqlMapper.GetHashCollissions() -> System.Collections.Generic.IEnumerable!>! +static Dapper.SqlMapper.GetRowParser(this System.Data.Common.DbDataReader! reader, System.Type! type, int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) -> System.Func! +static Dapper.SqlMapper.GetRowParser(this System.Data.IDataReader! reader, System.Type! type, int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) -> System.Func! +static Dapper.SqlMapper.GetRowParser(this System.Data.Common.DbDataReader! reader, System.Type? concreteType = null, int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) -> System.Func! +static Dapper.SqlMapper.GetRowParser(this System.Data.IDataReader! reader, System.Type? concreteType = null, int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) -> System.Func! +static Dapper.SqlMapper.GetTypeDeserializer(System.Type! type, System.Data.Common.DbDataReader! reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false) -> System.Func! +static Dapper.SqlMapper.GetTypeDeserializer(System.Type! type, System.Data.IDataReader! reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false) -> System.Func! +static Dapper.SqlMapper.GetTypeMap(System.Type! type) -> Dapper.SqlMapper.ITypeMap! +static Dapper.SqlMapper.GetTypeName(this System.Data.DataTable! table) -> string? +static Dapper.SqlMapper.HasTypeHandler(System.Type! type) -> bool +static Dapper.SqlMapper.LookupDbType(System.Type! type, string! name, bool demand, out Dapper.SqlMapper.ITypeHandler? handler) -> System.Data.DbType? +static Dapper.SqlMapper.PackListParameters(System.Data.IDbCommand! command, string! namePrefix, object? value) -> void +static Dapper.SqlMapper.Parse(this System.Data.IDataReader! reader) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Parse(this System.Data.IDataReader! reader, System.Type! type) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Parse(this System.Data.IDataReader! reader) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.PurgeQueryCache() -> void +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.Query(this System.Data.IDbConnection! cnn, string! sql, System.Type![]! types, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IEnumerable! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, System.Type! type, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command, System.Func! map, string! splitOn = "Id") -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryAsync(this System.Data.IDbConnection! cnn, string! sql, System.Type![]! types, System.Func! map, object? param = null, System.Data.IDbTransaction? transaction = null, bool buffered = true, string! splitOn = "Id", int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task!>! +static Dapper.SqlMapper.QueryCachePurged -> System.EventHandler? +static Dapper.SqlMapper.QueryFirst(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> dynamic! +static Dapper.SqlMapper.QueryFirst(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> object! +static Dapper.SqlMapper.QueryFirst(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> T +static Dapper.SqlMapper.QueryFirst(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> T +static Dapper.SqlMapper.QueryFirstAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstAsync(this System.Data.IDbConnection! cnn, System.Type! type, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstAsync(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstOrDefault(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> dynamic? +static Dapper.SqlMapper.QueryFirstOrDefault(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> object? +static Dapper.SqlMapper.QueryFirstOrDefault(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> T? +static Dapper.SqlMapper.QueryFirstOrDefault(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> T? +static Dapper.SqlMapper.QueryFirstOrDefaultAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstOrDefaultAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstOrDefaultAsync(this System.Data.IDbConnection! cnn, System.Type! type, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstOrDefaultAsync(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstOrDefaultAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryFirstOrDefaultAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryMultiple(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> Dapper.SqlMapper.GridReader! +static Dapper.SqlMapper.QueryMultiple(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> Dapper.SqlMapper.GridReader! +static Dapper.SqlMapper.QueryMultipleAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryMultipleAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingle(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> dynamic! +static Dapper.SqlMapper.QuerySingle(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> object! +static Dapper.SqlMapper.QuerySingle(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> T +static Dapper.SqlMapper.QuerySingle(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> T +static Dapper.SqlMapper.QuerySingleAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleAsync(this System.Data.IDbConnection! cnn, System.Type! type, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleAsync(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleOrDefault(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> dynamic? +static Dapper.SqlMapper.QuerySingleOrDefault(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> object? +static Dapper.SqlMapper.QuerySingleOrDefault(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> T? +static Dapper.SqlMapper.QuerySingleOrDefault(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> T? +static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, System.Type! type, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, System.Type! type, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, Dapper.CommandDefinition command) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QuerySingleOrDefaultAsync(this System.Data.IDbConnection! cnn, string! sql, object? param = null, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Threading.Tasks.Task! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.QueryUnbufferedAsync(this System.Data.Common.DbConnection! cnn, string! sql, object? param = null, System.Data.Common.DbTransaction? transaction = null, int? commandTimeout = null, System.Data.CommandType? commandType = null) -> System.Collections.Generic.IAsyncEnumerable! +static Dapper.SqlMapper.ReadChar(object! value) -> char +static Dapper.SqlMapper.ReadNullableChar(object! value) -> char? +static Dapper.SqlMapper.RemoveTypeMap(System.Type! type) -> void +static Dapper.SqlMapper.ReplaceLiterals(this Dapper.SqlMapper.IParameterLookup! parameters, System.Data.IDbCommand! command) -> void +static Dapper.SqlMapper.ResetTypeHandlers() -> void +static Dapper.SqlMapper.SanitizeParameterValue(object? value) -> object! +static Dapper.SqlMapper.SetDbType(System.Data.IDataParameter! parameter, object! value) -> void +static Dapper.SqlMapper.Settings.ApplyNullValues.get -> bool +static Dapper.SqlMapper.Settings.ApplyNullValues.set -> void +static Dapper.SqlMapper.Settings.SupportLegacyParameterTokens.get -> bool +static Dapper.SqlMapper.Settings.SupportLegacyParameterTokens.set -> void +static Dapper.SqlMapper.Settings.CommandTimeout.get -> int? +static Dapper.SqlMapper.Settings.CommandTimeout.set -> void +static Dapper.SqlMapper.Settings.FetchSize.get -> long +static Dapper.SqlMapper.Settings.FetchSize.set -> void +static Dapper.SqlMapper.Settings.InListStringSplitCount.get -> int +static Dapper.SqlMapper.Settings.InListStringSplitCount.set -> void +static Dapper.SqlMapper.Settings.PadListExpansions.get -> bool +static Dapper.SqlMapper.Settings.PadListExpansions.set -> void +static Dapper.SqlMapper.Settings.SetDefaults() -> void +static Dapper.SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames.get -> bool +static Dapper.SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames.set -> void +static Dapper.SqlMapper.Settings.UseSingleResultOptimization.get -> bool +static Dapper.SqlMapper.Settings.UseSingleResultOptimization.set -> void +static Dapper.SqlMapper.Settings.UseSingleRowOptimization.get -> bool +static Dapper.SqlMapper.Settings.UseSingleRowOptimization.set -> void +static Dapper.SqlMapper.SetTypeMap(System.Type! type, Dapper.SqlMapper.ITypeMap? map) -> void +static Dapper.SqlMapper.SetTypeName(this System.Data.DataTable! table, string! typeName) -> void +static Dapper.SqlMapper.ThrowDataException(System.Exception! ex, int index, System.Data.IDataReader! reader, object? value) -> void +static Dapper.SqlMapper.ThrowNullCustomQueryParameter(string! name) -> void +static Dapper.SqlMapper.TypeHandlerCache.Parse(object! value) -> T? +static Dapper.SqlMapper.TypeHandlerCache.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void +static Dapper.SqlMapper.TypeMapProvider -> System.Func! \ No newline at end of file diff --git a/Dapper/PublicAPI.Unshipped.txt b/Dapper/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..cf9343ccf --- /dev/null +++ b/Dapper/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +#nullable enable +static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.get -> bool +static Dapper.SqlMapper.Settings.PreferTypeHandlersForEnums.set -> void \ No newline at end of file diff --git a/Dapper/PublicAPI/net461/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net461/PublicAPI.Shipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper/PublicAPI/net461/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/PublicAPI/net461/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net461/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper/PublicAPI/net461/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..ab058de62 --- /dev/null +++ b/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 000000000..91b0e1a43 --- /dev/null +++ b/Dapper/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/Dapper/SimpleMemberMap.cs b/Dapper/SimpleMemberMap.cs index 6c9ede863..46ff5a070 100644 --- a/Dapper/SimpleMemberMap.cs +++ b/Dapper/SimpleMemberMap.cs @@ -49,21 +49,21 @@ public SimpleMemberMap(string columnName, ParameterInfo parameter) /// /// Target member type /// - public Type MemberType => Field?.FieldType ?? Property?.PropertyType ?? Parameter?.ParameterType; + public Type MemberType => Field?.FieldType ?? Property?.PropertyType ?? Parameter?.ParameterType!; /// /// Target property /// - public PropertyInfo Property { get; } + public PropertyInfo? Property { get; } /// /// Target field /// - public FieldInfo Field { get; } + public FieldInfo? Field { get; } /// /// Target constructor parameter /// - public ParameterInfo Parameter { get; } + public ParameterInfo? Parameter { get; } } } diff --git a/Dapper/SqlDataRecordHandler.cs b/Dapper/SqlDataRecordHandler.cs index 301aa36ab..2d9976338 100644 --- a/Dapper/SqlDataRecordHandler.cs +++ b/Dapper/SqlDataRecordHandler.cs @@ -4,7 +4,8 @@ namespace Dapper { - internal sealed class SqlDataRecordHandler : SqlMapper.ITypeHandler + internal sealed class SqlDataRecordHandler : SqlMapper.ITypeHandler + where T : IDataRecord { public object Parse(Type destinationType, object value) { @@ -13,7 +14,7 @@ public object Parse(Type destinationType, object value) public void SetValue(IDbDataParameter parameter, object value) { - SqlDataRecordListTVPParameter.Set(parameter, value as IEnumerable, null); + SqlDataRecordListTVPParameter.Set(parameter, value as IEnumerable, null); } } } diff --git a/Dapper/SqlDataRecordListTVPParameter.cs b/Dapper/SqlDataRecordListTVPParameter.cs index 8e8384705..28a6ab27c 100644 --- a/Dapper/SqlDataRecordListTVPParameter.cs +++ b/Dapper/SqlDataRecordListTVPParameter.cs @@ -1,22 +1,27 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Data; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; namespace Dapper { /// /// Used to pass a IEnumerable<SqlDataRecord> as a SqlDataRecordListTVPParameter /// - internal sealed class SqlDataRecordListTVPParameter : SqlMapper.ICustomQueryParameter + internal sealed class SqlDataRecordListTVPParameter : SqlMapper.ICustomQueryParameter + where T : IDataRecord { - private readonly IEnumerable data; + private readonly IEnumerable data; private readonly string typeName; /// - /// Create a new instance of . + /// Create a new instance of . /// /// The data records to convert into TVPs. /// The parameter type name. - public SqlDataRecordListTVPParameter(IEnumerable data, string typeName) + public SqlDataRecordListTVPParameter(IEnumerable data, string typeName) { this.data = data; this.typeName = typeName; @@ -30,14 +35,72 @@ void SqlMapper.ICustomQueryParameter.AddParameter(IDbCommand command, string nam command.Parameters.Add(param); } - internal static void Set(IDbDataParameter parameter, IEnumerable data, string typeName) + internal static void Set(IDbDataParameter parameter, IEnumerable? data, string? typeName) { - parameter.Value = (object)data ?? DBNull.Value; - if (parameter is System.Data.SqlClient.SqlParameter sqlParam) + parameter.Value = data is not null && data.Any() ? data : null; + StructuredHelper.ConfigureTVP(parameter, typeName); + } + } + static class StructuredHelper + { + private static readonly Hashtable s_udt = new Hashtable(), s_tvp = new Hashtable(); + + private static Action GetUDT(Type type) + => (Action?)s_udt[type] ?? SlowGetHelper(type, s_udt, "UdtTypeName", 29); // 29 = SqlDbType.Udt (avoiding ref) + private static Action GetTVP(Type type) + => (Action?)s_tvp[type] ?? SlowGetHelper(type, s_tvp, "TypeName", 30); // 30 = SqlDbType.Structured (avoiding ref) + + static Action SlowGetHelper(Type type, Hashtable hashtable, string nameProperty, int sqlDbType) + { + lock (hashtable) { - sqlParam.SqlDbType = SqlDbType.Structured; - sqlParam.TypeName = typeName; + var helper = (Action?)hashtable[type]; + if (helper is null) + { + helper = CreateFor(type, nameProperty, sqlDbType); + hashtable.Add(type, helper); + } + return helper; } } + + static Action CreateFor(Type type, string nameProperty, int sqlDbType) + { + var name = type.GetProperty(nameProperty, BindingFlags.Public | BindingFlags.Instance); + if (name is null || !name.CanWrite) + { + return (p, n) => { }; + } + + var dbType = type.GetProperty("SqlDbType", BindingFlags.Public | BindingFlags.Instance); + if (dbType is not null && !dbType.CanWrite) dbType = null; + + var dm = new DynamicMethod(nameof(CreateFor) + "_" + type.Name, null, + new[] { typeof(IDbDataParameter), typeof(string) }, true); + var il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, type); + il.Emit(OpCodes.Ldarg_1); + il.EmitCall(OpCodes.Callvirt, name.GetSetMethod()!, null); + + if (dbType is not null) + { + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, type); + il.Emit(OpCodes.Ldc_I4, sqlDbType); + il.EmitCall(OpCodes.Callvirt, dbType.GetSetMethod()!, null); + } + + il.Emit(OpCodes.Ret); + return (Action)dm.CreateDelegate(typeof(Action)); + + } + + // this needs to be done per-provider; "dynamic" doesn't work well on all runtimes, although that + // would be a fair option otherwise + internal static void ConfigureUDT(IDbDataParameter parameter, string typeName) + => GetUDT(parameter.GetType())(parameter, typeName); + internal static void ConfigureTVP(IDbDataParameter parameter, string? typeName) + => GetTVP(parameter.GetType())(parameter, typeName); } } diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index ddc2ed6f6..eade08cb2 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -5,6 +5,7 @@ using System.Data.Common; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -13,7 +14,7 @@ namespace Dapper public static partial class SqlMapper { /// - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The connection to query on. /// The SQL to execute for the query. @@ -22,11 +23,12 @@ public static partial class SqlMapper /// The command timeout (in seconds). /// The type of command to execute. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static Task> QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryAsync(cnn, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryAsync(cnn, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); /// - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The connection to query on. /// The command used to query on this connection. @@ -35,7 +37,7 @@ public static Task> QueryAsync(this IDbConnection cnn, Comm QueryAsync(cnn, typeof(DapperRow), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The command used to query on this connection. @@ -44,16 +46,16 @@ public static Task QueryFirstAsync(this IDbConnection cnn, CommandDefin QueryRowAsync(cnn, Row.First, typeof(DapperRow), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The command used to query on this connection. /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => - QueryRowAsync(cnn, Row.FirstOrDefault, typeof(DapperRow), command); + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => + QueryRowAsync(cnn, Row.FirstOrDefault, typeof(DapperRow), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The command used to query on this connection. @@ -62,16 +64,16 @@ public static Task QuerySingleAsync(this IDbConnection cnn, CommandDefi QueryRowAsync(cnn, Row.Single, typeof(DapperRow), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The command used to query on this connection. /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => - QueryRowAsync(cnn, Row.SingleOrDefault, typeof(DapperRow), command); + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => + QueryRowAsync(cnn, Row.SingleOrDefault, typeof(DapperRow), command); /// - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The type of results to return. /// The connection to query on. @@ -81,14 +83,15 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Co /// The command timeout (in seconds). /// The type of command to execute. /// - /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static Task> QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryAsync(cnn, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryAsync(cnn, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type of result to return. /// The connection to query on. @@ -97,11 +100,12 @@ public static Task> QueryAsync(this IDbConnection cnn, string /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QueryFirstAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.First, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QueryFirstAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.First, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type of result to return. /// The connection to query on. @@ -110,11 +114,12 @@ public static Task QueryFirstAsync(this IDbConnection cnn, string sql, obj /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.FirstOrDefault, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.FirstOrDefault, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type of result to return. /// The connection to query on. @@ -123,11 +128,12 @@ public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, string /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QuerySingleAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.Single, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QuerySingleAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.Single, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type to return. /// The connection to query on. @@ -136,11 +142,12 @@ public static Task QuerySingleAsync(this IDbConnection cnn, string sql, ob /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.SingleOrDefault, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.SingleOrDefault, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The SQL to execute for the query. @@ -148,11 +155,12 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, strin /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QueryFirstAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.First, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QueryFirstAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.First, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The SQL to execute for the query. @@ -160,11 +168,12 @@ public static Task QueryFirstAsync(this IDbConnection cnn, string sql, /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.FirstOrDefault, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.FirstOrDefault, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The SQL to execute for the query. @@ -172,11 +181,12 @@ public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, str /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QuerySingleAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.Single, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QuerySingleAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.Single, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The SQL to execute for the query. @@ -184,11 +194,12 @@ public static Task QuerySingleAsync(this IDbConnection cnn, string sql, /// The transaction to use, if any. /// The command timeout (in seconds). /// The type of command to execute. - public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryRowAsync(cnn, Row.SingleOrDefault, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryRowAsync(cnn, Row.SingleOrDefault, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); /// - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -198,14 +209,15 @@ public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, st /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static Task> QueryAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return QueryAsync(cnn, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + if (type is null) throw new ArgumentNullException(nameof(type)); + return QueryAsync(cnn, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); } /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -215,13 +227,14 @@ public static Task> QueryAsync(this IDbConnection cnn, Type /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static Task QueryFirstAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QueryFirstAsync(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return QueryRowAsync(cnn, Row.First, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + if (type is null) throw new ArgumentNullException(nameof(type)); + return QueryRowAsync(cnn, Row.First, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); } /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -231,13 +244,14 @@ public static Task QueryFirstAsync(this IDbConnection cnn, Type type, st /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return QueryRowAsync(cnn, Row.FirstOrDefault, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + if (type is null) throw new ArgumentNullException(nameof(type)); + return QueryRowAsync(cnn, Row.FirstOrDefault, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); } /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -247,13 +261,14 @@ public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, Type /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static Task QuerySingleAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QuerySingleAsync(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return QueryRowAsync(cnn, Row.Single, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + if (type is null) throw new ArgumentNullException(nameof(type)); + return QueryRowAsync(cnn, Row.Single, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); } /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -263,27 +278,28 @@ public static Task QuerySingleAsync(this IDbConnection cnn, Type type, s /// The command timeout (in seconds). /// The type of command to execute. /// is null. - public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return QueryRowAsync(cnn, Row.SingleOrDefault, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + if (type is null) throw new ArgumentNullException(nameof(type)); + return QueryRowAsync(cnn, Row.SingleOrDefault, type, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); } /// - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The type to return. /// The connection to query on. /// The command used to query on this connection. /// - /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command) => QueryAsync(cnn, typeof(T), command); /// - /// Execute a query asynchronously using .NET 4.5 Task. + /// Execute a query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -292,7 +308,7 @@ public static Task> QueryAsync(this IDbConnection cnn, Type QueryAsync(cnn, type, command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -301,7 +317,7 @@ public static Task QueryFirstAsync(this IDbConnection cnn, Type type, Co QueryRowAsync(cnn, Row.First, type, command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type to return. /// The connection to query on. @@ -310,25 +326,25 @@ public static Task QueryFirstAsync(this IDbConnection cnn, CommandDefiniti QueryRowAsync(cnn, Row.First, typeof(T), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. /// The command used to query on this connection. - public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command) => - QueryRowAsync(cnn, Row.FirstOrDefault, type, command); + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command) => + QueryRowAsync(cnn, Row.FirstOrDefault, type, command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type to return. /// The connection to query on. /// The command used to query on this connection. - public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => - QueryRowAsync(cnn, Row.FirstOrDefault, typeof(T), command); + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => + QueryRowAsync(cnn, Row.FirstOrDefault, typeof(T), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. @@ -337,7 +353,7 @@ public static Task QuerySingleAsync(this IDbConnection cnn, Type type, C QueryRowAsync(cnn, Row.Single, type, command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type to return. /// The connection to query on. @@ -346,155 +362,159 @@ public static Task QuerySingleAsync(this IDbConnection cnn, CommandDefinit QueryRowAsync(cnn, Row.Single, typeof(T), command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The connection to query on. /// The type to return. /// The command used to query on this connection. - public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command) => - QueryRowAsync(cnn, Row.SingleOrDefault, type, command); + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command) => + QueryRowAsync(cnn, Row.SingleOrDefault, type, command); /// - /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// Execute a single-row query asynchronously using Task. /// /// The type to return. /// The connection to query on. /// The command used to query on this connection. - public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => - QueryRowAsync(cnn, Row.SingleOrDefault, typeof(T), command); + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, CommandDefinition command) => + QueryRowAsync(cnn, Row.SingleOrDefault, typeof(T), command); private static Task ExecuteReaderWithFlagsFallbackAsync(DbCommand cmd, bool wasClosed, CommandBehavior behavior, CancellationToken cancellationToken) { var task = cmd.ExecuteReaderAsync(GetBehavior(wasClosed, behavior), cancellationToken); - if (task.Status == TaskStatus.Faulted && Settings.DisableCommandBehaviorOptimizations(behavior, task.Exception.InnerException)) + if (task.Status == TaskStatus.Faulted && Settings.DisableCommandBehaviorOptimizations(behavior, task.Exception!.InnerException!)) { // we can retry; this time it will have different flags return cmd.ExecuteReaderAsync(GetBehavior(wasClosed, behavior), cancellationToken); } return task; } + /// + /// Attempts to open a connection asynchronously, with a better error message for unsupported usages. + /// + private static Task TryOpenAsync(this IDbConnection cnn, CancellationToken cancel) + { + if (cnn is DbConnection dbConn) + { + return dbConn.OpenAsync(cancel); + } + else + { + throw new InvalidOperationException("Async operations require use of a DbConnection or an already-open IDbConnection"); + } + } + + /// + /// Attempts setup a on a , with a better error message for unsupported usages. + /// + private static DbCommand TrySetupAsyncCommand(this CommandDefinition command, IDbConnection cnn, Action? paramReader) + { + if (command.SetupCommand(cnn, paramReader) is DbCommand dbCommand) + { + return dbCommand; + } + else + { + throw new InvalidOperationException("Async operations require use of a DbConnection or an IDbConnection where .CreateCommand() returns a DbCommand"); + } + } + private static async Task> QueryAsync(this IDbConnection cnn, Type effectiveType, CommandDefinition command) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType(), null); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; var cancel = command.CancellationToken; - using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader)) + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + DbDataReader? reader = null; + try { - DbDataReader reader = null; - try - { - if (wasClosed) await ((DbConnection)cnn).OpenAsync(cancel).ConfigureAwait(false); - reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancel).ConfigureAwait(false); + if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false); + reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancel).ConfigureAwait(false); - var tuple = info.Deserializer; - int hash = GetColumnHash(reader); - if (tuple.Func == null || tuple.Hash != hash) - { - tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); - if (command.AddToCache) SetQueryCache(identity, info); - } + var tuple = info.Deserializer; + int hash = GetColumnHash(reader); + if (tuple.Func is null || tuple.Hash != hash) + { + if (reader.FieldCount == 0) + return Enumerable.Empty(); + tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); + if (command.AddToCache) SetQueryCache(identity, info); + } - var func = tuple.Func; + var func = tuple.Func; - if (command.Buffered) - { - var buffer = new List(); - var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; - while (await reader.ReadAsync(cancel).ConfigureAwait(false)) - { - object val = func(reader); - if (val == null || val is T) - { - buffer.Add((T)val); - } - else - { - buffer.Add((T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture)); - } - } - while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } - command.OnCompleted(); - return buffer; - } - else + if (command.Buffered) + { + var buffer = new List(); + var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; + while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { - // can't use ReadAsync / cancellation; but this will have to do - wasClosed = false; // don't close if handing back an open reader; rely on the command-behavior - var deferred = ExecuteReaderSync(reader, func, command.Parameters); - reader = null; // to prevent it being disposed before the caller gets to see it - return deferred; + object val = func(reader); + buffer.Add(GetValue(reader, effectiveType, val)); } + while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } + command.OnCompleted(); + return buffer; } - finally + else { - using (reader) { /* dispose if non-null */ } - if (wasClosed) cnn.Close(); + // can't use ReadAsync / cancellation; but this will have to do + wasClosed = false; // don't close if handing back an open reader; rely on the command-behavior + var deferred = ExecuteReaderSync(reader, func, command.Parameters); + reader = null; // to prevent it being disposed before the caller gets to see it + return deferred; } } + finally + { + using (reader) { /* dispose if non-null */ } + if (wasClosed) cnn.Close(); + } } private static async Task QueryRowAsync(this IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType(), null); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; var cancel = command.CancellationToken; - using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader)) + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + DbDataReader? reader = null; + try { - DbDataReader reader = null; - try - { - if (wasClosed) await ((DbConnection)cnn).OpenAsync(cancel).ConfigureAwait(false); - reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, (row & Row.Single) != 0 - ? CommandBehavior.SequentialAccess | CommandBehavior.SingleResult // need to allow multiple rows, to check fail condition - : CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancel).ConfigureAwait(false); + if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false); + reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, (row & Row.Single) != 0 + ? CommandBehavior.SequentialAccess | CommandBehavior.SingleResult // need to allow multiple rows, to check fail condition + : CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow, cancel).ConfigureAwait(false); - T result = default(T); - if (await reader.ReadAsync(cancel).ConfigureAwait(false) && reader.FieldCount != 0) - { - var tuple = info.Deserializer; - int hash = GetColumnHash(reader); - if (tuple.Func == null || tuple.Hash != hash) - { - tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); - if (command.AddToCache) SetQueryCache(identity, info); - } - - var func = tuple.Func; + T result = default!; + if (await reader.ReadAsync(cancel).ConfigureAwait(false) && reader.FieldCount != 0) + { + result = ReadRow(info, identity, ref command, effectiveType, reader); - object val = func(reader); - if (val == null || val is T) - { - result = (T)val; - } - else - { - var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; - result = (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); - } - if ((row & Row.Single) != 0 && await reader.ReadAsync(cancel).ConfigureAwait(false)) ThrowMultipleRows(row); - while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { /* ignore rows after the first */ } - } - else if ((row & Row.FirstOrDefault) == 0) // demanding a row, and don't have one - { - ThrowZeroRows(row); - } - while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore result sets after the first */ } - return result; + if ((row & Row.Single) != 0 && await reader.ReadAsync(cancel).ConfigureAwait(false)) ThrowMultipleRows(row); + while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { /* ignore rows after the first */ } } - finally + else if ((row & Row.FirstOrDefault) == 0) // demanding a row, and don't have one { - using (reader) { /* dispose if non-null */ } - if (wasClosed) cnn.Close(); + ThrowZeroRows(row); } + while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore result sets after the first */ } + command.OnCompleted(); + return result; + } + finally + { + using (reader) { /* dispose if non-null */ } + if (wasClosed) cnn.Close(); } } /// - /// Execute a command asynchronously using .NET 4.5 Task. + /// Execute a command asynchronously using Task. /// /// The connection to query on. /// The SQL to execute for this query. @@ -503,20 +523,20 @@ private static async Task QueryRowAsync(this IDbConnection cnn, Row row, T /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// The number of rows affected. - public static Task ExecuteAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - ExecuteAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + public static Task ExecuteAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + ExecuteAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default)); /// - /// Execute a command asynchronously using .NET 4.5 Task. + /// Execute a command asynchronously using Task. /// /// The connection to execute on. /// The command to execute on this connection. /// The number of rows affected. public static Task ExecuteAsync(this IDbConnection cnn, CommandDefinition command) { - object param = command.Parameters; - IEnumerable multiExec = GetMultiExec(param); - if (multiExec != null) + object? param = command.Parameters; + IEnumerable? multiExec = GetMultiExec(param); + if (multiExec is not null) { return ExecuteMultiImplAsync(cnn, command, multiExec); } @@ -526,7 +546,7 @@ public static Task ExecuteAsync(this IDbConnection cnn, CommandDefinition c } } - private struct AsyncExecState + private readonly struct AsyncExecState { public readonly DbCommand Command; public readonly Task Task; @@ -544,15 +564,15 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD bool wasClosed = cnn.State == ConnectionState.Closed; try { - if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); - CacheInfo info = null; - string masterSql = null; + CacheInfo? info = null; + string? masterSql = null; if ((command.Flags & CommandFlags.Pipelined) != 0) { const int MAX_PENDING = 100; var pending = new Queue(MAX_PENDING); - DbCommand cmd = null; + DbCommand? cmd = null; try { foreach (var obj in multiExec) @@ -560,9 +580,9 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD if (isFirst) { isFirst = false; - cmd = (DbCommand)command.SetupCommand(cnn, null); + cmd = command.TrySetupAsyncCommand(cnn, null); masterSql = cmd.CommandText; - var identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType(), null); + var identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType()); info = GetCacheInfo(identity, obj, command.AddToCache); } else if (pending.Count >= MAX_PENDING) @@ -575,9 +595,9 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD } else { - cmd = (DbCommand)command.SetupCommand(cnn, null); + cmd = command.TrySetupAsyncCommand(cnn, null); } - info.ParamReader(cmd, obj); + info!.ParamReader!(cmd, obj); var task = cmd.ExecuteNonQueryAsync(command.CancellationToken); pending.Enqueue(new AsyncExecState(cmd, task)); @@ -602,25 +622,23 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD } else { - using (var cmd = (DbCommand)command.SetupCommand(cnn, null)) + using var cmd = command.TrySetupAsyncCommand(cnn, null); + foreach (var obj in multiExec) { - foreach (var obj in multiExec) + if (isFirst) { - if (isFirst) - { - masterSql = cmd.CommandText; - isFirst = false; - var identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType(), null); - info = GetCacheInfo(identity, obj, command.AddToCache); - } - else - { - cmd.CommandText = masterSql; // because we do magic replaces on "in" etc - cmd.Parameters.Clear(); // current code is Add-tastic - } - info.ParamReader(cmd, obj); - total += await cmd.ExecuteNonQueryAsync(command.CancellationToken).ConfigureAwait(false); + masterSql = cmd.CommandText; + isFirst = false; + var identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType()); + info = GetCacheInfo(identity, obj, command.AddToCache); + } + else + { + cmd.CommandText = masterSql; // because we do magic replaces on "in" etc + cmd.Parameters.Clear(); // current code is Add-tastic } + info!.ParamReader!(cmd, obj); + total += await cmd.ExecuteNonQueryAsync(command.CancellationToken).ConfigureAwait(false); } } @@ -633,29 +651,27 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD return total; } - private static async Task ExecuteImplAsync(IDbConnection cnn, CommandDefinition command, object param) + private static async Task ExecuteImplAsync(IDbConnection cnn, CommandDefinition command, object? param) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param?.GetType(), null); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; - using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader)) + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + try { - try - { - if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); - var result = await cmd.ExecuteNonQueryAsync(command.CancellationToken).ConfigureAwait(false); - command.OnCompleted(); - return result; - } - finally - { - if (wasClosed) cnn.Close(); - } + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); + var result = await cmd.ExecuteNonQueryAsync(command.CancellationToken).ConfigureAwait(false); + command.OnCompleted(); + return result; + } + finally + { + if (wasClosed) cnn.Close(); } } /// - /// Perform a asynchronous multi-mapping query with 2 input types. + /// Perform an asynchronous multi-mapping query with 2 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -671,12 +687,13 @@ private static async Task ExecuteImplAsync(IDbConnection cnn, CommandDefini /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMapAsync(cnn, - new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default), map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 2 input types. + /// Perform an asynchronous multi-mapping query with 2 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -687,11 +704,12 @@ public static Task> QueryAsync(th /// The command to execute. /// The function to map row types to the return type. /// An enumerable of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") => MultiMapAsync(cnn, command, map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 3 input types. + /// Perform an asynchronous multi-mapping query with 3 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -708,12 +726,13 @@ public static Task> QueryAsync(th /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMapAsync(cnn, - new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default), map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 3 input types. + /// Perform an asynchronous multi-mapping query with 3 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -725,11 +744,12 @@ public static Task> QueryAsyncThe command to execute. /// The function to map row types to the return type. /// An enumerable of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") => MultiMapAsync(cnn, command, map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 4 input types. + /// Perform an asynchronous multi-mapping query with 4 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -747,12 +767,13 @@ public static Task> QueryAsyncNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMapAsync(cnn, - new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default), map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 4 input types. + /// Perform an asynchronous multi-mapping query with 4 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -765,11 +786,12 @@ public static Task> QueryAsyncThe command to execute. /// The function to map row types to the return type. /// An enumerable of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") => MultiMapAsync(cnn, command, map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 5 input types. + /// Perform an asynchronous multi-mapping query with 5 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -788,12 +810,13 @@ public static Task> QueryAsyncNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMapAsync(cnn, - new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default), map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 5 input types. + /// Perform an asynchronous multi-mapping query with 5 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -807,11 +830,12 @@ public static Task> QueryAsyncThe command to execute. /// The function to map row types to the return type. /// An enumerable of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") => MultiMapAsync(cnn, command, map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 6 input types. + /// Perform an asynchronous multi-mapping query with 6 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -831,12 +855,13 @@ public static Task> QueryAsyncNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMapAsync(cnn, - new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default), map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 6 input types. + /// Perform an asynchronous multi-mapping query with 6 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -851,11 +876,12 @@ public static Task> QueryAsyncThe command to execute. /// The function to map row types to the return type. /// An enumerable of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") => MultiMapAsync(cnn, command, map, splitOn); /// - /// Perform a asynchronous multi-mapping query with 7 input types. + /// Perform an asynchronous multi-mapping query with 7 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -876,12 +902,13 @@ public static Task> QueryAsyncNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMapAsync(cnn, - new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default), map, splitOn); /// - /// Perform an asynchronous multi-mapping query with 7 input types. + /// Perform an asynchronous multi-mapping query with 7 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -897,25 +924,24 @@ public static Task> QueryAsyncThe command to execute. /// The function to map row types to the return type. /// An enumerable of . + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") => MultiMapAsync(cnn, command, map, splitOn); private static async Task> MultiMapAsync(this IDbConnection cnn, CommandDefinition command, Delegate map, string splitOn) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(TFirst), param?.GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth), typeof(TSixth), typeof(TSeventh) }); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(TFirst), param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; try { - if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); - using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader)) - using (var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false)) - { - if (!command.Buffered) wasClosed = false; // handing back open reader; rely on command-behavior - var results = MultiMapImpl(null, CommandDefinition.ForCallback(command.Parameters), map, splitOn, reader, identity, true); - return command.Buffered ? results.ToList() : results; - } + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); + if (!command.Buffered) wasClosed = false; // handing back open reader; rely on command-behavior + var results = MultiMapImpl(null, CommandDefinition.ForCallback(command.Parameters, command.Flags), map, splitOn, reader, identity, true); + return command.Buffered ? results.ToList() : results; } finally { @@ -924,7 +950,7 @@ private static async Task> MultiMapAsync - /// Perform a asynchronous multi-mapping query with an arbitrary number of input types. + /// Perform an asynchronous multi-mapping query with an arbitrary number of input types. /// This returns a single type, combined from the raw types via . /// /// The combined type to return. @@ -939,9 +965,10 @@ private static async Task> MultiMapAsyncNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static Task> QueryAsync(this IDbConnection cnn, string sql, Type[] types, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task> QueryAsync(this IDbConnection cnn, string sql, Type[] types, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) { - var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)); + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default); return MultiMapAsync(cnn, command, types, map, splitOn); } @@ -952,19 +979,17 @@ private static async Task> MultiMapAsync(this IDbC throw new ArgumentException("you must provide at least one type to deserialize"); } - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, types[0], param?.GetType(), types); + object? param = command.Parameters; + var identity = new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn, types[0], param?.GetType(), types); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; try { - if (wasClosed) await ((DbConnection)cnn).OpenAsync().ConfigureAwait(false); - using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader)) - using (var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false)) - { - var results = MultiMapImpl(null, default(CommandDefinition), types, map, splitOn, reader, identity, true); - return command.Buffered ? results.ToList() : results; - } + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); + var results = MultiMapImpl(null, default, types, map, splitOn, reader, identity, true); + return command.Buffered ? results.ToList() : results; } finally { @@ -972,7 +997,7 @@ private static async Task> MultiMapAsync(this IDbC } } - private static IEnumerable ExecuteReaderSync(IDataReader reader, Func func, object parameters) + private static IEnumerable ExecuteReaderSync(DbDataReader reader, Func func, object? parameters) { using (reader) { @@ -994,7 +1019,7 @@ private static IEnumerable ExecuteReaderSync(IDataReader reader, FuncThe transaction to use for this query. /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? - public static Task QueryMultipleAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + public static Task QueryMultipleAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => QueryMultipleAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered)); /// @@ -1004,17 +1029,17 @@ public static Task QueryMultipleAsync(this IDbConnection cnn, string /// The command to execute for this query. public static async Task QueryMultipleAsync(this IDbConnection cnn, CommandDefinition command) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType(), null); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType()); CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); - DbCommand cmd = null; - IDataReader reader = null; + DbCommand? cmd = null; + DbDataReader? reader = null; bool wasClosed = cnn.State == ConnectionState.Closed; try { - if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); - cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader); + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); + cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess, command.CancellationToken).ConfigureAwait(false); var result = new GridReader(cmd, reader, identity, command.Parameters as DynamicParameters, command.AddToCache, command.CancellationToken); @@ -1026,11 +1051,11 @@ public static async Task QueryMultipleAsync(this IDbConnection cnn, } catch { - if (reader != null) + if (reader is not null) { if (!reader.IsClosed) { - try { cmd.Cancel(); } + try { cmd?.Cancel(); } catch { /* don't spoil the existing exception */ } @@ -1068,8 +1093,22 @@ public static async Task QueryMultipleAsync(this IDbConnection cnn, /// ]]> /// /// - public static Task ExecuteReaderAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - ExecuteReaderImplAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered), CommandBehavior.Default); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task ExecuteReaderAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + ExecuteWrappedReaderImplAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered), CommandBehavior.Default).CastResult(); + + /// + /// Execute parameterized SQL and return a . + /// + /// The connection to execute on. + /// The SQL to execute. + /// The parameters to use for this command. + /// The transaction to use for this command. + /// Number of seconds before command execution timeout. + /// Is it a stored proc or a batch? + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Task ExecuteReaderAsync(this DbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + ExecuteWrappedReaderImplAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered), CommandBehavior.Default); /// /// Execute parameterized SQL and return an . @@ -1082,7 +1121,15 @@ public static Task ExecuteReaderAsync(this IDbConnection cnn, strin /// or . /// public static Task ExecuteReaderAsync(this IDbConnection cnn, CommandDefinition command) => - ExecuteReaderImplAsync(cnn, command, CommandBehavior.Default); + ExecuteWrappedReaderImplAsync(cnn, command, CommandBehavior.Default).CastResult(); + + /// + /// Execute parameterized SQL and return a . + /// + /// The connection to execute on. + /// The command to execute. + public static Task ExecuteReaderAsync(this DbConnection cnn, CommandDefinition command) => + ExecuteWrappedReaderImplAsync(cnn, command, CommandBehavior.Default); /// /// Execute parameterized SQL and return an . @@ -1096,26 +1143,36 @@ public static Task ExecuteReaderAsync(this IDbConnection cnn, Comma /// or . /// public static Task ExecuteReaderAsync(this IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) => - ExecuteReaderImplAsync(cnn, command, commandBehavior); + ExecuteWrappedReaderImplAsync(cnn, command, commandBehavior).CastResult(); + + /// + /// Execute parameterized SQL and return a . + /// + /// The connection to execute on. + /// The command to execute. + /// The flags for this reader. + public static Task ExecuteReaderAsync(this DbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) => + ExecuteWrappedReaderImplAsync(cnn, command, commandBehavior); - private static async Task ExecuteReaderImplAsync(IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) + private static async Task ExecuteWrappedReaderImplAsync(IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) { - Action paramReader = GetParameterReader(cnn, ref command); + Action? paramReader = GetParameterReader(cnn, ref command); - DbCommand cmd = null; - bool wasClosed = cnn.State == ConnectionState.Closed; + DbCommand? cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed, disposeCommand = true; try { - cmd = (DbCommand)command.SetupCommand(cnn, paramReader); - if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); + cmd = command.TrySetupAsyncCommand(cnn, paramReader); + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, commandBehavior, command.CancellationToken).ConfigureAwait(false); wasClosed = false; - return reader; + disposeCommand = false; + return DbWrappedReader.Create(cmd, reader); } finally { if (wasClosed) cnn.Close(); - cmd?.Dispose(); + if (cmd is not null && disposeCommand) cmd.Dispose(); } } @@ -1129,7 +1186,7 @@ private static async Task ExecuteReaderImplAsync(IDbConnection cnn, /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// The first cell returned, as . - public static Task ExecuteScalarAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + public static Task ExecuteScalarAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => ExecuteScalarImplAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered)); /// @@ -1143,7 +1200,7 @@ public static Task ExecuteScalarAsync(this IDbConnection cnn, string sql /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// The first cell returned, as . - public static Task ExecuteScalarAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + public static Task ExecuteScalarAsync(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => ExecuteScalarImplAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered)); /// @@ -1152,36 +1209,36 @@ public static Task ExecuteScalarAsync(this IDbConnection cnn, string sql, /// The connection to execute on. /// The command to execute. /// The first cell selected as . - public static Task ExecuteScalarAsync(this IDbConnection cnn, CommandDefinition command) => + public static Task ExecuteScalarAsync(this IDbConnection cnn, CommandDefinition command) => ExecuteScalarImplAsync(cnn, command); /// - /// Execute parameterized SQL that selects a single value + /// Execute parameterized SQL that selects a single value. /// /// The type to return. /// The connection to execute on. /// The command to execute. /// The first cell selected as . - public static Task ExecuteScalarAsync(this IDbConnection cnn, CommandDefinition command) => + public static Task ExecuteScalarAsync(this IDbConnection cnn, CommandDefinition command) => ExecuteScalarImplAsync(cnn, command); - private static async Task ExecuteScalarImplAsync(IDbConnection cnn, CommandDefinition command) + private static async Task ExecuteScalarImplAsync(IDbConnection cnn, CommandDefinition command) { - Action paramReader = null; - object param = command.Parameters; - if (param != null) + Action? paramReader = null; + object? param = command.Parameters; + if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; } - DbCommand cmd = null; + DbCommand? cmd = null; bool wasClosed = cnn.State == ConnectionState.Closed; - object result; + object? result; try { - cmd = (DbCommand)command.SetupCommand(cnn, paramReader); - if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); + cmd = command.TrySetupAsyncCommand(cnn, paramReader); + if (wasClosed) await cnn.TryOpenAsync(command.CancellationToken).ConfigureAwait(false); result = await cmd.ExecuteScalarAsync(command.CancellationToken).ConfigureAwait(false); command.OnCompleted(); } @@ -1192,5 +1249,104 @@ private static async Task ExecuteScalarImplAsync(IDbConnection cnn, Comman } return Parse(result); } + + /// + /// Execute a query asynchronously using . + /// + /// The connection to query on. + /// The SQL to execute for the query. + /// The parameters to pass, if any. + /// The transaction to use, if any. + /// The command timeout (in seconds). + /// The type of command to execute. + /// + /// A sequence of data of dynamic data + /// + public static IAsyncEnumerable QueryUnbufferedAsync(this DbConnection cnn, string sql, object? param = null, DbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + // note: in many cases of adding a new async method I might add a CancellationToken - however, cancellation is expressed via WithCancellation on iterators + return QueryUnbufferedAsync(cnn, typeof(object), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); + } + + /// + /// Execute a query asynchronously using . + /// + /// The type of results to return. + /// The connection to query on. + /// The SQL to execute for the query. + /// The parameters to pass, if any. + /// The transaction to use, if any. + /// The command timeout (in seconds). + /// The type of command to execute. + /// + /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IAsyncEnumerable QueryUnbufferedAsync(this DbConnection cnn, string sql, object? param = null, DbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + // note: in many cases of adding a new async method I might add a CancellationToken - however, cancellation is expressed via WithCancellation on iterators + return QueryUnbufferedAsync(cnn, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default)); + } + + private static IAsyncEnumerable QueryUnbufferedAsync(this IDbConnection cnn, Type effectiveType, CommandDefinition command) + { + return Impl(cnn, effectiveType, command, command.CancellationToken); // proxy to allow CT expression + + static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, CommandDefinition command, + [EnumeratorCancellation] CancellationToken cancel) + { + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); + var info = GetCacheInfo(identity, param, command.AddToCache); + bool wasClosed = cnn.State == ConnectionState.Closed; + using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); + DbDataReader? reader = null; + try + { + if (wasClosed) await cnn.TryOpenAsync(cancel).ConfigureAwait(false); + reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, cancel).ConfigureAwait(false); + + var tuple = info.Deserializer; + int hash = GetColumnHash(reader); + if (tuple.Func is null || tuple.Hash != hash) + { + if (reader.FieldCount == 0) + { + yield break; + } + tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); + if (command.AddToCache) SetQueryCache(identity, info); + } + + var func = tuple.Func; + + var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; + while (await reader.ReadAsync(cancel).ConfigureAwait(false)) + { + object val = func(reader); + yield return GetValue(reader, effectiveType, val); + } + while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } + command.OnCompleted(); + } + finally + { + if (reader is not null) + { + if (!reader.IsClosed) + { + try { cmd?.Cancel(); } + catch { /* don't spoil any existing exception */ } + } +#if NET5_0_OR_GREATER + await reader.DisposeAsync(); +#else + reader.Dispose(); +#endif + } + if (wasClosed) cnn.Close(); + } + } + } } } diff --git a/Dapper/SqlMapper.CacheInfo.cs b/Dapper/SqlMapper.CacheInfo.cs index 409ea3411..69edc4eea 100644 --- a/Dapper/SqlMapper.CacheInfo.cs +++ b/Dapper/SqlMapper.CacheInfo.cs @@ -1,16 +1,17 @@ using System; using System.Data; +using System.Data.Common; using System.Threading; namespace Dapper { public static partial class SqlMapper { - private class CacheInfo + private sealed class CacheInfo { public DeserializerState Deserializer { get; set; } - public Func[] OtherDeserializers { get; set; } - public Action ParamReader { get; set; } + public Func[]? OtherDeserializers { get; set; } + public Action? ParamReader { get; set; } private int hitCount; public int GetHitCount() { return Interlocked.CompareExchange(ref hitCount, 0, 0); } public void RecordHit() { Interlocked.Increment(ref hitCount); } diff --git a/Dapper/SqlMapper.DapperRow.Descriptor.cs b/Dapper/SqlMapper.DapperRow.Descriptor.cs new file mode 100644 index 000000000..0824c6658 --- /dev/null +++ b/Dapper/SqlMapper.DapperRow.Descriptor.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Dapper +{ + public static partial class SqlMapper + { + [TypeDescriptionProvider(typeof(DapperRowTypeDescriptionProvider))] + private sealed partial class DapperRow + { + private sealed class DapperRowTypeDescriptionProvider : TypeDescriptionProvider + { + public override ICustomTypeDescriptor GetExtendedTypeDescriptor(object instance) + => new DapperRowTypeDescriptor(instance); + public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object? instance) + => new DapperRowTypeDescriptor(instance!); + } + + //// in theory we could implement this for zero-length results to bind; would require + //// additional changes, though, to capture a table even when no rows - so not currently provided + //internal sealed class DapperRowList : List, ITypedList + //{ + // private readonly DapperTable _table; + // public DapperRowList(DapperTable table) { _table = table; } + // PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors) + // { + // if (listAccessors is not null && listAccessors.Length != 0) return PropertyDescriptorCollection.Empty; + + // return DapperRowTypeDescriptor.GetProperties(_table); + // } + + // string ITypedList.GetListName(PropertyDescriptor[] listAccessors) => null; + //} + + private sealed class DapperRowTypeDescriptor : ICustomTypeDescriptor + { + private readonly DapperRow _row; + public DapperRowTypeDescriptor(object instance) + => _row = (DapperRow)instance; + + AttributeCollection ICustomTypeDescriptor.GetAttributes() + => AttributeCollection.Empty; + + string ICustomTypeDescriptor.GetClassName() => typeof(DapperRow).FullName!; + + string ICustomTypeDescriptor.GetComponentName() => null!; + + private static readonly TypeConverter s_converter = new ExpandableObjectConverter(); + TypeConverter ICustomTypeDescriptor.GetConverter() => s_converter; + + EventDescriptor ICustomTypeDescriptor.GetDefaultEvent() => null!; + + PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty() => null!; + + object ICustomTypeDescriptor.GetEditor(Type editorBaseType) => null!; + + EventDescriptorCollection ICustomTypeDescriptor.GetEvents() => EventDescriptorCollection.Empty; + + EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[]? attributes) => EventDescriptorCollection.Empty; + + internal static PropertyDescriptorCollection GetProperties(DapperRow row) => GetProperties(row?.table, row); + internal static PropertyDescriptorCollection GetProperties(DapperTable? table, IDictionary? row = null) + { + string[]? names = table?.FieldNames; + if (names is null || names.Length == 0) return PropertyDescriptorCollection.Empty; + var arr = new PropertyDescriptor[names.Length]; + for (int i = 0; i < arr.Length; i++) + { + var type = row is not null && row.TryGetValue(names[i], out var value) && value is not null + ? value.GetType() : typeof(object); + arr[i] = new RowBoundPropertyDescriptor(type, names[i], i); + } + return new PropertyDescriptorCollection(arr, true); + } + PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties() => GetProperties(_row); + + PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[]? attributes) => GetProperties(_row); + + object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor? pd) => _row; + } + + private sealed class RowBoundPropertyDescriptor : PropertyDescriptor + { + private readonly Type _type; + private readonly int _index; + public RowBoundPropertyDescriptor(Type type, string name, int index) : base(name, null) + { + _type = type; + _index = index; + } + public override bool CanResetValue(object component) => true; + public override void ResetValue(object component) => ((DapperRow)component).Remove(_index); + public override bool IsReadOnly => false; + public override bool ShouldSerializeValue(object component) => ((DapperRow)component).TryGetValue(_index, out _); + public override Type ComponentType => typeof(DapperRow); + public override Type PropertyType => _type; + public override object GetValue(object? component) + => ((DapperRow)component!).TryGetValue(_index, out var val) ? (val ?? DBNull.Value): DBNull.Value; + public override void SetValue(object? component, object? value) + => ((DapperRow)component!).SetValue(_index, value is DBNull ? null : value); + } + } + } +} diff --git a/Dapper/SqlMapper.DapperRow.cs b/Dapper/SqlMapper.DapperRow.cs index 399ca7e74..82b88d221 100644 --- a/Dapper/SqlMapper.DapperRow.cs +++ b/Dapper/SqlMapper.DapperRow.cs @@ -7,14 +7,14 @@ namespace Dapper { public static partial class SqlMapper { - private sealed class DapperRow - : System.Dynamic.IDynamicMetaObjectProvider - , IDictionary + private sealed partial class DapperRow + : IDictionary + , IReadOnlyDictionary { private readonly DapperTable table; - private object[] values; + private object?[] values; - public DapperRow(DapperTable table, object[] values) + public DapperRow(DapperTable table, object?[] values) { this.table = table ?? throw new ArgumentNullException(nameof(table)); this.values = values ?? throw new ArgumentNullException(nameof(values)); @@ -26,7 +26,7 @@ private sealed class DeadValue private DeadValue() { /* hiding constructor */ } } - int ICollection>.Count + int ICollection>.Count { get { @@ -39,9 +39,11 @@ int ICollection>.Count } } - public bool TryGetValue(string key, out object value) + public bool TryGetValue(string key, out object? value) + => TryGetValue(table.IndexOfName(key), out value); + + internal bool TryGetValue(int index, out object? value) { - var index = table.IndexOfName(key); if (index < 0) { // doesn't exist value = null; @@ -64,7 +66,7 @@ public override string ToString() { var value = kv.Value; sb.Append(", ").Append(kv.Key); - if (value != null) + if (value is not null) { sb.Append(" = '").Append(kv.Value).Append('\''); } @@ -74,24 +76,18 @@ public override string ToString() } } - return sb.Append('}').__ToStringRecycle(); - } - - System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject( - System.Linq.Expressions.Expression parameter) - { - return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this); + return sb.Append('}').ToStringRecycle(); } - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { var names = table.FieldNames; for (var i = 0; i < names.Length; i++) { - object value = i < values.Length ? values[i] : null; + object? value = i < values.Length ? values[i] : null!; if (!(value is DeadValue)) { - yield return new KeyValuePair(names[i], value); + yield return new KeyValuePair(names[i], value); } } } @@ -103,24 +99,24 @@ IEnumerator IEnumerable.GetEnumerator() #region Implementation of ICollection> - void ICollection>.Add(KeyValuePair item) + void ICollection>.Add(KeyValuePair item) { - IDictionary dic = this; + IDictionary dic = this; dic.Add(item.Key, item.Value); } - void ICollection>.Clear() + void ICollection>.Clear() { // removes values for **this row**, but doesn't change the fundamental table for (int i = 0; i < values.Length; i++) values[i] = DeadValue.Default; } - bool ICollection>.Contains(KeyValuePair item) + bool ICollection>.Contains(KeyValuePair item) { - return TryGetValue(item.Key, out object value) && Equals(value, item.Value); + return TryGetValue(item.Key, out object? value) && Equals(value, item.Value); } - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { foreach (var kv in this) { @@ -128,51 +124,53 @@ void ICollection>.CopyTo(KeyValuePair>.Remove(KeyValuePair item) + bool ICollection>.Remove(KeyValuePair item) { - IDictionary dic = this; + IDictionary dic = this; return dic.Remove(item.Key); } - bool ICollection>.IsReadOnly => false; + bool ICollection>.IsReadOnly => false; #endregion #region Implementation of IDictionary - bool IDictionary.ContainsKey(string key) + bool IDictionary.ContainsKey(string key) { int index = table.IndexOfName(key); if (index < 0 || index >= values.Length || values[index] is DeadValue) return false; return true; } - void IDictionary.Add(string key, object value) + void IDictionary.Add(string key, object? value) { SetValue(key, value, true); } - bool IDictionary.Remove(string key) + bool IDictionary.Remove(string key) + => Remove(table.IndexOfName(key)); + + internal bool Remove(int index) { - int index = table.IndexOfName(key); if (index < 0 || index >= values.Length || values[index] is DeadValue) return false; values[index] = DeadValue.Default; return true; } - object IDictionary.this[string key] + object? IDictionary.this[string key] { - get { TryGetValue(key, out object val); return val; } + get { TryGetValue(key, out object? val); return val; } set { SetValue(key, value, false); } } - public object SetValue(string key, object value) + public object? SetValue(string key, object? value) { return SetValue(key, value, false); } - private object SetValue(string key, object value, bool isAdd) + private object? SetValue(string key, object? value, bool isAdd) { - if (key == null) throw new ArgumentNullException(nameof(key)); + if (key is null) throw new ArgumentNullException(nameof(key)); int index = table.IndexOfName(key); if (index < 0) { @@ -183,6 +181,10 @@ private object SetValue(string key, object value, bool isAdd) // then semantically, this value already exists throw new ArgumentException("An item with the same key has already been added", nameof(key)); } + return SetValue(index, value); + } + internal object? SetValue(int index, object? value) + { int oldLength = values.Length; if (oldLength <= index) { @@ -197,17 +199,52 @@ private object SetValue(string key, object value, bool isAdd) return values[index] = value; } - ICollection IDictionary.Keys + ICollection IDictionary.Keys { get { return this.Select(kv => kv.Key).ToArray(); } } - ICollection IDictionary.Values + ICollection IDictionary.Values { get { return this.Select(kv => kv.Value).ToArray(); } } #endregion + + + #region Implementation of IReadOnlyDictionary + + + int IReadOnlyCollection>.Count + { + get + { + return values.Count(t => !(t is DeadValue)); + } + } + + bool IReadOnlyDictionary.ContainsKey(string key) + { + int index = table.IndexOfName(key); + return index >= 0 && index < values.Length && !(values[index] is DeadValue); + } + + object? IReadOnlyDictionary.this[string key] + { + get { TryGetValue(key, out object? val); return val; } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get { return this.Select(kv => kv.Key); } + } + + IEnumerable IReadOnlyDictionary.Values + { + get { return this.Select(kv => kv.Value); } + } + + #endregion } } } diff --git a/Dapper/SqlMapper.DapperRowMetaObject.cs b/Dapper/SqlMapper.DapperRowMetaObject.cs index 789ec680d..7b5350dfb 100644 --- a/Dapper/SqlMapper.DapperRowMetaObject.cs +++ b/Dapper/SqlMapper.DapperRowMetaObject.cs @@ -5,10 +5,19 @@ namespace Dapper { public static partial class SqlMapper { + private sealed partial class DapperRow : System.Dynamic.IDynamicMetaObjectProvider + { + System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject( + System.Linq.Expressions.Expression parameter) + { + return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this); + } + } + private sealed class DapperRowMetaObject : System.Dynamic.DynamicMetaObject { - private static readonly MethodInfo getValueMethod = typeof(IDictionary).GetProperty("Item").GetGetMethod(); - private static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue", new Type[] { typeof(string), typeof(object) }); + private static readonly MethodInfo getValueMethod = typeof(IDictionary).GetProperty("Item")!.GetGetMethod()!; + private static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue", new Type[] { typeof(string), typeof(object) })!; public DapperRowMetaObject( System.Linq.Expressions.Expression expression, @@ -72,13 +81,19 @@ public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.Se var parameters = new System.Linq.Expressions.Expression[] { System.Linq.Expressions.Expression.Constant(binder.Name), - value.Expression, + System.Linq.Expressions.Expression.Convert(value.Expression, typeof(object)), }; var callMethod = CallMethod(setValueMethod, parameters); return callMethod; } + + public override IEnumerable GetDynamicMemberNames() + { + if(HasValue && Value is IDictionary lookup) return lookup.Keys; + return Array.Empty(); + } } } } diff --git a/Dapper/SqlMapper.DapperTable.cs b/Dapper/SqlMapper.DapperTable.cs index f827286f9..760192c1b 100644 --- a/Dapper/SqlMapper.DapperTable.cs +++ b/Dapper/SqlMapper.DapperTable.cs @@ -21,18 +21,18 @@ public DapperTable(string[] fieldNames) for (int i = fieldNames.Length - 1; i >= 0; i--) { string key = fieldNames[i]; - if (key != null) fieldNameLookup[key] = i; + if (key is not null) fieldNameLookup[key] = i; } } internal int IndexOfName(string name) { - return (name != null && fieldNameLookup.TryGetValue(name, out int result)) ? result : -1; + return (name is not null && fieldNameLookup.TryGetValue(name, out int result)) ? result : -1; } internal int AddField(string name) { - if (name == null) throw new ArgumentNullException(nameof(name)); + if (name is null) throw new ArgumentNullException(nameof(name)); if (fieldNameLookup.ContainsKey(name)) throw new InvalidOperationException("Field already exists: " + name); int oldLen = fieldNames.Length; Array.Resize(ref fieldNames, oldLen + 1); // yes, this is sub-optimal, but this is not the expected common case @@ -41,7 +41,7 @@ internal int AddField(string name) return oldLen; } - internal bool FieldExists(string key) => key != null && fieldNameLookup.ContainsKey(key); + internal bool FieldExists(string key) => key is not null && fieldNameLookup.ContainsKey(key); public int FieldCount => fieldNames.Length; } diff --git a/Dapper/SqlMapper.DeserializerState.cs b/Dapper/SqlMapper.DeserializerState.cs index 26b176cc1..4b594e0f5 100644 --- a/Dapper/SqlMapper.DeserializerState.cs +++ b/Dapper/SqlMapper.DeserializerState.cs @@ -1,16 +1,17 @@ using System; using System.Data; +using System.Data.Common; namespace Dapper { public static partial class SqlMapper { - private struct DeserializerState + private readonly struct DeserializerState { public readonly int Hash; - public readonly Func Func; + public readonly Func Func; - public DeserializerState(int hash, Func func) + public DeserializerState(int hash, Func func) { Hash = hash; Func = func; diff --git a/Dapper/SqlMapper.DontMap.cs b/Dapper/SqlMapper.DontMap.cs index a97c5811e..765c7b937 100644 --- a/Dapper/SqlMapper.DontMap.cs +++ b/Dapper/SqlMapper.DontMap.cs @@ -5,6 +5,6 @@ public static partial class SqlMapper /// /// Dummy type for excluding from multi-map /// - private class DontMap { /* hiding constructor */ } + private sealed class DontMap { /* hiding constructor */ } } } diff --git a/Dapper/SqlMapper.GridReader.Async.cs b/Dapper/SqlMapper.GridReader.Async.cs index 89528826b..a32d53124 100644 --- a/Dapper/SqlMapper.GridReader.Async.cs +++ b/Dapper/SqlMapper.GridReader.Async.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Data; using System.Data.Common; -using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -11,14 +10,8 @@ namespace Dapper public static partial class SqlMapper { public partial class GridReader + : IAsyncDisposable { - private readonly CancellationToken cancel; - internal GridReader(IDbCommand command, IDataReader reader, Identity identity, DynamicParameters dynamicParams, bool addToCache, CancellationToken cancel) - : this(command, reader, identity, dynamicParams, addToCache) - { - this.cancel = cancel; - } - /// /// Read the next grid of results, returned as a dynamic object /// @@ -36,7 +29,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, D /// Read an individual row of the next grid of results, returned as a dynamic object /// /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public Task ReadFirstOrDefaultAsync() => ReadRowAsyncImpl(typeof(DapperRow), Row.FirstOrDefault); + public Task ReadFirstOrDefaultAsync() => ReadRowAsyncImpl(typeof(DapperRow), Row.FirstOrDefault); /// /// Read an individual row of the next grid of results, returned as a dynamic object @@ -48,7 +41,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, D /// Read an individual row of the next grid of results, returned as a dynamic object /// /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public Task ReadSingleOrDefaultAsync() => ReadRowAsyncImpl(typeof(DapperRow), Row.SingleOrDefault); + public Task ReadSingleOrDefaultAsync() => ReadRowAsyncImpl(typeof(DapperRow), Row.SingleOrDefault); /// /// Read the next grid of results @@ -58,7 +51,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, D /// is null. public Task> ReadAsync(Type type, bool buffered = true) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadAsyncImpl(type, buffered); } @@ -69,7 +62,7 @@ public Task> ReadAsync(Type type, bool buffered = true) /// is null. public Task ReadFirstAsync(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadRowAsyncImpl(type, Row.First); } @@ -78,10 +71,10 @@ public Task ReadFirstAsync(Type type) /// /// The type to read. /// is null. - public Task ReadFirstOrDefaultAsync(Type type) + public Task ReadFirstOrDefaultAsync(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return ReadRowAsyncImpl(type, Row.FirstOrDefault); + if (type is null) throw new ArgumentNullException(nameof(type)); + return ReadRowAsyncImpl(type, Row.FirstOrDefault); } /// @@ -91,7 +84,7 @@ public Task ReadFirstOrDefaultAsync(Type type) /// is null. public Task ReadSingleAsync(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadRowAsyncImpl(type, Row.Single); } @@ -100,10 +93,10 @@ public Task ReadSingleAsync(Type type) /// /// The type to read. /// is null. - public Task ReadSingleOrDefaultAsync(Type type) + public Task ReadSingleOrDefaultAsync(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); - return ReadRowAsyncImpl(type, Row.SingleOrDefault); + if (type is null) throw new ArgumentNullException(nameof(type)); + return ReadRowAsyncImpl(type, Row.SingleOrDefault); } /// @@ -123,7 +116,7 @@ public Task ReadSingleOrDefaultAsync(Type type) /// Read an individual row of the next grid of results. /// /// The type to read. - public Task ReadFirstOrDefaultAsync() => ReadRowAsyncImpl(typeof(T), Row.FirstOrDefault); + public Task ReadFirstOrDefaultAsync() => ReadRowAsyncImpl(typeof(T), Row.FirstOrDefault); /// /// Read an individual row of the next grid of results. @@ -135,82 +128,92 @@ public Task ReadSingleOrDefaultAsync(Type type) /// Read an individual row of the next grid of results. /// /// The type to read. - public Task ReadSingleOrDefaultAsync() => ReadRowAsyncImpl(typeof(T), Row.SingleOrDefault); + public Task ReadSingleOrDefaultAsync() => ReadRowAsyncImpl(typeof(T), Row.SingleOrDefault); - private async Task NextResultAsync() + /// + /// Marks the current grid as consumed, and moves to the next result + /// + protected async Task OnAfterGridAsync(int index) { - if (await ((DbDataReader)reader).NextResultAsync(cancel).ConfigureAwait(false)) + if (index != ResultIndex) + { + // not our data + } + else if (reader is null) + { + // nothing to do + } + else if (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { - readCount++; - gridIndex++; - IsConsumed = false; + // readCount++; + _resultIndexAndConsumedFlag = index + 1; } else { // happy path; close the reader cleanly - no // need for "Cancel" etc +#if NET5_0_OR_GREATER + await reader.DisposeAsync(); +#else reader.Dispose(); - reader = null; - callbacks?.OnCompleted(); - Dispose(); +#endif + reader = null!; + onCompleted?.Invoke(state); + + await DisposeAsync(); } } private Task> ReadAsyncImpl(Type type, bool buffered) { - if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); - if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); - var typedIdentity = identity.ForGrid(type, gridIndex); - CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache); - var deserializer = cache.Deserializer; - - int hash = GetColumnHash(reader); - if (deserializer.Func == null || deserializer.Hash != hash) + var deserializer = ValidateAndMarkConsumed(type, out var index); + if (buffered) { - deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); - cache.Deserializer = deserializer; - } - IsConsumed = true; - if (buffered && reader is DbDataReader) - { - return ReadBufferedAsync(gridIndex, deserializer.Func); + return ReadBufferedAsync(index, deserializer); } else { - var result = ReadDeferred(gridIndex, deserializer.Func, type); - if (buffered) result = result.ToList(); // for the "not a DbDataReader" scenario + var result = ReadDeferred(index, deserializer, type); return Task.FromResult(result); } } - private Task ReadRowAsyncImpl(Type type, Row row) + private Func ValidateAndMarkConsumed(Type type, out int index) { - if (reader is DbDataReader dbReader) return ReadRowAsyncImplViaDbReader(dbReader, type, row); + index = OnBeforeGrid(); + var typedIdentity = Identity.ForGrid(type, index); + CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache); + var deserializer = cache.Deserializer; - // no async API available; use non-async and fake it - return Task.FromResult(ReadRow(type, row)); + int hash = GetColumnHash(reader); + if (deserializer.Func is null || deserializer.Hash != hash) + { + deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); + cache.Deserializer = deserializer; + } + return deserializer.Func; } - private async Task ReadRowAsyncImplViaDbReader(DbDataReader reader, Type type, Row row) + private async Task ReadRowAsyncImpl(Type type, Row row) { - if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); - if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); + var index = OnBeforeGrid(); - IsConsumed = true; - T result = default(T); + T result = default!; if (await reader.ReadAsync(cancel).ConfigureAwait(false) && reader.FieldCount != 0) { - var typedIdentity = identity.ForGrid(type, gridIndex); + var typedIdentity = Identity.ForGrid(type, index); CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache); var deserializer = cache.Deserializer; int hash = GetColumnHash(reader); - if (deserializer.Func == null || deserializer.Hash != hash) + if (deserializer.Func is null || deserializer.Hash != hash) { deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); cache.Deserializer = deserializer; } - result = (T)deserializer.Func(reader); + + result = ConvertTo(deserializer.Func(reader)); + if ((row & Row.Single) != 0 && await reader.ReadAsync(cancel).ConfigureAwait(false)) ThrowMultipleRows(row); while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent rows */ } } @@ -218,29 +221,93 @@ private async Task ReadRowAsyncImplViaDbReader(DbDataReader reader, Type t { ThrowZeroRows(row); } - await NextResultAsync().ConfigureAwait(false); + await OnAfterGridAsync(index).ConfigureAwait(false); return result; } - private async Task> ReadBufferedAsync(int index, Func deserializer) + private async Task> ReadBufferedAsync(int index, Func deserializer) { try { - var reader = (DbDataReader)this.reader; var buffer = new List(); - while (index == gridIndex && await reader.ReadAsync(cancel).ConfigureAwait(false)) + while (index == ResultIndex && await reader!.ReadAsync(cancel).ConfigureAwait(false)) { - buffer.Add((T)deserializer(reader)); + buffer.Add(ConvertTo(deserializer(reader))); } return buffer; } finally // finally so that First etc progresses things even when multiple rows { - if (index == gridIndex) + await OnAfterGridAsync(index).ConfigureAwait(false); + } + } + + /// + /// Read the next grid of results. + /// + /// The type to read. + public IAsyncEnumerable ReadUnbufferedAsync() => ReadAsyncUnbufferedImpl(typeof(T)); + + /// + /// Read the next grid of results. + /// + public IAsyncEnumerable ReadUnbufferedAsync() => ReadAsyncUnbufferedImpl(typeof(DapperRow)); + + private IAsyncEnumerable ReadAsyncUnbufferedImpl(Type type) + { + var deserializer = ValidateAndMarkConsumed(type, out var index); + return ReadUnbufferedAsync(index, deserializer, cancel); + } + + private async IAsyncEnumerable ReadUnbufferedAsync(int index, Func deserializer, [EnumeratorCancellation] CancellationToken cancel) + { + try + { + while (index == ResultIndex && await reader!.ReadAsync(cancel).ConfigureAwait(false)) + { + yield return ConvertTo(deserializer(reader)); + } + } + finally // finally so that First etc progresses things even when multiple rows + { + await OnAfterGridAsync(index).ConfigureAwait(false); + } + } + + /// + /// Dispose the grid, closing and disposing both the underlying reader and command. + /// +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - for netfx version + public async ValueTask DisposeAsync() +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + if (reader is not null) + { + if (!reader.IsClosed) Command?.Cancel(); +#if NET5_0_OR_GREATER + await reader.DisposeAsync(); +#else + reader.Dispose(); +#endif + reader = null!; + } + if (Command is not null) + { + if (Command is DbCommand typed) + { +#if NET5_0_OR_GREATER + await typed.DisposeAsync(); +#else + typed.Dispose(); +#endif + } + else { - await NextResultAsync().ConfigureAwait(false); + Command.Dispose(); } + Command = null!; } + GC.SuppressFinalize(this); } } } diff --git a/Dapper/SqlMapper.GridReader.cs b/Dapper/SqlMapper.GridReader.cs index e4c697c1b..1370f7cc9 100644 --- a/Dapper/SqlMapper.GridReader.cs +++ b/Dapper/SqlMapper.GridReader.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data; -using System.Linq; +using System.Data.Common; using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; + namespace Dapper { public static partial class SqlMapper @@ -12,17 +17,44 @@ public static partial class SqlMapper /// public partial class GridReader : IDisposable { - private IDataReader reader; - private readonly Identity identity; + private DbDataReader reader; + private Identity? _identity; private readonly bool addToCache; + private readonly Action? onCompleted; + private readonly object? state; + private readonly CancellationToken cancel; - internal GridReader(IDbCommand command, IDataReader reader, Identity identity, IParameterCallbacks callbacks, bool addToCache) + /// + /// Creates a grid reader over an existing command and reader + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + protected GridReader(IDbCommand command, DbDataReader reader, Identity? identity, Action? onCompleted = null, object? state = null, bool addToCache = false, CancellationToken cancellationToken = default) { Command = command; this.reader = reader; - this.identity = identity; - this.callbacks = callbacks; + _identity = identity; + this.onCompleted = onCompleted; + this.state = state; this.addToCache = addToCache; + cancel = cancellationToken; + } + + internal GridReader(IDbCommand command, DbDataReader reader, Identity identity, IParameterCallbacks? callbacks, bool addToCache, + CancellationToken cancellationToken = default) + : this(command, reader, identity, callbacks is null ? null : static state => ((IParameterCallbacks)state!).OnCompleted(), + callbacks, addToCache, cancellationToken) + { } + + private Identity Identity => _identity ??= CreateIdentity(); + + private Identity CreateIdentity() + { + var cmd = Command; + if (cmd is not null && cmd.Connection is not null) + { + return new Identity(cmd.CommandText, cmd.CommandType, cmd.Connection, null, null); + } + throw new InvalidOperationException("This operation requires an identity or a connected command"); } /// @@ -42,7 +74,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, I /// Read an individual row of the next grid of results, returned as a dynamic object. /// /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public dynamic ReadFirstOrDefault() => ReadRow(typeof(DapperRow), Row.FirstOrDefault); + public dynamic? ReadFirstOrDefault() => ReadRow(typeof(DapperRow), Row.FirstOrDefault); /// /// Read an individual row of the next grid of results, returned as a dynamic object. @@ -54,7 +86,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, I /// Read an individual row of the next grid of results, returned as a dynamic object. /// /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public dynamic ReadSingleOrDefault() => ReadRow(typeof(DapperRow), Row.SingleOrDefault); + public dynamic? ReadSingleOrDefault() => ReadRow(typeof(DapperRow), Row.SingleOrDefault); /// /// Read the next grid of results. @@ -73,7 +105,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, I /// Read an individual row of the next grid of results. /// /// The type to read. - public T ReadFirstOrDefault() => ReadRow(typeof(T), Row.FirstOrDefault); + public T? ReadFirstOrDefault() => ReadRow(typeof(T), Row.FirstOrDefault); /// /// Read an individual row of the next grid of results. @@ -85,7 +117,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, I /// Read an individual row of the next grid of results. /// /// The type to read. - public T ReadSingleOrDefault() => ReadRow(typeof(T), Row.SingleOrDefault); + public T? ReadSingleOrDefault() => ReadRow(typeof(T), Row.SingleOrDefault); /// /// Read the next grid of results. @@ -95,7 +127,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity, I /// is null. public IEnumerable Read(Type type, bool buffered = true) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadImpl(type, buffered); } @@ -106,7 +138,7 @@ public IEnumerable Read(Type type, bool buffered = true) /// is null. public object ReadFirst(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadRow(type, Row.First); } @@ -115,9 +147,9 @@ public object ReadFirst(Type type) /// /// The type to read. /// is null. - public object ReadFirstOrDefault(Type type) + public object? ReadFirstOrDefault(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadRow(type, Row.FirstOrDefault); } @@ -128,7 +160,7 @@ public object ReadFirstOrDefault(Type type) /// is null. public object ReadSingle(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadRow(type, Row.Single); } @@ -137,60 +169,62 @@ public object ReadSingle(Type type) /// /// The type to read. /// is null. - public object ReadSingleOrDefault(Type type) + public object? ReadSingleOrDefault(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); return ReadRow(type, Row.SingleOrDefault); } - private IEnumerable ReadImpl(Type type, bool buffered) + + /// + /// Validates that data is available, returning the that corresponds to the current grid - and marks the current grid as consumed; + /// this call must be paired with a call to or + /// + protected int OnBeforeGrid() { - if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); + if (reader is null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); - var typedIdentity = identity.ForGrid(type, gridIndex); + _resultIndexAndConsumedFlag |= CONSUMED_FLAG; + return ResultIndex; + } + + private IEnumerable ReadImpl(Type type, bool buffered) + { + var index = OnBeforeGrid(); + var typedIdentity = Identity.ForGrid(type, index); CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache); var deserializer = cache.Deserializer; int hash = GetColumnHash(reader); - if (deserializer.Func == null || deserializer.Hash != hash) + if (deserializer.Func is null || deserializer.Hash != hash) { deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); cache.Deserializer = deserializer; } - IsConsumed = true; - var result = ReadDeferred(gridIndex, deserializer.Func, type); + var result = ReadDeferred(index, deserializer.Func, type); return buffered ? result.ToList() : result; } private T ReadRow(Type type, Row row) { - if (reader == null) throw new ObjectDisposedException(GetType().FullName, "The reader has been disposed; this can happen after all data has been consumed"); - if (IsConsumed) throw new InvalidOperationException("Query results must be consumed in the correct order, and each result can only be consumed once"); - IsConsumed = true; + var index = OnBeforeGrid(); - T result = default(T); + T result = default!; if (reader.Read() && reader.FieldCount != 0) { - var typedIdentity = identity.ForGrid(type, gridIndex); + var typedIdentity = Identity.ForGrid(type, index); CacheInfo cache = GetCacheInfo(typedIdentity, null, addToCache); var deserializer = cache.Deserializer; int hash = GetColumnHash(reader); - if (deserializer.Func == null || deserializer.Hash != hash) + if (deserializer.Func is null || deserializer.Hash != hash) { deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); cache.Deserializer = deserializer; } - object val = deserializer.Func(reader); - if (val == null || val is T) - { - result = (T)val; - } - else - { - var convertToType = Nullable.GetUnderlyingType(type) ?? type; - result = (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); - } + + result = ConvertTo(deserializer.Func(reader)); + if ((row & Row.Single) != 0 && reader.Read()) ThrowMultipleRows(row); while (reader.Read()) { /* ignore subsequent rows */ } } @@ -198,50 +232,42 @@ private T ReadRow(Type type, Row row) { ThrowZeroRows(row); } - NextResult(); + OnAfterGrid(index); return result; } private IEnumerable MultiReadInternal(Delegate func, string splitOn) { - var identity = this.identity.ForGrid(typeof(TReturn), new Type[] { - typeof(TFirst), - typeof(TSecond), - typeof(TThird), - typeof(TFourth), - typeof(TFifth), - typeof(TSixth), - typeof(TSeventh) - }, gridIndex); - - IsConsumed = true; + var index = OnBeforeGrid(); + var identity = Identity.ForGrid(typeof(TReturn), index); try { - foreach (var r in MultiMapImpl(null, default(CommandDefinition), func, splitOn, reader, identity, false)) + foreach (var r in MultiMapImpl(null, default, func, splitOn, reader, identity, false)) { yield return r; } } finally { - NextResult(); + OnAfterGrid(index); } } private IEnumerable MultiReadInternal(Type[] types, Func map, string splitOn) { - var identity = this.identity.ForGrid(typeof(TReturn), types, gridIndex); + var index = OnBeforeGrid(); + var identity = Identity.ForGrid(typeof(TReturn), types, index); try { - foreach (var r in MultiMapImpl(null, default(CommandDefinition), types, map, splitOn, reader, identity, false)) + foreach (var r in MultiMapImpl(null, default, types, map, splitOn, reader, identity, false)) { yield return r; } } finally { - NextResult(); + OnAfterGrid(index); } } @@ -364,61 +390,75 @@ public IEnumerable Read(Type[] types, Func return buffered ? result.ToList() : result; } - private IEnumerable ReadDeferred(int index, Func deserializer, Type effectiveType) + private IEnumerable ReadDeferred(int index, Func deserializer, Type effectiveType) { try { - var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; - while (index == gridIndex && reader.Read()) + while (index == ResultIndex && reader?.Read() == true) { - object val = deserializer(reader); - if (val == null || val is T) - { - yield return (T)val; - } - else - { - yield return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); - } + yield return ConvertTo(deserializer(reader)); } } finally // finally so that First etc progresses things even when multiple rows { - if (index == gridIndex) - { - NextResult(); - } + OnAfterGrid(index); } } - private int gridIndex, readCount; - private readonly IParameterCallbacks callbacks; + const int CONSUMED_FLAG = 1 << 31; + private int _resultIndexAndConsumedFlag; //, readCount; + + /// + /// Indicates the current result index + /// + protected int ResultIndex => _resultIndexAndConsumedFlag & ~CONSUMED_FLAG; /// /// Has the underlying reader been consumed? /// - public bool IsConsumed { get; private set; } + /// This also reports true if the current grid is actively being consumed + public bool IsConsumed => (_resultIndexAndConsumedFlag & CONSUMED_FLAG) != 0; /// /// The command associated with the reader /// public IDbCommand Command { get; set; } - private void NextResult() + /// + /// The underlying reader + /// + protected DbDataReader Reader => reader; + + /// + /// The cancellation token associated with this reader + /// + protected CancellationToken CancellationToken => cancel; + + /// + /// Marks the current grid as consumed, and moves to the next result + /// + protected void OnAfterGrid(int index) { - if (reader.NextResult()) + if (index != ResultIndex) { - readCount++; - gridIndex++; - IsConsumed = false; + // not our data! + } + else if (reader is null) + { + // nothing to do + } + else if (reader.NextResult()) + { + // readCount++; + _resultIndexAndConsumedFlag = index + 1; } else { // happy path; close the reader cleanly - no // need for "Cancel" etc reader.Dispose(); - reader = null; - callbacks?.OnCompleted(); + reader = null!; + onCompleted?.Invoke(state); Dispose(); } } @@ -428,18 +468,27 @@ private void NextResult() /// public void Dispose() { - if (reader != null) + if (reader is not null) { if (!reader.IsClosed) Command?.Cancel(); reader.Dispose(); - reader = null; + reader = null!; } - if (Command != null) + if (Command is not null) { Command.Dispose(); - Command = null; + Command = null!; } + GC.SuppressFinalize(this); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ConvertTo(object? value) => value switch + { + T typed => typed, + null or DBNull => default!, + _ => (T)Convert.ChangeType(value, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T), CultureInfo.InvariantCulture), + }; } } } diff --git a/Dapper/SqlMapper.IDataReader.cs b/Dapper/SqlMapper.IDataReader.cs index 196dc7d92..0dd0b8cd2 100644 --- a/Dapper/SqlMapper.IDataReader.cs +++ b/Dapper/SqlMapper.IDataReader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; namespace Dapper { @@ -13,13 +14,24 @@ public static partial class SqlMapper /// The data reader to parse results from. public static IEnumerable Parse(this IDataReader reader) { - if (reader.Read()) + var dbReader = GetDbDataReader(reader); + if (dbReader.Read()) { - var deser = GetDeserializer(typeof(T), reader, 0, -1, false); + var effectiveType = typeof(T); + var deser = GetDeserializer(effectiveType, dbReader, 0, -1, false); + var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; do { - yield return (T)deser(reader); - } while (reader.Read()); + object val = deser(dbReader); + if (val is null || val is T) + { + yield return (T)val!; + } + else + { + yield return (T)Convert.ChangeType(val, convertToType, System.Globalization.CultureInfo.InvariantCulture); + } + } while (dbReader.Read()); } } @@ -30,13 +42,14 @@ public static IEnumerable Parse(this IDataReader reader) /// The type to parse from the . public static IEnumerable Parse(this IDataReader reader, Type type) { - if (reader.Read()) + var dbReader = GetDbDataReader(reader); + if (dbReader.Read()) { - var deser = GetDeserializer(type, reader, 0, -1, false); + var deser = GetDeserializer(type, dbReader, 0, -1, false); do { - yield return deser(reader); - } while (reader.Read()); + yield return deser(dbReader); + } while (dbReader.Read()); } } @@ -46,13 +59,14 @@ public static IEnumerable Parse(this IDataReader reader, Type type) /// The data reader to parse results from. public static IEnumerable Parse(this IDataReader reader) { - if (reader.Read()) + var dbReader = GetDbDataReader(reader); + if (dbReader.Read()) { - var deser = GetDapperRowDeserializer(reader, 0, -1, false); + var deser = GetDapperRowDeserializer(dbReader, 0, -1, false); do { - yield return deser(reader); - } while (reader.Read()); + yield return deser(dbReader); + } while (dbReader.Read()); } } @@ -66,12 +80,49 @@ public static IEnumerable Parse(this IDataReader reader) /// The length of columns to read (default -1 = all fields following startIndex) /// Return null if we can't find the first column? (default false) /// A parser for this specific object from this row. +#if DEBUG // make sure we're not using this internally + [Obsolete(nameof(DbDataReader) + " API should be preferred")] +#endif + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Func GetRowParser(this IDataReader reader, Type type, int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) + { + return WrapObjectReader(GetDeserializer(type, GetDbDataReader(reader), startIndex, length, returnNullIfFirstMissing)); + } + + /// + /// Gets the row parser for a specific row on a data reader. This allows for type switching every row based on, for example, a TypeId column. + /// You could return a collection of the base type but have each more specific. + /// + /// The data reader to get the parser for the current row from + /// The type to get the parser for + /// The start column index of the object (default 0) + /// The length of columns to read (default -1 = all fields following startIndex) + /// Return null if we can't find the first column? (default false) + /// A parser for this specific object from this row. + public static Func GetRowParser(this DbDataReader reader, Type type, + int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) { return GetDeserializer(type, reader, startIndex, length, returnNullIfFirstMissing); } + /// +#if DEBUG // make sure we're not using this internally + [Obsolete(nameof(DbDataReader) + " API should be preferred")] +#endif + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static Func GetRowParser(this IDataReader reader, Type? concreteType = null, + int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) + { + concreteType ??= typeof(T); + var func = GetDeserializer(concreteType, GetDbDataReader(reader), startIndex, length, returnNullIfFirstMissing); + return Wrap(func); + + // this is just to be very clear about what we're capturing + static Func Wrap(Func func) + => reader => (T)func(GetDbDataReader(reader)); + } + /// /// Gets the row parser for a specific row on a data reader. This allows for type switching every row based on, for example, a TypeId column. /// You could return a collection of the base type but have each more specific. @@ -125,18 +176,18 @@ public static Func GetRowParser(this IDataReader reader, Ty /// public override int Type => 2; /// } /// - public static Func GetRowParser(this IDataReader reader, Type concreteType = null, + public static Func GetRowParser(this DbDataReader reader, Type? concreteType = null, int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) { - concreteType = concreteType ?? typeof(T); + concreteType ??= typeof(T); var func = GetDeserializer(concreteType, reader, startIndex, length, returnNullIfFirstMissing); - if (concreteType.IsValueType()) + if (concreteType.IsValueType) { return _ => (T)func(_); } else { - return (Func)(Delegate)func; + return (Func)(Delegate)func; } } } diff --git a/Dapper/SqlMapper.IMemberMap.cs b/Dapper/SqlMapper.IMemberMap.cs index fcd8eb379..25b39951d 100644 --- a/Dapper/SqlMapper.IMemberMap.cs +++ b/Dapper/SqlMapper.IMemberMap.cs @@ -23,17 +23,17 @@ public interface IMemberMap /// /// Target property /// - PropertyInfo Property { get; } + PropertyInfo? Property { get; } /// /// Target field /// - FieldInfo Field { get; } + FieldInfo? Field { get; } /// /// Target constructor parameter /// - ParameterInfo Parameter { get; } + ParameterInfo? Parameter { get; } } } } diff --git a/Dapper/SqlMapper.IParameterLookup.cs b/Dapper/SqlMapper.IParameterLookup.cs index aecf7bd1d..c097af1be 100644 --- a/Dapper/SqlMapper.IParameterLookup.cs +++ b/Dapper/SqlMapper.IParameterLookup.cs @@ -11,7 +11,7 @@ public interface IParameterLookup : IDynamicParameters /// Get the value of the specified parameter (return null if not found) /// /// The name of the parameter to get. - object this[string name] { get; } + object? this[string name] { get; } } } } diff --git a/Dapper/SqlMapper.ITypeHandler.cs b/Dapper/SqlMapper.ITypeHandler.cs index 31154a645..57a4d2153 100644 --- a/Dapper/SqlMapper.ITypeHandler.cs +++ b/Dapper/SqlMapper.ITypeHandler.cs @@ -23,7 +23,7 @@ public interface ITypeHandler /// The value from the database /// The type to parse to /// The typed value - object Parse(Type destinationType, object value); + object? Parse(Type destinationType, object value); } } } diff --git a/Dapper/SqlMapper.ITypeMap.cs b/Dapper/SqlMapper.ITypeMap.cs index dbd04ba61..42189a688 100644 --- a/Dapper/SqlMapper.ITypeMap.cs +++ b/Dapper/SqlMapper.ITypeMap.cs @@ -16,7 +16,7 @@ public interface ITypeMap /// DataReader column names /// DataReader column types /// Matching constructor or default one - ConstructorInfo FindConstructor(string[] names, Type[] types); + ConstructorInfo? FindConstructor(string[] names, Type[] types); /// /// Returns a constructor which should *always* be used. @@ -25,7 +25,7 @@ public interface ITypeMap /// /// Use this class to force object creation away from parameterless constructors you don't control. /// - ConstructorInfo FindExplicitConstructor(); + ConstructorInfo? FindExplicitConstructor(); /// /// Gets mapping for constructor parameter @@ -33,14 +33,14 @@ public interface ITypeMap /// Constructor to resolve /// DataReader column name /// Mapping implementation - IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName); + IMemberMap? GetConstructorParameter(ConstructorInfo constructor, string columnName); /// /// Gets member mapping for column /// /// DataReader column name /// Mapping implementation - IMemberMap GetMember(string columnName); + IMemberMap? GetMember(string columnName); } } } diff --git a/Dapper/SqlMapper.Identity.cs b/Dapper/SqlMapper.Identity.cs index a02f20e62..4871d9c05 100644 --- a/Dapper/SqlMapper.Identity.cs +++ b/Dapper/SqlMapper.Identity.cs @@ -1,20 +1,109 @@ using System; +using System.ComponentModel; using System.Data; +using System.Runtime.CompilerServices; namespace Dapper { public static partial class SqlMapper { + internal sealed class Identity : Identity + { + private static readonly int s_typeHash; + private static readonly int s_typeCount = CountNonTrivial(out s_typeHash); + + internal Identity(string sql, CommandType? commandType, string connectionString, Type type, Type? parametersType, int gridIndex = 0) + : base(sql, commandType, connectionString, type, parametersType, s_typeHash, gridIndex) + {} + internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type type, Type? parametersType, int gridIndex = 0) + : base(sql, commandType, connection.ConnectionString, type, parametersType, s_typeHash, gridIndex) + { } + + static int CountNonTrivial(out int hashCode) + { + int hashCodeLocal = 0; + int count = 0; + bool Map() + { + if(typeof(T) != typeof(DontMap)) + { + count++; + hashCodeLocal = (hashCodeLocal * 23) + (typeof(T).GetHashCode()); + return true; + } + return false; + } + _ = Map() && Map() && Map() + && Map() && Map() && Map() + && Map(); + hashCode = hashCodeLocal; + return count; + } + internal override int TypeCount => s_typeCount; + internal override Type GetType(int index) => index switch { + 0 => typeof(TFirst), + 1 => typeof(TSecond), + 2 => typeof(TThird), + 3 => typeof(TFourth), + 4 => typeof(TFifth), + 5 => typeof(TSixth), + 6 => typeof(TSeventh), + _ => base.GetType(index), + }; + } + internal sealed class IdentityWithTypes : Identity + { + private readonly Type[] _types; + + internal IdentityWithTypes(string sql, CommandType? commandType, string connectionString, Type type, Type? parametersType, Type[] otherTypes, int gridIndex = 0) + : base(sql, commandType, connectionString, type, parametersType, HashTypes(otherTypes), gridIndex) + { + _types = otherTypes ?? Type.EmptyTypes; + } + internal IdentityWithTypes(string sql, CommandType? commandType, IDbConnection connection, Type type, Type? parametersType, Type[] otherTypes, int gridIndex = 0) + : base(sql, commandType, connection.ConnectionString, type, parametersType, HashTypes(otherTypes), gridIndex) + { + _types = otherTypes ?? Type.EmptyTypes; + } + + internal override int TypeCount => _types.Length; + + internal override Type GetType(int index) => _types[index]; + + static int HashTypes(Type[] types) + { + var hashCode = 0; + if (types is not null) + { + foreach (var t in types) + { + hashCode = (hashCode * 23) + (t?.GetHashCode() ?? 0); + } + } + return hashCode; + } + } + /// /// Identity of a cached query in Dapper, used for extensibility. /// public class Identity : IEquatable { + internal virtual int TypeCount => 0; + + internal virtual Type GetType(int index) => throw new IndexOutOfRangeException(nameof(index)); + +#pragma warning disable CS0618 // Type or member is obsolete + internal Identity ForGrid(Type primaryType, int gridIndex) => + new Identity(sql, commandType, connectionString, primaryType, parametersType, gridIndex); + internal Identity ForGrid(Type primaryType, int gridIndex) => - new Identity(sql, commandType, connectionString, primaryType, parametersType, null, gridIndex); + new Identity(sql, commandType, connectionString, primaryType, parametersType, 0, gridIndex); internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) => - new Identity(sql, commandType, connectionString, primaryType, parametersType, otherTypes, gridIndex); + (otherTypes is null || otherTypes.Length == 0) + ? new Identity(sql, commandType, connectionString, primaryType, parametersType, 0, gridIndex) + : new IdentityWithTypes(sql, commandType, connectionString, primaryType, parametersType, otherTypes, gridIndex); /// /// Create an identity for use with DynamicParameters, internal use only. @@ -22,13 +111,15 @@ internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) => /// The parameters type to create an for. /// public Identity ForDynamicParameters(Type type) => - new Identity(sql, commandType, connectionString, this.type, type, null, -1); + new Identity(sql, commandType, connectionString, this.type, type, 0, -1); +#pragma warning restore CS0618 // Type or member is obsolete - internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type type, Type parametersType, Type[] otherTypes) - : this(sql, commandType, connection.ConnectionString, type, parametersType, otherTypes, 0) { /* base call */ } + internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type? type, Type? parametersType) + : this(sql, commandType, connection.ConnectionString, type, parametersType, 0, 0) { /* base call */ } - private Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType, Type[] otherTypes, int gridIndex) + private protected Identity(string sql, CommandType? commandType, string connectionString, Type? type, Type? parametersType, int otherTypesHash, int gridIndex) { +#pragma warning disable CS0618 // Type or member is obsolete this.sql = sql; this.commandType = commandType; this.connectionString = connectionString; @@ -42,79 +133,151 @@ private Identity(string sql, CommandType? commandType, string connectionString, hashCode = (hashCode * 23) + gridIndex.GetHashCode(); hashCode = (hashCode * 23) + (sql?.GetHashCode() ?? 0); hashCode = (hashCode * 23) + (type?.GetHashCode() ?? 0); - if (otherTypes != null) - { - foreach (var t in otherTypes) - { - hashCode = (hashCode * 23) + (t?.GetHashCode() ?? 0); - } - } - hashCode = (hashCode * 23) + (connectionString == null ? 0 : connectionStringComparer.GetHashCode(connectionString)); + hashCode = (hashCode * 23) + otherTypesHash; + hashCode = (hashCode * 23) + (connectionString is null ? 0 : connectionStringComparer.GetHashCode(connectionString)); hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0); } +#pragma warning restore CS0618 // Type or member is obsolete } /// /// Whether this equals another. /// /// The other to compare to. - public override bool Equals(object obj) => Equals(obj as Identity); + public override bool Equals(object? obj) => Equals(obj as Identity); /// /// The raw SQL command. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(Sql) + ". This API may be removed at a later date.")] public readonly string sql; + /// + /// The raw SQL command. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public string Sql => sql; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The SQL command type. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(CommandType) + ". This API may be removed at a later date.")] public readonly CommandType? commandType; + /// + /// The SQL command type. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public CommandType? CommandType => commandType; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The hash code of this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(GetHashCode) + ". This API may be removed at a later date.")] public readonly int hashCode; /// /// The grid index (position in the reader) of this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(GridIndex) + ". This API may be removed at a later date.")] public readonly int gridIndex; /// - /// This of this Identity. + /// The grid index (position in the reader) of this Identity. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public int GridIndex => gridIndex; +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// The of this Identity. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(Type) + ". This API may be removed at a later date.")] + public readonly Type? type; + + /// + /// The of this Identity. /// - public readonly Type type; +#pragma warning disable CS0618 // Type or member is obsolete + public Type? Type => type; +#pragma warning restore CS0618 // Type or member is obsolete /// /// The connection string for this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API may be removed at a later date.")] public readonly string connectionString; /// /// The type of the parameters object for this Identity. /// - public readonly Type parametersType; + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(ParametersType) + ". This API may be removed at a later date.")] + public readonly Type? parametersType; + + /// + /// The type of the parameters object for this Identity. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public Type? ParametersType => parametersType; +#pragma warning restore CS0618 // Type or member is obsolete /// /// Gets the hash code for this identity. /// /// +#pragma warning disable CS0618 // Type or member is obsolete public override int GetHashCode() => hashCode; +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// See object.ToString() + /// +#pragma warning disable CS0618 // Type or member is obsolete + public override string ToString() => sql; +#pragma warning restore CS0618 // Type or member is obsolete /// /// Compare 2 Identity objects /// /// The other object to compare. /// Whether the two are equal - public bool Equals(Identity other) + public bool Equals(Identity? other) { - return other != null - && gridIndex == other.gridIndex + if (ReferenceEquals(this, other)) return true; + if (other is null) return false; + + int typeCount; +#pragma warning disable CS0618 // Type or member is obsolete + return gridIndex == other.gridIndex && type == other.type && sql == other.sql && commandType == other.commandType && connectionStringComparer.Equals(connectionString, other.connectionString) - && parametersType == other.parametersType; + && parametersType == other.parametersType + && (typeCount = TypeCount) == other.TypeCount + && (typeCount == 0 || TypesEqual(this, other, typeCount)); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool TypesEqual(Identity x, Identity y, int count) + { + if (y.TypeCount != count) return false; + for(int i = 0; i < count; i++) + { + if (x.GetType(i) != y.GetType(i)) + return false; + } + return true; } } } diff --git a/Dapper/SqlMapper.Link.cs b/Dapper/SqlMapper.Link.cs index 602336fe7..710714412 100644 --- a/Dapper/SqlMapper.Link.cs +++ b/Dapper/SqlMapper.Link.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Diagnostics.CodeAnalysis; +using System.Threading; namespace Dapper { @@ -11,30 +12,31 @@ public static partial class SqlMapper /// /// The type to cache. /// The value type of the cache. - internal class Link where TKey : class + internal sealed class Link where TKey : class { - public static bool TryGet(Link link, TKey key, out TValue value) + public static void Clear(ref Link? head) => Interlocked.Exchange(ref head, null); + public static bool TryGet(Link? link, TKey key, [NotNullWhen(true)] out TValue? value) { - while (link != null) + while (link is not null) { if ((object)key == (object)link.Key) { - value = link.Value; + value = link.Value!; return true; } link = link.Tail; } - value = default(TValue); + value = default; return false; } - public static bool TryAdd(ref Link head, TKey key, ref TValue value) + public static bool TryAdd(ref Link? head, TKey key, ref TValue value) { bool tryAgain; do { var snapshot = Interlocked.CompareExchange(ref head, null, null); - if (TryGet(snapshot, key, out TValue found)) + if (TryGet(snapshot, key, out TValue? found)) { // existing match; report the existing value instead value = found; return false; @@ -46,7 +48,7 @@ public static bool TryAdd(ref Link head, TKey key, ref TValue valu return true; } - private Link(TKey key, TValue value, Link tail) + private Link(TKey key, TValue value, Link? tail) { Key = key; Value = value; @@ -55,7 +57,7 @@ private Link(TKey key, TValue value, Link tail) public TKey Key { get; } public TValue Value { get; } - public Link Tail { get; } + public Link? Tail { get; } } } } diff --git a/Dapper/SqlMapper.LiteralToken.cs b/Dapper/SqlMapper.LiteralToken.cs index d5ae56583..b5fdff0ec 100644 --- a/Dapper/SqlMapper.LiteralToken.cs +++ b/Dapper/SqlMapper.LiteralToken.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Dapper { @@ -7,7 +8,7 @@ public static partial class SqlMapper /// /// Represents a placeholder for a value that should be replaced as a literal value in the resulting sql /// - internal struct LiteralToken + internal readonly struct LiteralToken { /// /// The text in the original command that should be replaced @@ -25,7 +26,7 @@ internal LiteralToken(string token, string member) Member = member; } - internal static readonly IList None = new LiteralToken[0]; + internal static IList None => Array.Empty(); } } } diff --git a/Dapper/SqlMapper.Settings.cs b/Dapper/SqlMapper.Settings.cs index db517c7ad..c19835c11 100644 --- a/Dapper/SqlMapper.Settings.cs +++ b/Dapper/SqlMapper.Settings.cs @@ -1,5 +1,6 @@ using System; using System.Data; +using System.Threading; namespace Dapper { @@ -10,9 +11,10 @@ public static partial class SqlMapper /// public static class Settings { - // disable single result by default; prevents errors AFTER the select being detected properly - private const CommandBehavior DefaultAllowedCommandBehaviors = ~CommandBehavior.SingleResult; + // disable single row/result by default; prevents errors AFTER the select being detected properly + private const CommandBehavior DefaultAllowedCommandBehaviors = ~(CommandBehavior.SingleResult | CommandBehavior.SingleRow); internal static CommandBehavior AllowedCommandBehaviors { get; private set; } = DefaultAllowedCommandBehaviors; + private static void SetAllowedCommandBehaviors(CommandBehavior behavior, bool enabled) { if (enabled) AllowedCommandBehaviors |= behavior; @@ -44,7 +46,7 @@ internal static bool DisableCommandBehaviorOptimizations(CommandBehavior behavio { if (ex.Message.Contains(nameof(CommandBehavior.SingleResult)) || ex.Message.Contains(nameof(CommandBehavior.SingleRow))) - { // some providers just just allow these, so: try again without them and stop issuing them + { // some providers just allow these, so: try again without them and stop issuing them SetAllowedCommandBehaviors(CommandBehavior.SingleResult | CommandBehavior.SingleRow, false); return true; } @@ -63,7 +65,9 @@ static Settings() public static void SetDefaults() { CommandTimeout = null; - ApplyNullValues = false; + ApplyNullValues = PadListExpansions = UseIncrementalPseudoPositionalParameterNames = PreferTypeHandlersForEnums = false; + AllowedCommandBehaviors = DefaultAllowedCommandBehaviors; + FetchSize = InListStringSplitCount = -1; } /// @@ -90,9 +94,49 @@ public static void SetDefaults() public static bool PadListExpansions { get; set; } /// /// If set (non-negative), when performing in-list expansions of integer types ("where id in @ids", etc), switch to a string_split based - /// operation if there are more than this many elements. Note that this feautre requires SQL Server 2016 / compatibility level 130 (or above). + /// operation if there are this many elements or more. Note that this feature requires SQL Server 2016 / compatibility level 130 (or above). /// public static int InListStringSplitCount { get; set; } = -1; + + /// + /// If set, pseudo-positional parameters (i.e. ?foo?) are passed using auto-generated incremental names, i.e. "1", "2", "3" + /// instead of the original name; for most scenarios, this is ignored since the name is redundant, but "snowflake" requires this. + /// + public static bool UseIncrementalPseudoPositionalParameterNames { get; set; } + + /// + /// If assigned a non-negative value, then that value is applied to any commands FetchSize property, if it exists; + /// see https://docs.oracle.com/en/database/oracle/oracle-database/18/odpnt/CommandFetchSize.html; note that this value + /// can only be set globally - it is not intended for frequent/contextual changing. + /// + public static long FetchSize + { + get => Volatile.Read(ref s_FetchSize); + set + { + if (Volatile.Read(ref s_FetchSize) != value) + { + Volatile.Write(ref s_FetchSize, value); + CommandDefinition.ResetCommandInitCache(); // if this setting is useful: we've invalidated things + } + } + } + + /// + /// Indicates whether single-character parameter tokens (? etc) will be detected and used where possible; + /// this feature is not recommended and will be disabled by default in future versions; + /// where possible, prefer named parameters (@yourParam etc) or Dapper's "pseudo-positional" parameters (?yourParam? etc). + /// + public static bool SupportLegacyParameterTokens { get; set; } = true; + + /// + /// When true, Dapper checks for a registered TypeHandler for enum types before + /// falling back to the default behavior of sending enums as their underlying integer type. + /// This enables custom enum serialization (e.g. storing enums as strings), while preserving existing behavior. + /// + public static bool PreferTypeHandlersForEnums { get; set; } + + private static long s_FetchSize = -1; } } } diff --git a/Dapper/SqlMapper.TypeDeserializerCache.cs b/Dapper/SqlMapper.TypeDeserializerCache.cs index ef53b8b7a..3a34cd292 100644 --- a/Dapper/SqlMapper.TypeDeserializerCache.cs +++ b/Dapper/SqlMapper.TypeDeserializerCache.cs @@ -1,21 +1,21 @@ using System; -using System.Data; using System.Collections; using System.Collections.Generic; +using System.Data.Common; using System.Text; namespace Dapper { public static partial class SqlMapper { - private class TypeDeserializerCache + private sealed class TypeDeserializerCache { private TypeDeserializerCache(Type type) { this.type = type; } - private static readonly Hashtable byType = new Hashtable(); + private static readonly Hashtable byType = new(); private readonly Type type; internal static void Purge(Type type) { @@ -33,15 +33,15 @@ internal static void Purge() } } - internal static Func GetReader(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + internal static Func GetReader(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) { - var found = (TypeDeserializerCache)byType[type]; - if (found == null) + var found = (TypeDeserializerCache?)byType[type]; + if (found is null) { lock (byType) { - found = (TypeDeserializerCache)byType[type]; - if (found == null) + found = (TypeDeserializerCache?)byType[type]; + if (found is null) { byType[type] = found = new TypeDeserializerCache(type); } @@ -50,18 +50,18 @@ internal static Func GetReader(Type type, IDataReader reade return found.GetReader(reader, startBound, length, returnNullIfFirstMissing); } - private readonly Dictionary> readers = new Dictionary>(); + private readonly Dictionary> readers = new(); - private struct DeserializerKey : IEquatable + private readonly struct DeserializerKey : IEquatable { private readonly int startBound, length; private readonly bool returnNullIfFirstMissing; - private readonly IDataReader reader; - private readonly string[] names; - private readonly Type[] types; + private readonly DbDataReader? reader; + private readonly string[]? names; + private readonly Type[]? types; private readonly int hashCode; - public DeserializerKey(int hashCode, int startBound, int length, bool returnNullIfFirstMissing, IDataReader reader, bool copyDown) + public DeserializerKey(int hashCode, int startBound, int length, bool returnNullIfFirstMissing, DbDataReader reader, bool copyDown) { this.hashCode = hashCode; this.startBound = startBound; @@ -92,11 +92,11 @@ public DeserializerKey(int hashCode, int startBound, int length, bool returnNull public override string ToString() { // only used in the debugger - if (names != null) + if (names is not null) { return string.Join(", ", names); } - if (reader != null) + if (reader is not null) { var sb = new StringBuilder(); int index = startBound; @@ -107,13 +107,11 @@ public override string ToString() } return sb.ToString(); } - return base.ToString(); + return base.ToString() ?? ""; } - public override bool Equals(object obj) - { - return obj is DeserializerKey && Equals((DeserializerKey)obj); - } + public override bool Equals(object? obj) + => obj is DeserializerKey key && Equals(key); public bool Equals(DeserializerKey other) { @@ -138,17 +136,17 @@ public bool Equals(DeserializerKey other) } } - private Func GetReader(IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + private Func GetReader(DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) { if (length < 0) length = reader.FieldCount - startBound; int hash = GetColumnHash(reader, startBound, length); if (returnNullIfFirstMissing) hash *= -27; // get a cheap key first: false means don't copy the values down var key = new DeserializerKey(hash, startBound, length, returnNullIfFirstMissing, reader, false); - Func deser; + Func? deser; lock (readers) { - if (readers.TryGetValue(key, out deser)) return deser; + if (readers.TryGetValue(key, out deser)) return deser!; } deser = GetTypeDeserializerImpl(type, reader, startBound, length, returnNullIfFirstMissing); // get a more expensive key: true means copy the values down so it can be used as a key later diff --git a/Dapper/SqlMapper.TypeHandler.cs b/Dapper/SqlMapper.TypeHandler.cs index 7a712ee49..c36a60b4b 100644 --- a/Dapper/SqlMapper.TypeHandler.cs +++ b/Dapper/SqlMapper.TypeHandler.cs @@ -16,14 +16,14 @@ public abstract class TypeHandler : ITypeHandler /// /// The parameter to configure /// Parameter value - public abstract void SetValue(IDbDataParameter parameter, T value); + public abstract void SetValue(IDbDataParameter parameter, T? value); /// /// Parse a database value back to a typed value /// /// The value from the database /// The typed value - public abstract T Parse(object value); + public abstract T? Parse(object value); void ITypeHandler.SetValue(IDbDataParameter parameter, object value) { @@ -33,11 +33,11 @@ void ITypeHandler.SetValue(IDbDataParameter parameter, object value) } else { - SetValue(parameter, (T)value); + SetValue(parameter, (T?)value); } } - object ITypeHandler.Parse(Type destinationType, object value) + object? ITypeHandler.Parse(Type destinationType, object value) { return Parse(value); } @@ -56,7 +56,7 @@ public abstract class StringTypeHandler : TypeHandler protected abstract T Parse(string xml); /// - /// Format an instace into a string (the instance will never be null) + /// Format an instance into a string (the instance will never be null) /// /// The string to format. protected abstract string Format(T xml); @@ -66,9 +66,9 @@ public abstract class StringTypeHandler : TypeHandler /// /// The parameter to configure /// Parameter value - public override void SetValue(IDbDataParameter parameter, T value) + public override void SetValue(IDbDataParameter parameter, T? value) { - parameter.Value = value == null ? (object)DBNull.Value : Format(value); + parameter.Value = value is null ? (object)DBNull.Value : Format(value); } /// @@ -78,7 +78,7 @@ public override void SetValue(IDbDataParameter parameter, T value) /// The typed value public override T Parse(object value) { - if (value == null || value is DBNull) return default(T); + if (value is null || value is DBNull) return default!; return Parse((string)value); } } diff --git a/Dapper/SqlMapper.TypeHandlerCache.cs b/Dapper/SqlMapper.TypeHandlerCache.cs index 04d9baba8..b3d13764a 100644 --- a/Dapper/SqlMapper.TypeHandlerCache.cs +++ b/Dapper/SqlMapper.TypeHandlerCache.cs @@ -11,9 +11,7 @@ public static partial class SqlMapper /// /// The type to have a cache for. [Obsolete(ObsoleteInternalUsageOnly, false)] -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] public static class TypeHandlerCache { @@ -22,7 +20,7 @@ public static class TypeHandlerCache /// /// The object to parse. [Obsolete(ObsoleteInternalUsageOnly, true)] - public static T Parse(object value) => (T)handler.Parse(typeof(T), value); + public static T? Parse(object value) => (T?)handler.Parse(typeof(T), value); /// /// Not intended for direct usage. @@ -34,12 +32,10 @@ public static class TypeHandlerCache internal static void SetHandler(ITypeHandler handler) { -#pragma warning disable 618 TypeHandlerCache.handler = handler; -#pragma warning restore 618 } - private static ITypeHandler handler; + private static ITypeHandler handler = null!; } } } diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index 96eb11805..2fa0e72b7 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -1,6 +1,6 @@ /* License: http://www.apache.org/licenses/LICENSE-2.0 - Home page: https://github.com/StackExchange/dapper-dot-net + Home page: https://github.com/DapperLib/Dapper-dot-net */ using System; @@ -8,6 +8,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; +using System.Data.Common; +using System.Data.SqlTypes; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; @@ -19,10 +22,6 @@ using System.Xml; using System.Xml.Linq; -#if NETSTANDARD1_3 -using DataException = System.InvalidOperationException; -#endif - namespace Dapper { /// @@ -30,11 +29,11 @@ namespace Dapper /// public static partial class SqlMapper { - private class PropertyInfoByNameComparer : IComparer + private sealed class PropertyInfoByNameComparer : IComparer { - public int Compare(PropertyInfo x, PropertyInfo y) => string.CompareOrdinal(x.Name, y.Name); + public int Compare(PropertyInfo? x, PropertyInfo? y) => string.CompareOrdinal(x?.Name, y?.Name); } - private static int GetColumnHash(IDataReader reader, int startBound = 0, int length = -1) + private static int GetColumnHash(DbDataReader reader, int startBound = 0, int length = -1) { unchecked { @@ -52,14 +51,14 @@ private static int GetColumnHash(IDataReader reader, int startBound = 0, int len /// /// Called if the query cache is purged via PurgeQueryCache /// - public static event EventHandler QueryCachePurged; + public static event EventHandler? QueryCachePurged; private static void OnQueryCachePurged() { var handler = QueryCachePurged; handler?.Invoke(null, EventArgs.Empty); } - private static readonly System.Collections.Concurrent.ConcurrentDictionary _queryCache = new System.Collections.Concurrent.ConcurrentDictionary(); + private static readonly System.Collections.Concurrent.ConcurrentDictionary _queryCache = new(); private static void SetQueryCache(Identity key, CacheInfo value) { if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS) @@ -77,7 +76,7 @@ private static void CollectCacheGarbage() { if (pair.Value.GetHitCount() <= COLLECT_HIT_COUNT_MIN) { - _queryCache.TryRemove(pair.Key, out CacheInfo cache); + _queryCache.TryRemove(pair.Key, out var _); } } } @@ -90,9 +89,10 @@ private static void CollectCacheGarbage() private const int COLLECT_PER_ITEMS = 1000, COLLECT_HIT_COUNT_MIN = 0; private static int collect; - private static bool TryGetQueryCache(Identity key, out CacheInfo value) + + private static bool TryGetQueryCache(Identity key, [NotNullWhen(true)] out CacheInfo? value) { - if (_queryCache.TryGetValue(key, out value)) + if (_queryCache.TryGetValue(key, out value!)) { value.RecordHit(); return true; @@ -115,8 +115,8 @@ private static void PurgeQueryCacheByType(Type type) { foreach (var entry in _queryCache) { - if (entry.Key.type == type) - _queryCache.TryRemove(entry.Key, out CacheInfo cache); + if (entry.Key.Type == type) + _queryCache.TryRemove(entry.Key, out _); } TypeDeserializerCache.Purge(type); } @@ -137,7 +137,9 @@ public static int GetCachedSQLCount() /// public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue) { - var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount())); +#pragma warning disable CS0618 // Type or member is obsolete + var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.Sql, pair.Value.GetHitCount())); +#pragma warning restore CS0618 // Type or member is obsolete return (ignoreHitCountAbove < int.MaxValue) ? data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove) : data; @@ -146,19 +148,19 @@ public static IEnumerable> GetCachedSQL(int ignoreHit /// /// Deep diagnostics only: find any hash collisions in the cache /// - /// - public static IEnumerable> GetHashCollissions() + public static IEnumerable> GetHashCollissions() // legacy incorrect spelling, oops { var counts = new Dictionary(); foreach (var key in _queryCache.Keys) { - if (!counts.TryGetValue(key.hashCode, out int count)) + var hash = key.GetHashCode(); + if (!counts.TryGetValue(hash, out int count)) { - counts.Add(key.hashCode, 1); + counts.Add(hash, 1); } else { - counts[key.hashCode] = count + 1; + counts[hash] = count + 1; } } return from pair in counts @@ -166,11 +168,44 @@ where pair.Value > 1 select Tuple.Create(pair.Key, pair.Value); } - private static Dictionary typeMap; + private static Dictionary typeMap; + + [Flags] + internal enum TypeMapEntryFlags + { + None = 0, + SetType = 1 << 0, + UseGetFieldValue = 1 << 1, + } + internal readonly struct TypeMapEntry : IEquatable + { + public readonly DbType DbType { get; } + public readonly TypeMapEntryFlags Flags; + public TypeMapEntry(DbType dbType, TypeMapEntryFlags flags) + { + DbType = dbType; + Flags = flags; + } + public override int GetHashCode() => (int)DbType ^ (int)Flags; + public override string ToString() => $"{DbType}, {Flags}"; + public override bool Equals(object? obj) => obj is TypeMapEntry other && Equals(other); + public bool Equals(TypeMapEntry other) => other.DbType == DbType && other.Flags == Flags; + public static readonly TypeMapEntry + DoNotSet = new((DbType)(-2), TypeMapEntryFlags.None), + DoNotSetFieldValue = new((DbType)(-2), TypeMapEntryFlags.UseGetFieldValue), + DecimalFieldValue = new(DbType.Decimal, TypeMapEntryFlags.SetType | TypeMapEntryFlags.UseGetFieldValue); + + public static implicit operator TypeMapEntry(DbType dbType) + => new(dbType, TypeMapEntryFlags.SetType); + } static SqlMapper() { - typeMap = new Dictionary + typeMap = new Dictionary(41 +#if NET6_0_OR_GREATER && DATEONLY + + 4 // {Date|Time}Only[?] +#endif + ) { [typeof(byte)] = DbType.Byte, [typeof(sbyte)] = DbType.SByte, @@ -187,9 +222,9 @@ static SqlMapper() [typeof(string)] = DbType.String, [typeof(char)] = DbType.StringFixedLength, [typeof(Guid)] = DbType.Guid, - [typeof(DateTime)] = DbType.DateTime, + [typeof(DateTime)] = TypeMapEntry.DoNotSet, [typeof(DateTimeOffset)] = DbType.DateTimeOffset, - [typeof(TimeSpan)] = DbType.Time, + [typeof(TimeSpan)] = TypeMapEntry.DoNotSet, [typeof(byte[])] = DbType.Binary, [typeof(byte?)] = DbType.Byte, [typeof(sbyte?)] = DbType.SByte, @@ -205,10 +240,20 @@ static SqlMapper() [typeof(bool?)] = DbType.Boolean, [typeof(char?)] = DbType.StringFixedLength, [typeof(Guid?)] = DbType.Guid, - [typeof(DateTime?)] = DbType.DateTime, + [typeof(DateTime?)] = TypeMapEntry.DoNotSet, [typeof(DateTimeOffset?)] = DbType.DateTimeOffset, - [typeof(TimeSpan?)] = DbType.Time, - [typeof(object)] = DbType.Object + [typeof(TimeSpan?)] = TypeMapEntry.DoNotSet, + [typeof(object)] = DbType.Object, + [typeof(SqlDecimal)] = TypeMapEntry.DecimalFieldValue, + [typeof(SqlDecimal?)] = TypeMapEntry.DecimalFieldValue, + [typeof(SqlMoney)] = TypeMapEntry.DecimalFieldValue, + [typeof(SqlMoney?)] = TypeMapEntry.DecimalFieldValue, +#if NET6_0_OR_GREATER && DATEONLY + [typeof(DateOnly)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(TimeOnly)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(DateOnly?)] = TypeMapEntry.DoNotSetFieldValue, + [typeof(TimeOnly?)] = TypeMapEntry.DoNotSetFieldValue, +#endif }; ResetTypeHandlers(false); } @@ -218,26 +263,17 @@ static SqlMapper() /// public static void ResetTypeHandlers() => ResetTypeHandlers(true); + [MemberNotNull(nameof(typeHandlers))] private static void ResetTypeHandlers(bool clone) { - typeHandlers = new Dictionary(); -#if !NETSTANDARD1_3 - AddTypeHandlerImpl(typeof(DataTable), new DataTableHandler(), clone); -#endif - try + lock (typeHandlersSyncLock) { - AddSqlDataRecordsTypeHandler(clone); + typeHandlers = []; + AddTypeHandlerCore(typeof(DataTable), new DataTableHandler(), clone); + AddTypeHandlerCore(typeof(XmlDocument), new XmlDocumentHandler(), clone); + AddTypeHandlerCore(typeof(XDocument), new XDocumentHandler(), clone); + AddTypeHandlerCore(typeof(XElement), new XElementHandler(), clone); } - catch { /* https://github.com/StackExchange/dapper-dot-net/issues/424 */ } - AddTypeHandlerImpl(typeof(XmlDocument), new XmlDocumentHandler(), clone); - AddTypeHandlerImpl(typeof(XDocument), new XDocumentHandler(), clone); - AddTypeHandlerImpl(typeof(XElement), new XElementHandler(), clone); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void AddSqlDataRecordsTypeHandler(bool clone) - { - AddTypeHandlerImpl(typeof(IEnumerable), new SqlDataRecordHandler(), clone); } /// @@ -246,13 +282,42 @@ private static void AddSqlDataRecordsTypeHandler(bool clone) /// The type to map from. /// The database type to map to. public static void AddTypeMap(Type type, DbType dbType) + => AddTypeMap(type, dbType, false); + + /// + /// Configure the specified type to be mapped to a given db-type. + /// + /// The type to map from. + /// The database type to map to. + /// Whether to prefer over . + public static void AddTypeMap(Type type, DbType dbType, bool useGetFieldValue) { // use clone, mutate, replace to avoid threading issues var snapshot = typeMap; + var flags = TypeMapEntryFlags.None; + if (dbType >= 0) + { + flags |= TypeMapEntryFlags.SetType; + } + if (useGetFieldValue) + { + flags |= TypeMapEntryFlags.UseGetFieldValue; + } + var value = new TypeMapEntry(dbType, flags); + if (snapshot.TryGetValue(type, out var oldValue) && oldValue.Equals(value)) return; // nothing to do - if (snapshot.TryGetValue(type, out DbType oldValue) && oldValue == dbType) return; // nothing to do + SetTypeMap(new Dictionary(snapshot) { [type] = value }); + } + + private static void SetTypeMap(Dictionary value) + { + typeMap = value; - typeMap = new Dictionary(snapshot) { [type] = dbType }; + // this cache is predicated on the contents of the type-map; reset it + lock (s_ReadViaGetFieldValueCache) + { + s_ReadViaGetFieldValueCache.Clear(); + } } /// @@ -266,10 +331,10 @@ public static void RemoveTypeMap(Type type) if (!snapshot.ContainsKey(type)) return; // nothing to do - var newCopy = new Dictionary(snapshot); + var newCopy = new Dictionary(snapshot); newCopy.Remove(type); - typeMap = newCopy; + SetTypeMap(newCopy); } /// @@ -277,9 +342,13 @@ public static void RemoveTypeMap(Type type) /// /// The type to handle. /// The handler to process the . - public static void AddTypeHandler(Type type, ITypeHandler handler) => AddTypeHandlerImpl(type, handler, true); - - internal static bool HasTypeHandler(Type type) => typeHandlers.ContainsKey(type); + public static void AddTypeHandler(Type type, ITypeHandler handler) => AddTypeHandlerCore(type, handler, true); + /// + /// Determine if the specified type will be processed by a custom handler. + /// + /// The type to handle. + /// Boolean value specifying whether the type will be processed by a custom handler. + public static bool HasTypeHandler(Type type) => typeHandlers.ContainsKey(type); /// /// Configure the specified type to be processed by a custom handler. @@ -287,15 +356,24 @@ public static void RemoveTypeMap(Type type) /// The type to handle. /// The handler to process the . /// Whether to clone the current type handler map. - public static void AddTypeHandlerImpl(Type type, ITypeHandler handler, bool clone) + [Obsolete("Please use " + nameof(AddTypeHandler), error: true)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static void AddTypeHandlerImpl(Type type, ITypeHandler? handler, bool clone) + { + // this method was accidentally made public; we'll mark it as illegal, but + // preserve existing usage in compiled code; sorry about this! + AddTypeHandlerCore(type, handler, true); // do not allow suppress clone + } + + private static void AddTypeHandlerCore(Type type, ITypeHandler? handler, bool clone) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); - Type secondary = null; - if (type.IsValueType()) + Type? secondary = null; + if (type.IsValueType) { var underlying = Nullable.GetUnderlyingType(type); - if (underlying == null) + if (underlying is null) { secondary = typeof(Nullable<>).MakeGenericType(type); // the Nullable // type is already the T @@ -307,29 +385,34 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler handler, bool clon } } - var snapshot = typeHandlers; - if (snapshot.TryGetValue(type, out ITypeHandler oldValue) && handler == oldValue) return; // nothing to do + // synchronize between callers mutating type-handlers; note that regular query + // code may still be accessing the field, so we still use snapshot/mutate/swap; + // the synchronize is just to prevent lost writes + lock (typeHandlersSyncLock) + { + if (typeHandlers.TryGetValue(type, out var oldValue) && handler == oldValue) return; // nothing to do - var newCopy = clone ? new Dictionary(snapshot) : snapshot; + var newCopy = clone ? new Dictionary(typeHandlers) : typeHandlers; #pragma warning disable 618 - typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { handler }); - if (secondary != null) - { - typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { handler }); - } + typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]); + if (secondary is not null) + { + typeof(TypeHandlerCache<>).MakeGenericType(secondary).GetMethod(nameof(TypeHandlerCache.SetHandler), BindingFlags.Static | BindingFlags.NonPublic)!.Invoke(null, [handler]); + } #pragma warning restore 618 - if (handler == null) - { - newCopy.Remove(type); - if (secondary != null) newCopy.Remove(secondary); - } - else - { - newCopy[type] = handler; - if (secondary != null) newCopy[secondary] = handler; + if (handler is null) + { + newCopy.Remove(type); + if (secondary is not null) newCopy.Remove(secondary); + } + else + { + newCopy[type] = handler; + if (secondary is not null) newCopy[secondary] = handler; + } + typeHandlers = newCopy; } - typeHandlers = newCopy; } /// @@ -337,9 +420,10 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler handler, bool clon /// /// The type to handle. /// The handler for the type . - public static void AddTypeHandler(TypeHandler handler) => AddTypeHandlerImpl(typeof(T), handler, true); + public static void AddTypeHandler(TypeHandler handler) => AddTypeHandlerCore(typeof(T), handler, true); private static Dictionary typeHandlers; + private static readonly object typeHandlersSyncLock = new(); internal const string LinqBinary = "System.Data.Linq.Binary"; @@ -348,17 +432,20 @@ public static void AddTypeHandlerImpl(Type type, ITypeHandler handler, bool clon /// /// Get the DbType that maps to a given value. /// + /// The parameter to configure the value for. /// The object to get a corresponding database type for. [Obsolete(ObsoleteInternalUsageOnly, false)] -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] - public static DbType GetDbType(object value) + public static void SetDbType(IDataParameter parameter, object value) { - if (value == null || value is DBNull) return DbType.Object; + if (value is null || value is DBNull) return; - return LookupDbType(value.GetType(), "n/a", false, out ITypeHandler handler); + var dbType = LookupDbType(value.GetType(), "n/a", false, out _); + if (DynamicParameters.ShouldSetDbType(dbType)) + { + parameter.DbType = dbType.GetValueOrDefault(); + } } /// @@ -369,22 +456,28 @@ public static DbType GetDbType(object value) /// Whether to demand a value (throw if missing). /// The handler for . [Obsolete(ObsoleteInternalUsageOnly, false)] -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] - public static DbType LookupDbType(Type type, string name, bool demand, out ITypeHandler handler) + public static DbType? LookupDbType(Type type, string name, bool demand, out ITypeHandler? handler) { handler = null; var nullUnderlyingType = Nullable.GetUnderlyingType(type); - if (nullUnderlyingType != null) type = nullUnderlyingType; - if (type.IsEnum() && !typeMap.ContainsKey(type)) + if (nullUnderlyingType is not null) type = nullUnderlyingType; + if (type.IsEnum && !typeMap.ContainsKey(type)) { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out handler)) + { + return DbType.Object; + } type = Enum.GetUnderlyingType(type); } - if (typeMap.TryGetValue(type, out DbType dbType)) + if (typeMap.TryGetValue(type, out var mapEntry)) { - return dbType; + if ((mapEntry.Flags & TypeMapEntryFlags.SetType) == 0) + { + return null; + } + return mapEntry.DbType; } if (type.FullName == LinqBinary) { @@ -396,10 +489,30 @@ public static DbType LookupDbType(Type type, string name, bool demand, out IType } if (typeof(IEnumerable).IsAssignableFrom(type)) { + // auto-detect things like IEnumerable as a family + if (type.IsInterface && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(IEnumerable<>) + && typeof(IEnumerable).IsAssignableFrom(type)) + { + var argTypes = type.GetGenericArguments(); + if (typeof(IDataRecord).IsAssignableFrom(argTypes[0])) + { + try + { + handler = (ITypeHandler)Activator.CreateInstance( + typeof(SqlDataRecordHandler<>).MakeGenericType(argTypes))!; + AddTypeHandlerCore(type, handler, true); + return DbType.Object; + } + catch + { + handler = null; + } + } + } return DynamicParameters.EnumerableMultiParameter; } -#if !NETSTANDARD1_3 && !NETSTANDARD2_0 switch (type.FullName) { case "Microsoft.SqlServer.Types.SqlGeography": @@ -412,7 +525,7 @@ public static DbType LookupDbType(Type type, string name, bool demand, out IType AddTypeHandler(type, handler = new UdtTypeHandler("hierarchyid")); return DbType.Object; } -#endif + if (demand) throw new NotSupportedException($"The member {name} of type {type.FullName} cannot be used as a parameter value"); return DbType.Object; @@ -424,8 +537,12 @@ public static DbType LookupDbType(Type type, string name, bool demand, out IType /// /// The type of element in the list. /// The enumerable to return as a list. - public static List AsList(this IEnumerable source) => - (source == null || source is List) ? (List)source : source.ToList(); + public static List AsList(this IEnumerable? source) => source switch + { + null => null!, + List list => list, + _ => Enumerable.ToList(source), + }; /// /// Execute parameterized SQL. @@ -437,7 +554,7 @@ public static List AsList(this IEnumerable source) => /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// The number of rows affected. - public static int Execute(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static int Execute(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered); return ExecuteImpl(cnn, ref command); @@ -461,7 +578,7 @@ public static int Execute(this IDbConnection cnn, string sql, object param = nul /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// The first cell selected as . - public static object ExecuteScalar(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static object? ExecuteScalar(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered); return ExecuteScalarImpl(cnn, ref command); @@ -478,7 +595,7 @@ public static object ExecuteScalar(this IDbConnection cnn, string sql, object pa /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// The first cell returned, as . - public static T ExecuteScalar(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static T? ExecuteScalar(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered); return ExecuteScalarImpl(cnn, ref command); @@ -490,7 +607,7 @@ public static T ExecuteScalar(this IDbConnection cnn, string sql, object para /// The connection to execute on. /// The command to execute. /// The first cell selected as . - public static object ExecuteScalar(this IDbConnection cnn, CommandDefinition command) => + public static object? ExecuteScalar(this IDbConnection cnn, CommandDefinition command) => ExecuteScalarImpl(cnn, ref command); /// @@ -500,12 +617,14 @@ public static object ExecuteScalar(this IDbConnection cnn, CommandDefinition com /// The connection to execute on. /// The command to execute. /// The first cell selected as . - public static T ExecuteScalar(this IDbConnection cnn, CommandDefinition command) => + public static T? ExecuteScalar(this IDbConnection cnn, CommandDefinition command) => ExecuteScalarImpl(cnn, ref command); - private static IEnumerable GetMultiExec(object param) + private static IEnumerable? GetMultiExec(object? param) { +#pragma warning disable IDE0038 // Use pattern matching - complicated enough! return (param is IEnumerable +#pragma warning restore IDE0038 // Use pattern matching && !(param is string || param is IEnumerable> || param is IDynamicParameters) @@ -514,11 +633,11 @@ private static IEnumerable GetMultiExec(object param) private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition command) { - object param = command.Parameters; - IEnumerable multiExec = GetMultiExec(param); + object? param = command.Parameters; + IEnumerable? multiExec = GetMultiExec(param); Identity identity; - CacheInfo info = null; - if (multiExec != null) + CacheInfo? info = null; + if (multiExec is not null) { if ((command.Flags & CommandFlags.Pipelined) != 0) { @@ -533,22 +652,22 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com if (wasClosed) cnn.Open(); using (var cmd = command.SetupCommand(cnn, null)) { - string masterSql = null; + string? masterSql = null; foreach (var obj in multiExec) { if (isFirst) { masterSql = cmd.CommandText; isFirst = false; - identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType(), null); + identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType()); info = GetCacheInfo(identity, obj, command.AddToCache); } else { - cmd.CommandText = masterSql; // because we do magic replaces on "in" etc + cmd.CommandText = masterSql; // because we do magic replacements on "in" etc cmd.Parameters.Clear(); // current code is Add-tastic } - info.ParamReader(cmd, obj); + info!.ParamReader!(cmd, obj); total += cmd.ExecuteNonQuery(); } } @@ -562,12 +681,12 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com } // nice and simple - if (param != null) + if (param is not null) { - identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); info = GetCacheInfo(identity, param, command.AddToCache); } - return ExecuteCommand(cnn, ref command, param == null ? null : info.ParamReader); + return ExecuteCommand(cnn, ref command, param is null ? null : info!.ParamReader); } /// @@ -595,11 +714,11 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com /// ]]> /// /// - public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered); - var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand dbcmd); - return new WrappedReader(dbcmd, reader); + var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand? dbcmd); + return DbWrappedReader.Create(dbcmd, reader); } /// @@ -614,8 +733,8 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, obje /// public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command) { - var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand dbcmd); - return new WrappedReader(dbcmd, reader); + var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand? dbcmd); + return DbWrappedReader.Create(dbcmd, reader); } /// @@ -631,8 +750,8 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinitio /// public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) { - var reader = ExecuteReaderImpl(cnn, ref command, commandBehavior, out IDbCommand dbcmd); - return new WrappedReader(dbcmd, reader); + var reader = ExecuteReaderImpl(cnn, ref command, commandBehavior, out IDbCommand? dbcmd); + return DbWrappedReader.Create(dbcmd, reader); } /// @@ -646,8 +765,9 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinitio /// The command timeout (in seconds). /// The type of command to execute. /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static IEnumerable Query(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) => - Query(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) => + Query(cnn, sql, param, transaction, buffered, commandTimeout, commandType); /// /// Return a dynamic object with properties matching the columns. @@ -659,8 +779,9 @@ public static IEnumerable Query(this IDbConnection cnn, string sql, obj /// The command timeout (in seconds). /// The type of command to execute. /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static dynamic QueryFirst(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryFirst(cnn, sql, param as object, transaction, commandTimeout, commandType); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static dynamic QueryFirst(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryFirst(cnn, sql, param, transaction, commandTimeout, commandType); /// /// Return a dynamic object with properties matching the columns. @@ -672,8 +793,9 @@ public static dynamic QueryFirst(this IDbConnection cnn, string sql, object para /// The command timeout (in seconds). /// The type of command to execute. /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static dynamic QueryFirstOrDefault(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QueryFirstOrDefault(cnn, sql, param as object, transaction, commandTimeout, commandType); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static dynamic? QueryFirstOrDefault(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QueryFirstOrDefault(cnn, sql, param, transaction, commandTimeout, commandType); /// /// Return a dynamic object with properties matching the columns. @@ -685,8 +807,9 @@ public static dynamic QueryFirstOrDefault(this IDbConnection cnn, string sql, ob /// The command timeout (in seconds). /// The type of command to execute. /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static dynamic QuerySingle(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QuerySingle(cnn, sql, param as object, transaction, commandTimeout, commandType); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static dynamic QuerySingle(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QuerySingle(cnn, sql, param, transaction, commandTimeout, commandType); /// /// Return a dynamic object with properties matching the columns. @@ -698,8 +821,9 @@ public static dynamic QuerySingle(this IDbConnection cnn, string sql, object par /// The command timeout (in seconds). /// The type of command to execute. /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> - public static dynamic QuerySingleOrDefault(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) => - QuerySingleOrDefault(cnn, sql, param as object, transaction, commandTimeout, commandType); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static dynamic? QuerySingleOrDefault(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) => + QuerySingleOrDefault(cnn, sql, param, transaction, commandTimeout, commandType); /// /// Executes a query, returning the data typed as . @@ -713,10 +837,11 @@ public static dynamic QuerySingleOrDefault(this IDbConnection cnn, string sql, o /// The command timeout (in seconds). /// The type of command to execute. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static IEnumerable Query(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var data = QueryImpl(cnn, command, typeof(T)); @@ -734,10 +859,11 @@ public static IEnumerable Query(this IDbConnection cnn, string sql, object /// The command timeout (in seconds). /// The type of command to execute. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static T QueryFirst(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static T QueryFirst(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.First, ref command, typeof(T)); @@ -754,10 +880,11 @@ public static T QueryFirst(this IDbConnection cnn, string sql, object param = /// The command timeout (in seconds). /// The type of command to execute. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static T QueryFirstOrDefault(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static T? QueryFirstOrDefault(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.FirstOrDefault, ref command, typeof(T)); @@ -774,10 +901,11 @@ public static T QueryFirstOrDefault(this IDbConnection cnn, string sql, objec /// The command timeout (in seconds). /// The type of command to execute. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static T QuerySingle(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static T QuerySingle(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.Single, ref command, typeof(T)); @@ -794,10 +922,11 @@ public static T QuerySingle(this IDbConnection cnn, string sql, object param /// The command timeout (in seconds). /// The type of command to execute. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static T QuerySingleOrDefault(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static T? QuerySingleOrDefault(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.SingleOrDefault, ref command, typeof(T)); @@ -816,12 +945,13 @@ public static T QuerySingleOrDefault(this IDbConnection cnn, string sql, obje /// The type of command to execute. /// is null. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static IEnumerable Query(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var data = QueryImpl(cnn, command, type); return command.Buffered ? data.ToList() : data; @@ -839,12 +969,13 @@ public static IEnumerable Query(this IDbConnection cnn, Type type, strin /// The type of command to execute. /// is null. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static object QueryFirst(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static object QueryFirst(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.First, ref command, type); } @@ -861,12 +992,13 @@ public static object QueryFirst(this IDbConnection cnn, Type type, string sql, o /// The type of command to execute. /// is null. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static object QueryFirstOrDefault(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static object? QueryFirstOrDefault(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.FirstOrDefault, ref command, type); } @@ -883,12 +1015,13 @@ public static object QueryFirstOrDefault(this IDbConnection cnn, Type type, stri /// The type of command to execute. /// is null. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static object QuerySingle(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static object QuerySingle(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.Single, ref command, type); } @@ -905,12 +1038,13 @@ public static object QuerySingle(this IDbConnection cnn, Type type, string sql, /// The type of command to execute. /// is null. /// - /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static object QuerySingleOrDefault(this IDbConnection cnn, Type type, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static object? QuerySingleOrDefault(this IDbConnection cnn, Type type, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { - if (type == null) throw new ArgumentNullException(nameof(type)); + if (type is null) throw new ArgumentNullException(nameof(type)); var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); return QueryRowImpl(cnn, Row.SingleOrDefault, ref command, type); } @@ -922,7 +1056,7 @@ public static object QuerySingleOrDefault(this IDbConnection cnn, Type type, str /// The connection to query on. /// The command used to query on this connection. /// - /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A sequence of data of ; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static IEnumerable Query(this IDbConnection cnn, CommandDefinition command) @@ -938,7 +1072,7 @@ public static IEnumerable Query(this IDbConnection cnn, CommandDefinition /// The connection to query on. /// The command used to query on this connection. /// - /// A single instance or null of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A single instance or null of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static T QueryFirst(this IDbConnection cnn, CommandDefinition command) => @@ -951,10 +1085,10 @@ public static T QueryFirst(this IDbConnection cnn, CommandDefinition command) /// The connection to query on. /// The command used to query on this connection. /// - /// A single or null instance of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A single or null instance of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static T QueryFirstOrDefault(this IDbConnection cnn, CommandDefinition command) => + public static T? QueryFirstOrDefault(this IDbConnection cnn, CommandDefinition command) => QueryRowImpl(cnn, Row.FirstOrDefault, ref command, typeof(T)); /// @@ -964,7 +1098,7 @@ public static T QueryFirstOrDefault(this IDbConnection cnn, CommandDefinition /// The connection to query on. /// The command used to query on this connection. /// - /// A single instance of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A single instance of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static T QuerySingle(this IDbConnection cnn, CommandDefinition command) => @@ -977,10 +1111,10 @@ public static T QuerySingle(this IDbConnection cnn, CommandDefinition command /// The connection to query on. /// The command used to query on this connection. /// - /// A single instance of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column in assumed, otherwise an instance is + /// A single instance of the supplied type; if a basic type (int, string, etc) is queried then the data from the first column is assumed, otherwise an instance is /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// - public static T QuerySingleOrDefault(this IDbConnection cnn, CommandDefinition command) => + public static T? QuerySingleOrDefault(this IDbConnection cnn, CommandDefinition command) => QueryRowImpl(cnn, Row.SingleOrDefault, ref command, typeof(T)); /// @@ -992,7 +1126,7 @@ public static T QuerySingleOrDefault(this IDbConnection cnn, CommandDefinitio /// The transaction to use for this query. /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? - public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered); return QueryMultipleImpl(cnn, ref command); @@ -1008,12 +1142,12 @@ public static GridReader QueryMultiple(this IDbConnection cnn, CommandDefinition private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandDefinition command) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType(), null); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType()); CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); - IDbCommand cmd = null; - IDataReader reader = null; + IDbCommand? cmd = null; + DbDataReader? reader = null; bool wasClosed = cnn.State == ConnectionState.Closed; try { @@ -1031,33 +1165,35 @@ private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandD } catch { - if (reader != null) + if (reader is not null) { if (!reader.IsClosed) { try { cmd?.Cancel(); } - catch { /* don't spoil the existing exception */ } + catch { /* don't spoil any existing exception */ } } reader.Dispose(); } + + cmd?.Parameters.Clear(); cmd?.Dispose(); if (wasClosed) cnn.Close(); throw; } } - private static IDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool wasClosed, CommandBehavior behavior) + private static DbDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool wasClosed, CommandBehavior behavior) { try { - return cmd.ExecuteReader(GetBehavior(wasClosed, behavior)); + return GetDbDataReader(cmd.ExecuteReader(GetBehavior(wasClosed, behavior))); } catch (ArgumentException ex) { // thanks, Sqlite! if (Settings.DisableCommandBehaviorOptimizations(behavior, ex)) { // we can retry; this time it will have different flags - return cmd.ExecuteReader(GetBehavior(wasClosed, behavior)); + return GetDbDataReader(cmd.ExecuteReader(GetBehavior(wasClosed, behavior))); } throw; } @@ -1065,12 +1201,12 @@ private static IDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool w private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefinition command, Type effectiveType) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType(), null); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); - IDbCommand cmd = null; - IDataReader reader = null; + IDbCommand? cmd = null; + DbDataReader? reader = null; bool wasClosed = cnn.State == ConnectionState.Closed; try @@ -1085,7 +1221,7 @@ private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefini // in the connection closing itself var tuple = info.Deserializer; int hash = GetColumnHash(reader); - if (tuple.Func == null || tuple.Hash != hash) + if (tuple.Func is null || tuple.Hash != hash) { if (reader.FieldCount == 0) //https://code.google.com/p/dapper-dot-net/issues/detail?id=57 yield break; @@ -1097,15 +1233,8 @@ private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefini var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; while (reader.Read()) { - object val = func(reader); - if (val == null || val is T) - { - yield return (T)val; - } - else - { - yield return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); - } + object? val = func(reader); + yield return GetValue(reader, effectiveType, val); } while (reader.NextResult()) { /* ignore subsequent result sets */ } // happy path; close the reader cleanly - no @@ -1117,16 +1246,18 @@ private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefini } finally { - if (reader != null) + if (reader is not null) { if (!reader.IsClosed) { - try { cmd.Cancel(); } - catch { /* don't spoil the existing exception */ } + try { cmd?.Cancel(); } + catch { /* don't spoil any existing exception */ } } reader.Dispose(); } if (wasClosed) cnn.Close(); + + cmd?.Parameters.Clear(); cmd?.Dispose(); } } @@ -1140,35 +1271,35 @@ internal enum Row SingleOrDefault = 3 } - private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = new int[0]; + private static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = []; private static void ThrowMultipleRows(Row row) { - switch (row) - { // get the standard exception from the runtime - case Row.Single: ErrTwoRows.Single(); break; - case Row.SingleOrDefault: ErrTwoRows.SingleOrDefault(); break; - default: throw new InvalidOperationException(); - } + _ = row switch + { + Row.Single => ErrTwoRows.Single(), + Row.SingleOrDefault => ErrTwoRows.SingleOrDefault(), + _ => throw new InvalidOperationException(), + }; } private static void ThrowZeroRows(Row row) { - switch (row) - { // get the standard exception from the runtime - case Row.First: ErrZeroRows.First(); break; - case Row.Single: ErrZeroRows.Single(); break; - default: throw new InvalidOperationException(); - } + _ = row switch + { // get the standard exception from the runtime + Row.First => ErrZeroRows.First(), + Row.Single => ErrZeroRows.Single(), + _ => throw new InvalidOperationException(), + }; } private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefinition command, Type effectiveType) { - object param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType(), null); + object? param = command.Parameters; + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); - IDbCommand cmd = null; - IDataReader reader = null; + IDbCommand? cmd = null; + DbDataReader? reader = null; bool wasClosed = cnn.State == ConnectionState.Closed; try @@ -1181,31 +1312,14 @@ private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefiniti : CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow); wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader - T result = default(T); + T result = default!; if (reader.Read() && reader.FieldCount != 0) { // with the CloseConnection flag, so the reader will deal with the connection; we // still need something in the "finally" to ensure that broken SQL still results // in the connection closing itself - var tuple = info.Deserializer; - int hash = GetColumnHash(reader); - if (tuple.Func == null || tuple.Hash != hash) - { - tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); - if (command.AddToCache) SetQueryCache(identity, info); - } + result = ReadRow(info, identity, ref command, effectiveType, reader); - var func = tuple.Func; - object val = func(reader); - if (val == null || val is T) - { - result = (T)val; - } - else - { - var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; - result = (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); - } if ((row & Row.Single) != 0 && reader.Read()) ThrowMultipleRows(row); while (reader.Read()) { /* ignore subsequent rows */ } } @@ -1224,22 +1338,78 @@ private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefiniti } finally { - if (reader != null) + if (reader is not null) { if (!reader.IsClosed) { - try { cmd.Cancel(); } - catch { /* don't spoil the existing exception */ } + try { cmd?.Cancel(); } + catch { /* don't spoil any existing exception */ } } reader.Dispose(); } if (wasClosed) cnn.Close(); + cmd?.Parameters.Clear(); cmd?.Dispose(); } } /// - /// Perform a multi-mapping query with 2 input types. + /// Shared value deserialization path for QueryRowImpl and QueryRowAsync + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T ReadRow(CacheInfo info, Identity identity, ref CommandDefinition command, Type effectiveType, DbDataReader reader) + { + var tuple = info.Deserializer; + int hash = GetColumnHash(reader); + if (tuple.Func is null || tuple.Hash != hash) + { + tuple = info.Deserializer = new DeserializerState(hash, GetDeserializer(effectiveType, reader, 0, -1, false)); + if (command.AddToCache) SetQueryCache(identity, info); + } + + var func = tuple.Func; + object? val = func(reader); + return GetValue(reader, effectiveType, val); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T GetValue(DbDataReader reader, Type effectiveType, object? val) + { + if (val is T tVal) + { + return tVal; + } + else if (val is null && (!effectiveType.IsValueType || Nullable.GetUnderlyingType(effectiveType) is not null)) + { + return default!; + } + else if (val is Array array && typeof(T).IsArray) + { + var elementType = typeof(T).GetElementType()!; + var result = Array.CreateInstance(elementType, array.Length); + for (int i = 0; i < array.Length; i++) + result.SetValue(Convert.ChangeType(array.GetValue(i), elementType, CultureInfo.InvariantCulture), i); + return (T)(object)result; + } + else + { + try + { + var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; + return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture)!; + } + catch (Exception ex) + { +#pragma warning disable CS0618 // Type or member is obsolete + ThrowDataException(ex, 0, reader, val); +#pragma warning restore CS0618 // Type or member is obsolete + return default!; // For the compiler - we've already thrown + } + } + } + + /// + /// Perform a multi-mapping query with 2 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -1255,11 +1425,12 @@ private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefiniti /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); /// - /// Perform a multi-mapping query with 3 input types. + /// Perform a multi-mapping query with 3 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -1276,11 +1447,12 @@ public static IEnumerable Query(this IDbConne /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); /// - /// Perform a multi-mapping query with 4 input types. + /// Perform a multi-mapping query with 4 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -1298,11 +1470,12 @@ public static IEnumerable Query(this /// Number of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); /// - /// Perform a multi-mapping query with 5 input types. + /// Perform a multi-mapping query with 5 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -1321,11 +1494,12 @@ public static IEnumerable QueryNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); /// - /// Perform a multi-mapping query with 6 input types. + /// Perform a multi-mapping query with 6 input types. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -1345,11 +1519,12 @@ public static IEnumerable QueryNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); /// - /// Perform a multi-mapping query with 7 input types. + /// Perform a multi-mapping query with 7 input types. If you need more types -> use Query with Type[] parameter. /// This returns a single type, combined from the raw types via . /// /// The first type in the recordset. @@ -1370,11 +1545,12 @@ public static IEnumerable QueryNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) => MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); /// - /// Perform a multi-mapping query with an arbitrary number of input types. + /// Perform a multi-mapping query with an arbitrary number of input types. /// This returns a single type, combined from the raw types via . /// /// The combined type to return. @@ -1389,7 +1565,8 @@ public static IEnumerable QueryNumber of seconds before command execution timeout. /// Is it a stored proc or a batch? /// An enumerable of . - public static IEnumerable Query(this IDbConnection cnn, string sql, Type[] types, Func map, object param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static IEnumerable Query(this IDbConnection cnn, string sql, Type[] types, Func map, object? param = null, IDbTransaction? transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var results = MultiMapImpl(cnn, command, types, map, splitOn, null, null, true); @@ -1397,47 +1574,47 @@ public static IEnumerable Query(this IDbConnection cnn, string } private static IEnumerable MultiMap( - this IDbConnection cnn, string sql, Delegate map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType) + this IDbConnection cnn, string sql, Delegate map, object? param, IDbTransaction? transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType) { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var results = MultiMapImpl(cnn, command, map, splitOn, null, null, true); return buffered ? results.ToList() : results; } - private static IEnumerable MultiMapImpl(this IDbConnection cnn, CommandDefinition command, Delegate map, string splitOn, IDataReader reader, Identity identity, bool finalize) + private static IEnumerable MultiMapImpl(this IDbConnection? cnn, CommandDefinition command, Delegate map, string splitOn, DbDataReader? reader, Identity? identity, bool finalize) { - object param = command.Parameters; - identity = identity ?? new Identity(command.CommandText, command.CommandType, cnn, typeof(TFirst), param?.GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth), typeof(TSixth), typeof(TSeventh) }); + object? param = command.Parameters; + identity ??= new Identity(command.CommandText, command.CommandTypeDirect, cnn!, typeof(TFirst), param?.GetType()); CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); - IDbCommand ownedCommand = null; - IDataReader ownedReader = null; + IDbCommand? ownedCommand = null; + DbDataReader? ownedReader = null; bool wasClosed = cnn?.State == ConnectionState.Closed; try { - if (reader == null) + if (reader is null) { - ownedCommand = command.SetupCommand(cnn, cinfo.ParamReader); - if (wasClosed) cnn.Open(); + ownedCommand = command.SetupCommand(cnn!, cinfo.ParamReader); + if (wasClosed) cnn!.Open(); ownedReader = ExecuteReaderWithFlagsFallback(ownedCommand, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult); reader = ownedReader; } var deserializer = default(DeserializerState); - Func[] otherDeserializers; + Func[]? otherDeserializers; int hash = GetColumnHash(reader); - if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash) + if ((deserializer = cinfo.Deserializer).Func is null || (otherDeserializers = cinfo.OtherDeserializers) is null || hash != deserializer.Hash) { - var deserializers = GenerateDeserializers(new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth), typeof(TSixth), typeof(TSeventh) }, splitOn, reader); + var deserializers = GenerateDeserializers(identity, splitOn, reader); deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]); otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); if (command.AddToCache) SetQueryCache(identity, cinfo); } - Func mapIt = GenerateMapper(deserializer.Func, otherDeserializers, map); + Func mapIt = GenerateMapper(deserializer.Func, otherDeserializers, map); - if (mapIt != null) + if (mapIt is not null) { while (reader.Read()) { @@ -1458,8 +1635,9 @@ private static IEnumerable MultiMapImpl MultiMapImpl(this IDbConnection cnn, CommandDefinition command, Type[] types, Func map, string splitOn, IDataReader reader, Identity identity, bool finalize) + private static IEnumerable MultiMapImpl(this IDbConnection? cnn, CommandDefinition command, Type[] types, Func map, string splitOn, DbDataReader? reader, Identity? identity, bool finalize) { if (types.Length < 1) { throw new ArgumentException("you must provide at least one type to deserialize"); } - object param = command.Parameters; - identity = identity ?? new Identity(command.CommandText, command.CommandType, cnn, types[0], param?.GetType(), types); + object? param = command.Parameters; + identity ??= new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn!, types[0], param?.GetType(), types); CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); - IDbCommand ownedCommand = null; - IDataReader ownedReader = null; + IDbCommand? ownedCommand = null; + DbDataReader? ownedReader = null; bool wasClosed = cnn?.State == ConnectionState.Closed; try { - if (reader == null) + if (reader is null) { - ownedCommand = command.SetupCommand(cnn, cinfo.ParamReader); - if (wasClosed) cnn.Open(); + ownedCommand = command.SetupCommand(cnn!, cinfo.ParamReader); + if (wasClosed) cnn!.Open(); ownedReader = ExecuteReaderWithFlagsFallback(ownedCommand, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult); reader = ownedReader; } DeserializerState deserializer; - Func[] otherDeserializers; + Func[]? otherDeserializers; int hash = GetColumnHash(reader); - if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash) + if ((deserializer = cinfo.Deserializer).Func is null || (otherDeserializers = cinfo.OtherDeserializers) is null || hash != deserializer.Hash) { - var deserializers = GenerateDeserializers(types, splitOn, reader); + var deserializers = GenerateDeserializers(identity, splitOn, reader); deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]); otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); - SetQueryCache(identity, cinfo); + if (command.AddToCache) SetQueryCache(identity, cinfo); } - Func mapIt = GenerateMapper(types.Length, deserializer.Func, otherDeserializers, map); + Func mapIt = GenerateMapper(types.Length, deserializer.Func, otherDeserializers, map); - if (mapIt != null) + if (mapIt is not null) { while (reader.Read()) { @@ -1528,34 +1706,26 @@ private static IEnumerable MultiMapImpl(this IDbConnection cnn } finally { + ownedCommand?.Parameters.Clear(); ownedCommand?.Dispose(); - if (wasClosed) cnn.Close(); + if (wasClosed) cnn!.Close(); } } } - private static Func GenerateMapper(Func deserializer, Func[] otherDeserializers, object map) + private static Func GenerateMapper(Func deserializer, Func[] otherDeserializers, object map) + => otherDeserializers.Length switch { - switch (otherDeserializers.Length) - { - case 1: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r)); - case 2: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r)); - case 3: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r)); - case 4: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r)); - case 5: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r), (TSixth)otherDeserializers[4](r)); - case 6: - return r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r), (TSixth)otherDeserializers[4](r), (TSeventh)otherDeserializers[5](r)); - default: - throw new NotSupportedException(); - } - } - - private static Func GenerateMapper(int length, Func deserializer, Func[] otherDeserializers, Func map) + 1 => r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r)), + 2 => r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r)), + 3 => r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r)), + 4 => r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r)), + 5 => r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r), (TSixth)otherDeserializers[4](r)), + 6 => r => ((Func)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r), (TThird)otherDeserializers[1](r), (TFourth)otherDeserializers[2](r), (TFifth)otherDeserializers[3](r), (TSixth)otherDeserializers[4](r), (TSeventh)otherDeserializers[5](r)), + _ => throw new NotSupportedException(), + }; + + private static Func GenerateMapper(int length, Func deserializer, Func[] otherDeserializers, Func map) { return r => { @@ -1571,12 +1741,14 @@ private static Func GenerateMapper(int length, Fu }; } - private static Func[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader) + private static Func[] GenerateDeserializers(Identity identity, string splitOn, DbDataReader reader) { - var deserializers = new List>(); + var deserializers = new List>(); var splits = splitOn.Split(',').Select(s => s.Trim()).ToArray(); bool isMultiSplit = splits.Length > 1; - if (types[0] == typeof(object)) + + int typeCount = identity.TypeCount; + if (identity.GetType(0) == typeof(object)) { // we go left to right for dynamic multi-mapping so that the madness of TestMultiMappingVariations // is supported @@ -1584,8 +1756,10 @@ private static Func[] GenerateDeserializers(Type[] types, s int currentPos = 0; int splitIdx = 0; string currentSplit = splits[splitIdx]; - foreach (var type in types) + + for (int i = 0; i < typeCount; i++) { + Type type = identity.GetType(i); if (type == typeof(DontMap)) { break; @@ -1608,9 +1782,9 @@ private static Func[] GenerateDeserializers(Type[] types, s int currentPos = reader.FieldCount; int splitIdx = splits.Length - 1; var currentSplit = splits[splitIdx]; - for (var typeIdx = types.Length - 1; typeIdx >= 0; --typeIdx) + for (var typeIdx = typeCount - 1; typeIdx >= 0; --typeIdx) { - var type = types[typeIdx]; + var type = identity.GetType(typeIdx); if (type == typeof(DontMap)) { continue; @@ -1636,11 +1810,11 @@ private static Func[] GenerateDeserializers(Type[] types, s return deserializers.ToArray(); } - private static int GetNextSplitDynamic(int startIdx, string splitOn, IDataReader reader) + private static int GetNextSplitDynamic(int startIdx, string splitOn, DbDataReader reader) { if (startIdx == reader.FieldCount) { - throw MultiMapException(reader); + throw MultiMapException(reader, splitOn); } if (splitOn == "*") @@ -1659,7 +1833,7 @@ private static int GetNextSplitDynamic(int startIdx, string splitOn, IDataReader return reader.FieldCount; } - private static int GetNextSplit(int startIdx, string splitOn, IDataReader reader) + private static int GetNextSplit(int startIdx, string splitOn, DbDataReader reader) { if (splitOn == "*") { @@ -1674,39 +1848,39 @@ private static int GetNextSplit(int startIdx, string splitOn, IDataReader reader } } - throw MultiMapException(reader); + throw MultiMapException(reader, splitOn); } - private static CacheInfo GetCacheInfo(Identity identity, object exampleParameters, bool addToCache) + private static CacheInfo GetCacheInfo(Identity identity, object? exampleParameters, bool addToCache) { - if (!TryGetQueryCache(identity, out CacheInfo info)) + if (!TryGetQueryCache(identity, out CacheInfo? info)) { - if (GetMultiExec(exampleParameters) != null) + if (GetMultiExec(exampleParameters) is not null) { throw new InvalidOperationException("An enumerable sequence of parameters (arrays, lists, etc) is not allowed in this context"); } info = new CacheInfo(); - if (identity.parametersType != null) + if (identity.ParametersType is not null) { - Action reader; + Action reader; if (exampleParameters is IDynamicParameters) { - reader = (cmd, obj) => ((IDynamicParameters)obj).AddParameters(cmd, identity); + reader = (cmd, obj) => ((IDynamicParameters)obj!).AddParameters(cmd, identity); } else if (exampleParameters is IEnumerable>) { reader = (cmd, obj) => { - IDynamicParameters mapped = new DynamicParameters(obj); + IDynamicParameters mapped = new DynamicParameters(obj!); mapped.AddParameters(cmd, identity); }; } else { - var literals = GetLiteralTokens(identity.sql); + var literals = GetLiteralTokens(identity.Sql); reader = CreateParamInfoGenerator(identity, false, true, literals); } - if ((identity.commandType == null || identity.commandType == CommandType.Text) && ShouldPassByPosition(identity.sql)) + if ((identity.CommandType is null || identity.CommandType == CommandType.Text) && ShouldPassByPosition(identity.Sql)) { var tail = reader; reader = (cmd, obj) => @@ -1724,29 +1898,31 @@ private static CacheInfo GetCacheInfo(Identity identity, object exampleParameter private static bool ShouldPassByPosition(string sql) { - return sql?.IndexOf('?') >= 0 && pseudoPositional.IsMatch(sql); + return sql?.IndexOf('?') >= 0 && CompiledRegex.PseudoPositional.IsMatch(sql); } private static void PassByPosition(IDbCommand cmd) { if (cmd.Parameters.Count == 0) return; - Dictionary parameters = new Dictionary(StringComparer.Ordinal); + Dictionary parameters = new(StringComparer.Ordinal); foreach (IDbDataParameter param in cmd.Parameters) { if (!string.IsNullOrEmpty(param.ParameterName)) parameters[param.ParameterName] = param; } - HashSet consumed = new HashSet(StringComparer.Ordinal); + var consumed = new HashSet(StringComparer.Ordinal); bool firstMatch = true; - cmd.CommandText = pseudoPositional.Replace(cmd.CommandText, match => + int index = 0; // use this to spoof names; in most pseudo-positional cases, the name is ignored, however: + // for "snowflake", the name needs to be incremental i.e. "1", "2", "3" + cmd.CommandText = CompiledRegex.PseudoPositional.Replace(cmd.CommandText, match => { string key = match.Groups[1].Value; if (!consumed.Add(key)) { throw new InvalidOperationException("When passing parameters by position, each parameter can only be referenced once"); } - else if (parameters.TryGetValue(key, out IDbDataParameter param)) + else if (parameters.TryGetValue(key, out IDbDataParameter? param)) { if (firstMatch) { @@ -1754,6 +1930,10 @@ private static void PassByPosition(IDbCommand cmd) cmd.Parameters.Clear(); // only clear if we are pretty positive that we've found this pattern successfully } // if found, return the anonymous token "?" + if (Settings.UseIncrementalPseudoPositionalParameterNames) + { + param.ParameterName = (++index).ToString(); + } cmd.Parameters.Add(param); parameters.Remove(key); consumed.Add(key); @@ -1767,43 +1947,64 @@ private static void PassByPosition(IDbCommand cmd) }); } - private static Func GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + static DbDataReader GetDbDataReader(IDataReader reader) { + return reader as DbDataReader ?? new WrappedBasicReader(reader); + } + + private static Func GetDeserializer(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + { + + // dynamic is passed in as Object ... by c# design if (type == typeof(object) || type == typeof(DapperRow)) { return GetDapperRowDeserializer(reader, startBound, length, returnNullIfFirstMissing); } - Type underlyingType = null; - if (!(typeMap.ContainsKey(type) || type.IsEnum() || type.FullName == LinqBinary - || (type.IsValueType() && (underlyingType = Nullable.GetUnderlyingType(type)) != null && underlyingType.IsEnum()))) + + Type? underlyingType = null; + bool useGetFieldValue = false; + if (typeMap.TryGetValue(type, out var mapEntry)) + { + useGetFieldValue = (mapEntry.Flags & TypeMapEntryFlags.UseGetFieldValue) != 0; + } + else if (!(type.IsEnum || type.IsArray || type.FullName == LinqBinary + || (type.IsValueType && (underlyingType = Nullable.GetUnderlyingType(type)) is not null && underlyingType.IsEnum))) { - if (typeHandlers.TryGetValue(type, out ITypeHandler handler)) + if (typeHandlers.TryGetValue(type, out ITypeHandler? handler)) { return GetHandlerDeserializer(handler, type, startBound); } return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing); } - return GetStructDeserializer(type, underlyingType ?? type, startBound); + return GetSimpleValueDeserializer(type, underlyingType ?? type, startBound, useGetFieldValue); } - private static Func GetHandlerDeserializer(ITypeHandler handler, Type type, int startBound) + private static Func GetHandlerDeserializer(ITypeHandler handler, Type type, int startBound) { - return reader => handler.Parse(type, reader.GetValue(startBound)); + return reader => handler.Parse(type, reader.GetValue(startBound))!; } - private static Exception MultiMapException(IDataRecord reader) + private static Exception MultiMapException(IDataRecord reader, string? splitOn = null) { bool hasFields = false; - try { hasFields = reader != null && reader.FieldCount != 0; } + try { hasFields = reader is not null && reader.FieldCount != 0; } catch { /* don't throw when trying to throw */ } if (hasFields) - return new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); + { + return new ArgumentException( + string.IsNullOrEmpty(splitOn) + ? "When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id" + : $"Multi-map error: splitOn column '{splitOn}' was not found - please ensure your splitOn parameter is set and in the correct order", + nameof(splitOn)); + } else + { return new InvalidOperationException("No columns were selected"); + } } - internal static Func GetDapperRowDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) + internal static Func GetDapperRowDeserializer(DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) { var fieldCount = reader.FieldCount; if (length == -1) @@ -1818,12 +2019,12 @@ internal static Func GetDapperRowDeserializer(IDataRecord r var effectiveFieldCount = Math.Min(fieldCount - startBound, length); - DapperTable table = null; + DapperTable? table = null; return r => { - if (table == null) + if (table is null) { string[] names = new string[effectiveFieldCount]; for (int i = 0; i < effectiveFieldCount; i++) @@ -1840,7 +2041,7 @@ internal static Func GetDapperRowDeserializer(IDataRecord r values[0] = r.GetValue(startBound); if (values[0] is DBNull) { - return null; + return null!; } } @@ -1849,7 +2050,7 @@ internal static Func GetDapperRowDeserializer(IDataRecord r for (int i = 0; i < values.Length; i++) { object val = r.GetValue(i); - values[i] = val is DBNull ? null : val; + values[i] = val is DBNull ? null! : val; } } else @@ -1858,7 +2059,7 @@ internal static Func GetDapperRowDeserializer(IDataRecord r for (var iter = begin; iter < effectiveFieldCount; ++iter) { object obj = r.GetValue(iter + startBound); - values[iter] = obj is DBNull ? null : obj; + values[iter] = obj is DBNull ? null! : obj; } } return new DapperRow(table, values); @@ -1868,34 +2069,30 @@ internal static Func GetDapperRowDeserializer(IDataRecord r /// Internal use only. /// /// The object to convert to a character. -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete(ObsoleteInternalUsageOnly, false)] public static char ReadChar(object value) { - if (value == null || value is DBNull) throw new ArgumentNullException(nameof(value)); - var s = value as string; - if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", nameof(value)); - return s[0]; + if (value is null || value is DBNull) throw new ArgumentNullException(nameof(value)); + if (value is string s && s.Length == 1) return s[0]; + if (value is char c) return c; + throw new ArgumentException("A single-character was expected", nameof(value)); } /// /// Internal use only. /// /// The object to convert to a character. -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete(ObsoleteInternalUsageOnly, false)] public static char? ReadNullableChar(object value) { - if (value == null || value is DBNull) return null; - var s = value as string; - if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", nameof(value)); - return s[0]; + if (value is null || value is DBNull) return null; + if (value is string s && s.Length == 1) return s[0]; + if (value is char c) return c; + throw new ArgumentException("A single-character was expected", nameof(value)); } /// @@ -1904,9 +2101,7 @@ public static char ReadChar(object value) /// The parameter collection to search in. /// The command for this fetch. /// The name of the parameter to get. -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete(ObsoleteInternalUsageOnly, true)] public static IDbDataParameter FindOrAddParameter(IDataParameterCollection parameters, IDbCommand command, string name) @@ -1954,7 +2149,7 @@ internal static int GetListPaddingExtraCount(int count) private static string GetInListRegex(string name, bool byPosition) => byPosition ? (@"(\?)" + Regex.Escape(name) + @"\?(?!\w)(\s+(?i)unknown(?-i))?") - : ("([?@:]" + Regex.Escape(name) + @")(?!\w)(\s+(?i)unknown(?-i))?"); + : ("([?@:$]" + Regex.Escape(name) + @")(?!\w)(\s+(?i)unknown(?-i))?"); /// /// Internal use only. @@ -1962,12 +2157,10 @@ private static string GetInListRegex(string name, bool byPosition) => byPosition /// The command to pack parameters for. /// The name prefix for these parameters. /// The parameter value can be an -#if !NETSTANDARD1_3 [Browsable(false)] -#endif [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete(ObsoleteInternalUsageOnly, false)] - public static void PackListParameters(IDbCommand command, string namePrefix, object value) + public static void PackListParameters(IDbCommand command, string namePrefix, object? value) { // initially we tried TVP, however it performs quite poorly. // keep in mind SQL support up to 2000 params easily in sp_executesql, needing more is rare @@ -1986,32 +2179,31 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj var count = 0; bool isString = value is IEnumerable; bool isDbString = value is IEnumerable; - DbType dbType = 0; + DbType? dbType = null; int splitAt = SqlMapper.Settings.InListStringSplitCount; bool viaSplit = splitAt >= 0 && TryStringSplit(ref list, splitAt, namePrefix, command, byPosition); - if (list != null && !viaSplit) + if (list is not null && !viaSplit) { - object lastValue = null; + object? lastValue = null; foreach (var item in list) { if (++count == 1) // first item: fetch some type info { - if (item == null) + if (item is null) { throw new NotSupportedException("The first item in a list-expansion cannot be null"); } if (!isDbString) { - dbType = LookupDbType(item.GetType(), "", true, out ITypeHandler handler); + dbType = LookupDbType(item.GetType(), "", true, out var handler); } } var nextName = namePrefix + count.ToString(); - if (isDbString && item is DbString) + if (isDbString && item is DbString str) { - var str = item as DbString; str.AddParameter(command, nextName); } else @@ -2021,24 +2213,24 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj if (isString) { listParam.Size = DbString.DefaultLength; - if (item != null && ((string)item).Length > DbString.DefaultLength) + if (item is not null && ((string)item).Length > DbString.DefaultLength) { listParam.Size = -1; } } var tmp = listParam.Value = SanitizeParameterValue(item); - if (tmp != null && !(tmp is DBNull)) + if (tmp is not null && tmp is not DBNull) lastValue = tmp; // only interested in non-trivial values for padding - if (listParam.DbType != dbType) + if (DynamicParameters.ShouldSetDbType(dbType) && listParam.DbType != dbType.GetValueOrDefault()) { - listParam.DbType = dbType; + listParam.DbType = dbType.GetValueOrDefault(); } command.Parameters.Add(listParam); } } - if (Settings.PadListExpansions && !isDbString && lastValue != null) + if (Settings.PadListExpansions && !isDbString && lastValue is not null) { int padCount = GetListPaddingExtraCount(count); for (int i = 0; i < padCount; i++) @@ -2047,7 +2239,10 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj var padParam = command.CreateParameter(); padParam.ParameterName = namePrefix + count.ToString(); if (isString) padParam.Size = DbString.DefaultLength; - padParam.DbType = dbType; + if (DynamicParameters.ShouldSetDbType(dbType)) + { + padParam.DbType = dbType.GetValueOrDefault(); + } padParam.Value = lastValue; command.Parameters.Add(padParam); } @@ -2096,18 +2291,18 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj { sb.Append(',').Append(variableName).Append(i).Append(suffix); } - return sb.__ToStringRecycle(); + return sb.ToStringRecycle(); } else { var sb = GetStringBuilder().Append('(').Append(variableName); - if (!byPosition) sb.Append(1); + if (!byPosition) sb.Append(1); else sb.Append(namePrefix).Append(1).Append(variableName); for (int i = 2; i <= count; i++) { sb.Append(',').Append(variableName); - if (!byPosition) sb.Append(i); + if (!byPosition) sb.Append(i); else sb.Append(namePrefix).Append(i).Append(variableName); } - return sb.Append(')').__ToStringRecycle(); + return sb.Append(')').ToStringRecycle(); } }, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant); } @@ -2115,35 +2310,30 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj } } - private static bool TryStringSplit(ref IEnumerable list, int splitAt, string namePrefix, IDbCommand command, bool byPosition) + private static bool TryStringSplit(ref IEnumerable? list, int splitAt, string namePrefix, IDbCommand command, bool byPosition) { - if (list == null || splitAt < 0) return false; - switch (list) + if (list is null || splitAt < 0) return false; + return list switch { - case IEnumerable l: - return TryStringSplit(ref l, splitAt, namePrefix, command, "int", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); - case IEnumerable l: - return TryStringSplit(ref l, splitAt, namePrefix, command, "bigint", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); - case IEnumerable l: - return TryStringSplit(ref l, splitAt, namePrefix, command, "smallint", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); - case IEnumerable l: - return TryStringSplit(ref l, splitAt, namePrefix, command, "tinyint", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); - } - return false; + IEnumerable l => TryStringSplit(ref l, splitAt, namePrefix, command, "int", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))), + IEnumerable l => TryStringSplit(ref l, splitAt, namePrefix, command, "bigint", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))), + IEnumerable l => TryStringSplit(ref l, splitAt, namePrefix, command, "smallint", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))), + IEnumerable l => TryStringSplit(ref l, splitAt, namePrefix, command, "tinyint", byPosition, (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))), + _ => false, + }; } private static bool TryStringSplit(ref IEnumerable list, int splitAt, string namePrefix, IDbCommand command, string colType, bool byPosition, Action append) { - var typed = list as ICollection; - if (typed == null) + if (list is not ICollection typed) { typed = list.ToList(); list = typed; // because we still need to be able to iterate it, even if we fail here } if (typed.Count < splitAt) return false; - string varName = null; + string? varName = null; var regexIncludingUnknown = GetInListRegex(namePrefix, byPosition); var sql = Regex.Replace(command.CommandText, regexIncludingUnknown, match => { @@ -2159,7 +2349,7 @@ private static bool TryStringSplit(ref IEnumerable list, int splitAt, stri return "(select cast([value] as " + colType + ") from string_split(" + variableName + ",','))"; } }, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant); - if (varName == null) return false; // couldn't resolve the var! + if (varName is null) return false; // couldn't resolve the var! command.CommandText = sql; var concatenatedParam = command.CreateParameter(); @@ -2190,24 +2380,19 @@ private static bool TryStringSplit(ref IEnumerable list, int splitAt, stri } /// - /// OBSOLETE: For internal usage only. Sanitizes the paramter value with proper type casting. + /// OBSOLETE: For internal usage only. Sanitizes the parameter value with proper type casting. /// /// The value to sanitize. [Obsolete(ObsoleteInternalUsageOnly, false)] - public static object SanitizeParameterValue(object value) + public static object SanitizeParameterValue(object? value) { - if (value == null) return DBNull.Value; + if (value is null) return DBNull.Value; if (value is Enum) { - TypeCode typeCode; - if (value is IConvertible) - { - typeCode = ((IConvertible)value).GetTypeCode(); - } - else - { - typeCode = TypeExtensions.GetTypeCode(Enum.GetUnderlyingType(value.GetType())); - } + TypeCode typeCode = value is IConvertible convertible + ? convertible.GetTypeCode() + : Type.GetTypeCode(Enum.GetUnderlyingType(value.GetType())); + switch (typeCode) { case TypeCode.Byte: return (byte)value; @@ -2228,49 +2413,42 @@ private static IEnumerable FilterParameters(IEnumerable(16); foreach (var p in parameters) { - if (Regex.IsMatch(sql, @"[?@:]" + p.Name + @"([^\p{L}\p{N}_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)) + if (Regex.IsMatch(sql, @"[?@:$]" + p.Name + @"([^\p{L}\p{N}_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)) list.Add(p); } return list; } - // look for ? / @ / : *by itself* - private static readonly Regex smellsLikeOleDb = new Regex(@"(? /// Replace all literal tokens with their text form. /// /// The parameter lookup to do replacements with. - /// The command to repalce parameters in. + /// The command to replace parameters in. public static void ReplaceLiterals(this IParameterLookup parameters, IDbCommand command) { var tokens = GetLiteralTokens(command.CommandText); if (tokens.Count != 0) ReplaceLiterals(parameters, command, tokens); } - internal static readonly MethodInfo format = typeof(SqlMapper).GetMethod("Format", BindingFlags.Public | BindingFlags.Static); + internal static readonly MethodInfo format = typeof(SqlMapper).GetMethod("Format", BindingFlags.Public | BindingFlags.Static)!; /// /// Convert numeric values to their string form for SQL literal purposes. /// /// The value to get a string for. [Obsolete(ObsoleteInternalUsageOnly)] - public static string Format(object value) + public static string Format(object? value) { - if (value == null) + if (value is null) { return "null"; } else { - switch (TypeExtensions.GetTypeCode(value.GetType())) + switch (Type.GetTypeCode(value.GetType())) { -#if !NETSTANDARD1_3 case TypeCode.DBNull: return "null"; -#endif case TypeCode.Boolean: return ((bool)value) ? "1" : "0"; case TypeCode.Byte: @@ -2297,9 +2475,9 @@ public static string Format(object value) return ((decimal)value).ToString(CultureInfo.InvariantCulture); default: var multiExec = GetMultiExec(value); - if (multiExec != null) + if (multiExec is not null) { - StringBuilder sb = null; + StringBuilder? sb = null; bool first = true; foreach (object subval in multiExec) { @@ -2310,7 +2488,7 @@ public static string Format(object value) } else { - sb.Append(','); + sb!.Append(','); } sb.Append(Format(subval)); } @@ -2320,10 +2498,10 @@ public static string Format(object value) } else { - return sb.Append(')').__ToStringRecycle(); + return sb!.Append(')').ToStringRecycle(); } } - throw new NotSupportedException(value.GetType().Name); + throw new NotSupportedException($"The type '{value.GetType().Name}' is not supported for SQL literals."); } } } @@ -2333,7 +2511,7 @@ internal static void ReplaceLiterals(IParameterLookup parameters, IDbCommand com var sql = command.CommandText; foreach (var token in tokens) { - object value = parameters[token.Member]; + object? value = parameters[token.Member]; #pragma warning disable 0618 string text = Format(value); #pragma warning restore 0618 @@ -2342,14 +2520,15 @@ internal static void ReplaceLiterals(IParameterLookup parameters, IDbCommand com command.CommandText = sql; } + [SuppressMessage("Style", "IDE0220:Add explicit cast", Justification = "Regex matches are Match")] internal static IList GetLiteralTokens(string sql) { if (string.IsNullOrEmpty(sql)) return LiteralToken.None; - if (!literalTokens.IsMatch(sql)) return LiteralToken.None; + if (!CompiledRegex.LiteralTokens.IsMatch(sql)) return LiteralToken.None; - var matches = literalTokens.Matches(sql); + var matches = CompiledRegex.LiteralTokens.Matches(sql); var found = new HashSet(StringComparer.Ordinal); - List list = new List(matches.Count); + var list = new List(matches.Count); foreach (Match match in matches) { string token = match.Value; @@ -2368,66 +2547,52 @@ internal static IList GetLiteralTokens(string sql) /// Whether to check for duplicates. /// Whether to remove unused parameters. public static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) => - CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.sql)); + CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.Sql)); - private static bool IsValueTuple(Type type) => type?.IsValueType() == true && type.FullName.StartsWith("System.ValueTuple`", StringComparison.Ordinal); + private static bool IsValueTuple(Type? type) => (type?.IsValueType == true + && type.FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true) + || (type is not null && IsValueTuple(Nullable.GetUnderlyingType(type))); - private static List GetValueTupleMembers(Type type, string[] names) + internal static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList literals) { - var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); - var result = new List(names.Length); - for (int i = 0; i < names.Length; i++) - { - FieldInfo field = null; - string name = "Item" + (i + 1).ToString(CultureInfo.InvariantCulture); - foreach (var test in fields) - { - if (test.Name == name) - { - field = test; - break; - } - } - result.Add(field == null ? null : new SimpleMemberMap(string.IsNullOrWhiteSpace(names[i]) ? name : names[i], field)); - } - return result; - } - - internal static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList literals) - { - Type type = identity.parametersType; + Type type = identity.ParametersType!; if (IsValueTuple(type)) { throw new NotSupportedException("ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing"); } - bool filterParams = false; - if (removeUnused && identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) + bool filterParams = removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text; + + if (filterParams && Settings.SupportLegacyParameterTokens) { - filterParams = !smellsLikeOleDb.IsMatch(identity.sql); + filterParams = !CompiledRegex.LegacyParameter.IsMatch(identity.Sql); } - var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); + + var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, [typeof(IDbCommand), typeof(object)], type, true); var il = dm.GetILGenerator(); - bool isStruct = type.IsValueType(); - bool haveInt32Arg1 = false; + bool isStruct = type.IsValueType; + var _sizeLocal = (LocalBuilder?)null; + LocalBuilder GetSizeLocal() => _sizeLocal ??= il.DeclareLocal(typeof(int)); il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param] + + LocalBuilder typedParameterLocal; if (isStruct) { - il.DeclareLocal(type.MakePointerType()); + typedParameterLocal = il.DeclareLocal(type.MakeByRefType()); // note: ref-local il.Emit(OpCodes.Unbox, type); // stack is now [typed-param] } else { - il.DeclareLocal(type); // 0 + typedParameterLocal = il.DeclareLocal(type); il.Emit(OpCodes.Castclass, type); // stack is now [typed-param] } - il.Emit(OpCodes.Stloc_0);// stack is now empty + il.Emit(OpCodes.Stloc, typedParameterLocal); // stack is now empty il.Emit(OpCodes.Ldarg_0); // stack is now [command] - il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetProperty(nameof(IDbCommand.Parameters)).GetGetMethod(), null); // stack is now [parameters] + il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetProperty(nameof(IDbCommand.Parameters))!.GetGetMethod()!, null); // stack is now [parameters] var allTypeProps = type.GetProperties(); var propsList = new List(allTypeProps.Length); @@ -2440,7 +2605,7 @@ internal static Action CreateParamInfoGenerator(Identity ide var ctors = type.GetConstructors(); ParameterInfo[] ctorParams; - IEnumerable props = null; + IEnumerable? props = null; // try to detect tuple patterns, e.g. anon-types, and use that to choose the order // otherwise: alphabetical if (ctors.Length == 1 && propsList.Count == (ctorParams = ctors[0].GetParameters()).Length) @@ -2462,10 +2627,10 @@ internal static Action CreateParamInfoGenerator(Identity ide } else { // might still all be accounted for; check the hard way - var positionByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + var positionByName = new Dictionary(ctorParams.Length, StringComparer.OrdinalIgnoreCase); foreach (var param in ctorParams) { - positionByName[param.Name] = param.Position; + positionByName[param.Name!] = param.Position; } if (positionByName.Count == propsList.Count) { @@ -2488,14 +2653,14 @@ internal static Action CreateParamInfoGenerator(Identity ide } } } - if (props == null) + if (props is null) { propsList.Sort(new PropertyInfoByNameComparer()); props = propsList; } if (filterParams) { - props = FilterParameters(props, identity.sql); + props = FilterParameters(props, identity.Sql); } var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt; @@ -2503,28 +2668,42 @@ internal static Action CreateParamInfoGenerator(Identity ide { if (typeof(ICustomQueryParameter).IsAssignableFrom(prop.PropertyType)) { - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [typed-param] - il.Emit(callOpCode, prop.GetGetMethod()); // stack is [parameters] [custom] + il.Emit(OpCodes.Ldloc, typedParameterLocal); // stack is now [parameters] [typed-param] + il.Emit(callOpCode, prop.GetGetMethod()!); // stack is [parameters] [custom] + if (prop.PropertyType.IsValueType) + { + il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [boxed-custom] + } + else + { + // throw if null + var notNull = il.DefineLabel(); + il.Emit(OpCodes.Dup); // stack is [parameters] [custom] [custom] + il.Emit(OpCodes.Brtrue_S, notNull); // stack is [parameters] [custom] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is [parameters] [custom] [name] + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(ThrowNullCustomQueryParameter))!, null); // stack is [parameters] [custom] + il.MarkLabel(notNull); + } il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [custom] [command] il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [custom] [command] [name] - il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod(nameof(ICustomQueryParameter.AddParameter)), null); // stack is now [parameters] + il.EmitCall(OpCodes.Callvirt, typeof(ICustomQueryParameter).GetMethod(nameof(ICustomQueryParameter.AddParameter))!, null); // stack is now [parameters] continue; } #pragma warning disable 618 - DbType dbType = LookupDbType(prop.PropertyType, prop.Name, true, out ITypeHandler handler); + DbType? dbType = LookupDbType(prop.PropertyType, prop.Name, true, out ITypeHandler? handler); #pragma warning restore 618 if (dbType == DynamicParameters.EnumerableMultiParameter) { // this actually represents special handling for list types; il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [command] il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [command] [name] - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [command] [name] [typed-param] - il.Emit(callOpCode, prop.GetGetMethod()); // stack is [parameters] [command] [name] [typed-value] - if (prop.PropertyType.IsValueType()) + il.Emit(OpCodes.Ldloc, typedParameterLocal); // stack is now [parameters] [command] [name] [typed-param] + il.Emit(callOpCode, prop.GetGetMethod()!); // stack is [parameters] [command] [name] [typed-value] + if (prop.PropertyType.IsValueType) { il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [command] [name] [boxed-value] } - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.PackListParameters)), null); // stack is [parameters] + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.PackListParameters))!, null); // stack is [parameters] continue; } il.Emit(OpCodes.Dup); // stack is now [parameters] [parameters] @@ -2535,52 +2714,57 @@ internal static Action CreateParamInfoGenerator(Identity ide { // need to be a little careful about adding; use a utility method il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [command] [name] - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.FindOrAddParameter)), null); // stack is [parameters] [parameter] + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.FindOrAddParameter))!, null); // stack is [parameters] [parameter] } else { // no risk of duplicates; just blindly add - il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetMethod(nameof(IDbCommand.CreateParameter)), null);// stack is now [parameters] [parameters] [parameter] + il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetMethod(nameof(IDbCommand.CreateParameter))!, null);// stack is now [parameters] [parameters] [parameter] il.Emit(OpCodes.Dup);// stack is now [parameters] [parameters] [parameter] [parameter] il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [parameter] [parameter] [name] - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.ParameterName)).GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter] + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.ParameterName))!.GetSetMethod()!, null);// stack is now [parameters] [parameters] [parameter] } - if (dbType != DbType.Time && handler == null) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time + if (DynamicParameters.ShouldSetDbType(dbType) && dbType != DbType.Time && handler is null) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time { il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - if (dbType == DbType.Object && prop.PropertyType == typeof(object)) // includes dynamic + if (dbType.GetValueOrDefault() == DbType.Object && prop.PropertyType == typeof(object)) // includes dynamic { // look it up from the param value - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [[parameters]] [parameter] [parameter] [typed-param] - il.Emit(callOpCode, prop.GetGetMethod()); // stack is [parameters] [[parameters]] [parameter] [parameter] [object-value] - il.Emit(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.GetDbType), BindingFlags.Static | BindingFlags.Public)); // stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] + il.Emit(OpCodes.Ldloc, typedParameterLocal); // stack is now [parameters] [[parameters]] [parameter] [parameter] [typed-param] + il.Emit(callOpCode, prop.GetGetMethod()!); // stack is [parameters] [[parameters]] [parameter] [parameter] [object-value] + il.Emit(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.SetDbType), BindingFlags.Static | BindingFlags.Public)!); // stack is now [parameters] [[parameters]] [parameter] } else { // constant value; nice and simple - EmitInt32(il, (int)dbType);// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] + EmitInt32(il, (int)dbType.GetValueOrDefault());// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.DbType))!.GetSetMethod()!, null);// stack is now [parameters] [[parameters]] [parameter] } - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.DbType)).GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] } il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] EmitInt32(il, (int)ParameterDirection.Input);// stack is now [parameters] [[parameters]] [parameter] [parameter] [dir] - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.Direction)).GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.Direction))!.GetSetMethod()!, null);// stack is now [parameters] [[parameters]] [parameter] il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [[parameters]] [parameter] [parameter] [typed-param] - il.Emit(callOpCode, prop.GetGetMethod()); // stack is [parameters] [[parameters]] [parameter] [parameter] [typed-value] + il.Emit(OpCodes.Ldloc, typedParameterLocal); // stack is now [parameters] [[parameters]] [parameter] [parameter] [typed-param] + il.Emit(callOpCode, prop.GetGetMethod()!); // stack is [parameters] [[parameters]] [parameter] [parameter] [typed-value] bool checkForNull; - if (prop.PropertyType.IsValueType()) + if (prop.PropertyType.IsValueType) { var propType = prop.PropertyType; var nullType = Nullable.GetUnderlyingType(propType); bool callSanitize = false; - if ((nullType ?? propType).IsEnum()) + if ((nullType ?? propType).IsEnum) { - if (nullType != null) + if (handler is not null) + { + // TypeHandler registered - box as the enum type, handler does conversion + checkForNull = nullType is not null; + } + else if (nullType is not null) { // Nullable; we want to box as the underlying type; that's just *hard*; for // simplicity, box as Nullable and call SanitizeParameterValue @@ -2590,7 +2774,7 @@ internal static Action CreateParamInfoGenerator(Identity ide { checkForNull = false; // non-nullable enum; we can do that! just box to the wrong type! (no, really) - switch (TypeExtensions.GetTypeCode(Enum.GetUnderlyingType(propType))) + switch (Type.GetTypeCode(Enum.GetUnderlyingType(propType))) { case TypeCode.Byte: propType = typeof(byte); break; case TypeCode.SByte: propType = typeof(sbyte); break; @@ -2605,13 +2789,13 @@ internal static Action CreateParamInfoGenerator(Identity ide } else { - checkForNull = nullType != null; + checkForNull = nullType is not null; } il.Emit(OpCodes.Box, propType); // stack is [parameters] [[parameters]] [parameter] [parameter] [boxed-value] if (callSanitize) { checkForNull = false; // handled by sanitize - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SanitizeParameterValue)), null); + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SanitizeParameterValue))!, null); // stack is [parameters] [[parameters]] [parameter] [parameter] [boxed-value] } } @@ -2621,11 +2805,6 @@ internal static Action CreateParamInfoGenerator(Identity ide } if (checkForNull) { - if ((dbType == DbType.String || dbType == DbType.AnsiString) && !haveInt32Arg1) - { - il.DeclareLocal(typeof(int)); - haveInt32Arg1 = true; - } // relative stack: [boxed value] il.Emit(OpCodes.Dup);// relative stack: [boxed value] [boxed value] Label notNull = il.DefineLabel(); @@ -2633,18 +2812,18 @@ internal static Action CreateParamInfoGenerator(Identity ide il.Emit(OpCodes.Brtrue_S, notNull); // relative stack [boxed value = null] il.Emit(OpCodes.Pop); // relative stack empty - il.Emit(OpCodes.Ldsfld, typeof(DBNull).GetField(nameof(DBNull.Value))); // relative stack [DBNull] + il.Emit(OpCodes.Ldsfld, typeof(DBNull).GetField(nameof(DBNull.Value))!); // relative stack [DBNull] if (dbType == DbType.String || dbType == DbType.AnsiString) { EmitInt32(il, 0); - il.Emit(OpCodes.Stloc_1); + il.Emit(OpCodes.Stloc, GetSizeLocal()); } - if (allDone != null) il.Emit(OpCodes.Br_S, allDone.Value); + if (allDone is not null) il.Emit(OpCodes.Br_S, allDone.Value); il.MarkLabel(notNull); if (prop.PropertyType == typeof(string)) { il.Emit(OpCodes.Dup); // [string] [string] - il.EmitCall(OpCodes.Callvirt, typeof(string).GetProperty(nameof(string.Length)).GetGetMethod(), null); // [string] [length] + il.EmitCall(OpCodes.Callvirt, typeof(string).GetProperty(nameof(string.Length))!.GetGetMethod()!, null); // [string] [length] EmitInt32(il, DbString.DefaultLength); // [string] [length] [4000] il.Emit(OpCodes.Cgt); // [string] [0 or 1] Label isLong = il.DefineLabel(), lenDone = il.DefineLabel(); @@ -2654,37 +2833,38 @@ internal static Action CreateParamInfoGenerator(Identity ide il.MarkLabel(isLong); EmitInt32(il, -1); // [string] [-1] il.MarkLabel(lenDone); - il.Emit(OpCodes.Stloc_1); // [string] + il.Emit(OpCodes.Stloc, GetSizeLocal()); // [string] } if (prop.PropertyType.FullName == LinqBinary) { - il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod("ToArray", BindingFlags.Public | BindingFlags.Instance), null); + il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod("ToArray", BindingFlags.Public | BindingFlags.Instance)!, null); } - if (allDone != null) il.MarkLabel(allDone.Value); + if (allDone is not null) il.MarkLabel(allDone.Value); // relative stack [boxed value or DBNull] } - if (handler != null) + if (handler is not null) { #pragma warning disable 618 - il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache.SetValue))); // stack is now [parameters] [[parameters]] [parameter] + il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod(nameof(TypeHandlerCache.SetValue))!); // stack is now [parameters] [[parameters]] [parameter] #pragma warning restore 618 } else { - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.Value)).GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty(nameof(IDataParameter.Value))!.GetSetMethod()!, null);// stack is now [parameters] [[parameters]] [parameter] } if (prop.PropertyType == typeof(string)) { var endOfSize = il.DefineLabel(); + var sizeLocal = GetSizeLocal(); // don't set if 0 - il.Emit(OpCodes.Ldloc_1); // [parameters] [[parameters]] [parameter] [size] + il.Emit(OpCodes.Ldloc, sizeLocal); // [parameters] [[parameters]] [parameter] [size] il.Emit(OpCodes.Brfalse_S, endOfSize); // [parameters] [[parameters]] [parameter] il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] - il.Emit(OpCodes.Ldloc_1); // stack is now [parameters] [[parameters]] [parameter] [parameter] [size] - il.EmitCall(OpCodes.Callvirt, typeof(IDbDataParameter).GetProperty(nameof(IDbDataParameter.Size)).GetSetMethod(), null); // stack is now [parameters] [[parameters]] [parameter] + il.Emit(OpCodes.Ldloc, sizeLocal); // stack is now [parameters] [[parameters]] [parameter] [parameter] [size] + il.EmitCall(OpCodes.Callvirt, typeof(IDbDataParameter).GetProperty(nameof(IDbDataParameter.Size))!.GetSetMethod()!, null); // stack is now [parameters] [[parameters]] [parameter] il.MarkLabel(endOfSize); } @@ -2697,7 +2877,7 @@ internal static Action CreateParamInfoGenerator(Identity ide { // stack is now [parameters] [parameters] [parameter] // blindly add - il.EmitCall(OpCodes.Callvirt, typeof(IList).GetMethod(nameof(IList.Add)), null); // stack is now [parameters] + il.EmitCall(OpCodes.Callvirt, typeof(IList).GetMethod(nameof(IList.Add))!, null); // stack is now [parameters] il.Emit(OpCodes.Pop); // IList.Add returns the new index (int); we don't care } } @@ -2705,18 +2885,18 @@ internal static Action CreateParamInfoGenerator(Identity ide // stack is currently [parameters] il.Emit(OpCodes.Pop); // stack is now empty - if (literals.Count != 0 && propsList != null) + if (literals.Count != 0 && propsList is not null) { il.Emit(OpCodes.Ldarg_0); // command il.Emit(OpCodes.Ldarg_0); // command, command - var cmdText = typeof(IDbCommand).GetProperty(nameof(IDbCommand.CommandText)); - il.EmitCall(OpCodes.Callvirt, cmdText.GetGetMethod(), null); // command, sql - Dictionary locals = null; - LocalBuilder local = null; + var cmdText = typeof(IDbCommand).GetProperty(nameof(IDbCommand.CommandText))!; + il.EmitCall(OpCodes.Callvirt, cmdText.GetGetMethod()!, null); // command, sql + Dictionary? locals = null; + LocalBuilder? local = null; foreach (var literal in literals) { // find the best member, preferring case-sensitive - PropertyInfo exact = null, fallback = null; + PropertyInfo? exact = null, fallback = null; string huntName = literal.Member; for (int i = 0; i < propsList.Count; i++) { @@ -2733,13 +2913,13 @@ internal static Action CreateParamInfoGenerator(Identity ide } var prop = exact ?? fallback; - if (prop != null) + if (prop is not null) { il.Emit(OpCodes.Ldstr, literal.Token); - il.Emit(OpCodes.Ldloc_0); // command, sql, typed parameter - il.EmitCall(callOpCode, prop.GetGetMethod(), null); // command, sql, typed value + il.Emit(OpCodes.Ldloc, typedParameterLocal); // command, sql, typed parameter + il.EmitCall(callOpCode, prop.GetGetMethod()!, null); // command, sql, typed value Type propType = prop.PropertyType; - var typeCode = TypeExtensions.GetTypeCode(propType); + var typeCode = Type.GetTypeCode(propType); switch (typeCode) { case TypeCode.Boolean: @@ -2764,19 +2944,19 @@ internal static Action CreateParamInfoGenerator(Identity ide case TypeCode.Decimal: // need to stloc, ldloca, call // re-use existing locals (both the last known, and via a dictionary) - var convert = GetToString(typeCode); - if (local == null || local.LocalType != propType) + var convert = GetToString(typeCode)!; + if (local is null || local.LocalType != propType) { - if (locals == null) + if (locals is null) { - locals = new Dictionary(); + locals = []; local = null; } else { if (!locals.TryGetValue(propType, out local)) local = null; } - if (local == null) + if (local is null) { local = il.DeclareLocal(propType); locals.Add(propType, local); @@ -2788,37 +2968,37 @@ internal static Action CreateParamInfoGenerator(Identity ide il.EmitCall(OpCodes.Call, convert, null); // command, sql, string value break; default: - if (propType.IsValueType()) il.Emit(OpCodes.Box, propType); // command, sql, object value + if (propType.IsValueType) il.Emit(OpCodes.Box, propType); // command, sql, object value il.EmitCall(OpCodes.Call, format, null); // command, sql, string value break; } il.EmitCall(OpCodes.Callvirt, StringReplace, null); } } - il.EmitCall(OpCodes.Callvirt, cmdText.GetSetMethod(), null); // empty + il.EmitCall(OpCodes.Callvirt, cmdText.GetSetMethod()!, null); // empty } il.Emit(OpCodes.Ret); - return (Action)dm.CreateDelegate(typeof(Action)); + return (Action)dm.CreateDelegate(typeof(Action)); } private static readonly Dictionary toStrings = new[] { typeof(bool), typeof(sbyte), typeof(byte), typeof(ushort), typeof(short), typeof(uint), typeof(int), typeof(ulong), typeof(long), typeof(float), typeof(double), typeof(decimal) - }.ToDictionary(x => TypeExtensions.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), new[] { typeof(IFormatProvider) })); + }.ToDictionary(x => Type.GetTypeCode(x), x => x.GetPublicInstanceMethod(nameof(object.ToString), [typeof(IFormatProvider)])!); - private static MethodInfo GetToString(TypeCode typeCode) + private static MethodInfo? GetToString(TypeCode typeCode) { - return toStrings.TryGetValue(typeCode, out MethodInfo method) ? method : null; + return toStrings.TryGetValue(typeCode, out MethodInfo? method) ? method : null; } - private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), new Type[] { typeof(string), typeof(string) }), - InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static).GetGetMethod(); + private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), [typeof(string), typeof(string)])!, + InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static)!.GetGetMethod()!; - private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action paramReader) + private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action? paramReader) { - IDbCommand cmd = null; + IDbCommand? cmd = null; bool wasClosed = cnn.State == ConnectionState.Closed; try { @@ -2831,23 +3011,24 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma finally { if (wasClosed) cnn.Close(); + cmd?.Parameters.Clear(); cmd?.Dispose(); } } - private static T ExecuteScalarImpl(IDbConnection cnn, ref CommandDefinition command) + private static T? ExecuteScalarImpl(IDbConnection cnn, ref CommandDefinition command) { - Action paramReader = null; - object param = command.Parameters; - if (param != null) + Action? paramReader = null; + object? param = command.Parameters; + if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; } - IDbCommand cmd = null; + IDbCommand? cmd = null; bool wasClosed = cnn.State == ConnectionState.Closed; - object result; + object? result; try { cmd = command.SetupCommand(cnn, paramReader); @@ -2858,14 +3039,15 @@ private static T ExecuteScalarImpl(IDbConnection cnn, ref CommandDefinition c finally { if (wasClosed) cnn.Close(); + cmd?.Parameters.Clear(); cmd?.Dispose(); } return Parse(result); } - private static IDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefinition command, CommandBehavior commandBehavior, out IDbCommand cmd) + private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefinition command, CommandBehavior commandBehavior, out IDbCommand? cmd) { - Action paramReader = GetParameterReader(cnn, ref command); + Action? paramReader = GetParameterReader(cnn, ref command); cmd = null; bool wasClosed = cnn.State == ConnectionState.Closed, disposeCommand = true; try @@ -2881,31 +3063,35 @@ private static IDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefin finally { if (wasClosed) cnn.Close(); - if (cmd != null && disposeCommand) cmd.Dispose(); + if (cmd is not null && disposeCommand) + { + cmd.Parameters.Clear(); + cmd.Dispose(); + } } } - private static Action GetParameterReader(IDbConnection cnn, ref CommandDefinition command) + private static Action? GetParameterReader(IDbConnection cnn, ref CommandDefinition command) { - object param = command.Parameters; - IEnumerable multiExec = GetMultiExec(param); - CacheInfo info = null; - if (multiExec != null) + object? param = command.Parameters; + IEnumerable? multiExec = GetMultiExec(param); + CacheInfo? info = null; + if (multiExec is not null) { throw new NotSupportedException("MultiExec is not supported by ExecuteReader"); } // nice and simple - if (param != null) + if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); info = GetCacheInfo(identity, param, command.AddToCache); } var paramReader = info?.ParamReader; return paramReader; } - private static Func GetStructDeserializer(Type type, Type effectiveType, int index) + private static Func GetSimpleValueDeserializer(Type type, Type effectiveType, int index, bool useGetFieldValue) { // no point using special per-type handling here; it boils down to the same, plus not all are supported anyway (see: SqlDataReader.GetChar - not supported!) #pragma warning disable 618 @@ -2915,16 +3101,25 @@ private static Func GetStructDeserializer(Type type, Type e } if (type == typeof(char?)) { - return r => ReadNullableChar(r.GetValue(index)); + return r => ReadNullableChar(r.GetValue(index))!; } if (type.FullName == LinqBinary) { - return r => Activator.CreateInstance(type, r.GetValue(index)); + return r => Activator.CreateInstance(type, r.GetValue(index))!; } #pragma warning restore 618 - if (effectiveType.IsEnum()) - { // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum + if (effectiveType.IsEnum) + { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out var enumHandler)) + { + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null! : enumHandler.Parse(type, val)!; + }; + } + // assume the value is returned as the correct type (int/byte/etc), but box back to the typed enum return r => { var val = r.GetValue(index); @@ -2932,56 +3127,95 @@ private static Func GetStructDeserializer(Type type, Type e { val = Convert.ChangeType(val, Enum.GetUnderlyingType(effectiveType), CultureInfo.InvariantCulture); } - return val is DBNull ? null : Enum.ToObject(effectiveType, val); + return val is DBNull ? null! : Enum.ToObject(effectiveType, val); }; } - if (typeHandlers.TryGetValue(type, out ITypeHandler handler)) + if (typeHandlers.TryGetValue(type, out var handler)) { return r => { var val = r.GetValue(index); - return val is DBNull ? null : handler.Parse(type, val); + return val is DBNull ? null! : handler.Parse(type, val)!; }; } - return r => + return useGetFieldValue ? ReadViaGetFieldValueFactory(type, index) : ReadViaGetValue(index); + + static Func ReadViaGetValue(int index) + => reader => + { + var val = reader.GetValue(index); + return val is DBNull ? null! : val; + }; + } + + static Func ReadViaGetFieldValueFactory(Type type, int index) + { + type = Nullable.GetUnderlyingType(type) ?? type; + var factory = (Func>?)s_ReadViaGetFieldValueCache[type]; + if (factory is null) { - var val = r.GetValue(index); - return val is DBNull ? null : val; - }; + factory = (Func>)Delegate.CreateDelegate( + typeof(Func>), null, typeof(SqlMapper).GetMethod( + nameof(UnderlyingReadViaGetFieldValueFactory), BindingFlags.Static | BindingFlags.NonPublic)! + .MakeGenericMethod(type)); + lock (s_ReadViaGetFieldValueCache) + { + s_ReadViaGetFieldValueCache[type] = factory; + } + } + return factory(index); } + // cache of ReadViaGetFieldValueFactory for per-value T + static readonly Hashtable s_ReadViaGetFieldValueCache = []; + + static Func UnderlyingReadViaGetFieldValueFactory(int index) + => reader => reader.IsDBNull(index) ? null! : reader.GetFieldValue(index)!; + + static bool UseGetFieldValue(Type type) => typeMap.TryGetValue(type, out var mapEntry) + && (mapEntry.Flags & TypeMapEntryFlags.UseGetFieldValue) != 0; - private static T Parse(object value) + private static T Parse(object? value) { - if (value == null || value is DBNull) return default(T); - if (value is T) return (T)value; + if (value is null || value is DBNull) return default!; + if (value is T t) return t; var type = typeof(T); type = Nullable.GetUnderlyingType(type) ?? type; - if (type.IsEnum()) + if (type.IsEnum) { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.TryGetValue(type, out ITypeHandler? enumHandler)) + { + return (T)enumHandler.Parse(type, value)!; + } if (value is float || value is double || value is decimal) { value = Convert.ChangeType(value, Enum.GetUnderlyingType(type), CultureInfo.InvariantCulture); } return (T)Enum.ToObject(type, value); } - if (typeHandlers.TryGetValue(type, out ITypeHandler handler)) + if (typeHandlers.TryGetValue(type, out ITypeHandler? handler)) { - return (T)handler.Parse(type, value); + return (T)handler.Parse(type, value)!; } return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture); } private static readonly MethodInfo - enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), new Type[] { typeof(Type), typeof(string), typeof(bool) }), - getItem = typeof(IDataRecord).GetProperties(BindingFlags.Instance | BindingFlags.Public) + enumParse = typeof(Enum).GetMethod(nameof(Enum.Parse), [typeof(Type), typeof(string), typeof(bool)])!, + getItem = typeof(DbDataReader).GetProperties(BindingFlags.Instance | BindingFlags.Public) .Where(p => p.GetIndexParameters().Length > 0 && p.GetIndexParameters()[0].ParameterType == typeof(int)) - .Select(p => p.GetGetMethod()).First(); + .Select(p => p.GetGetMethod()).First()!, + getFieldValueT = typeof(DbDataReader).GetMethod(nameof(DbDataReader.GetFieldValue), + BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!, + isDbNull = typeof(DbDataReader).GetMethod(nameof(DbDataReader.IsDBNull), + BindingFlags.Instance | BindingFlags.Public, null, [typeof(int)], null)!; /// /// Gets type-map for the given type /// /// Type map instance, default is to create new instance of DefaultTypeMap +#pragma warning disable CA2211 // Non-constant fields should not be visible - I agree with you, but we can't do that until we break the API public static Func TypeMapProvider = (Type type) => new DefaultTypeMap(type); +#pragma warning restore CA2211 // Non-constant fields should not be visible /// /// Gets type-map for the given . @@ -2990,16 +3224,16 @@ private static readonly MethodInfo /// Type map implementation, DefaultTypeMap instance if no override present public static ITypeMap GetTypeMap(Type type) { - if (type == null) throw new ArgumentNullException(nameof(type)); - var map = (ITypeMap)_typeMaps[type]; - if (map == null) + if (type is null) throw new ArgumentNullException(nameof(type)); + var map = (ITypeMap?)_typeMaps[type]; + if (map is null) { lock (_typeMaps) { // double-checked; store this to avoid reflection next time we see this type // since multiple queries commonly use the same domain-entity/DTO/view-model type - map = (ITypeMap)_typeMaps[type]; + map = (ITypeMap?)_typeMaps[type]; - if (map == null) + if (map is null) { map = TypeMapProvider(type); _typeMaps[type] = map; @@ -3010,19 +3244,19 @@ public static ITypeMap GetTypeMap(Type type) } // use Hashtable to get free lockless reading - private static readonly Hashtable _typeMaps = new Hashtable(); + private static readonly Hashtable _typeMaps = []; /// /// Set custom mapping for type deserializers /// /// Entity type to override - /// Mapping rules impementation, null to remove custom map - public static void SetTypeMap(Type type, ITypeMap map) + /// Mapping rules implementation, null to remove custom map + public static void SetTypeMap(Type type, ITypeMap? map) { - if (type == null) + if (type is null) throw new ArgumentNullException(nameof(type)); - if (map == null || map is DefaultTypeMap) + if (map is null || map is DefaultTypeMap) { lock (_typeMaps) { @@ -3049,44 +3283,59 @@ public static void SetTypeMap(Type type, ITypeMap map) /// /// /// +#if DEBUG // make sure we're not using this internally + [Obsolete(nameof(DbDataReader) + " API should be preferred")] +#endif + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] public static Func GetTypeDeserializer( Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false ) + { + return WrapObjectReader(GetTypeDeserializer(type, GetDbDataReader(reader), startBound, length, returnNullIfFirstMissing)); + } + + private static Func WrapObjectReader(Func dbReader) + => reader => dbReader(GetDbDataReader(reader)); // we'll eat the extra layer here; this is not a core API + + + /// + /// Internal use only + /// + /// + /// + /// + /// + /// + public static Func GetTypeDeserializer( + Type type, DbDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false + ) { return TypeDeserializerCache.GetReader(type, reader, startBound, length, returnNullIfFirstMissing); } - private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary locals, Type type, bool initAndLoad) + private static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary? locals, Type type, bool initAndLoad) { - if (type == null) throw new ArgumentNullException(nameof(type)); - locals = locals ?? new Dictionary(); - if (!locals.TryGetValue(type, out LocalBuilder found)) + if (type is null) throw new ArgumentNullException(nameof(type)); + locals ??= []; + if (!locals.TryGetValue(type, out LocalBuilder? found)) { found = il.DeclareLocal(type); locals.Add(type, found); } if (initAndLoad) { - il.Emit(OpCodes.Ldloca, (short)found.LocalIndex); + il.Emit(OpCodes.Ldloca, found); il.Emit(OpCodes.Initobj, type); - il.Emit(OpCodes.Ldloca, (short)found.LocalIndex); + il.Emit(OpCodes.Ldloca, found); il.Emit(OpCodes.Ldobj, type); } return found; } - private static Func GetTypeDeserializerImpl( - Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false + private static Func GetTypeDeserializerImpl( + Type type, DbDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false ) { - var returnType = type.IsValueType() ? typeof(object) : type; - var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, new[] { typeof(IDataReader) }, type, true); - var il = dm.GetILGenerator(); - il.DeclareLocal(typeof(int)); - il.DeclareLocal(type); - il.Emit(OpCodes.Ldc_I4_0); - il.Emit(OpCodes.Stloc_0); - if (length == -1) { length = reader.FieldCount - startBound; @@ -3097,20 +3346,144 @@ private static Func GetTypeDeserializerImpl( throw MultiMapException(reader); } + var returnType = type.IsValueType ? typeof(object) : type; + var dm = new DynamicMethod("Deserialize" + Guid.NewGuid().ToString(), returnType, [typeof(DbDataReader)], type, true); + var il = dm.GetILGenerator(); + + if (IsValueTuple(type)) + { + GenerateValueTupleDeserializer(type, reader, startBound, length, il); + } + else + { + GenerateDeserializerFromMap(type, reader, startBound, length, returnNullIfFirstMissing, il); + } + + var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(DbDataReader), returnType); + return (Func)dm.CreateDelegate(funcType); + } + + private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataReader reader, int startBound, int length, ILGenerator il) + { + var nullableUnderlyingType = Nullable.GetUnderlyingType(valueTupleType); + var currentValueTupleType = nullableUnderlyingType ?? valueTupleType; + + var constructors = new List(); + var languageTupleElementTypes = new List(); + + while (true) + { + var arity = int.Parse(currentValueTupleType.Name.Substring("ValueTuple`".Length), CultureInfo.InvariantCulture); + var constructorParameterTypes = new Type[arity]; + var restField = (FieldInfo?)null; + + foreach (var field in currentValueTupleType.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + if (field.Name == "Rest") + { + restField = field; + } + else if (field.Name.StartsWith("Item", StringComparison.Ordinal)) + { + var elementNumber = int.Parse(field.Name.Substring("Item".Length), CultureInfo.InvariantCulture); + constructorParameterTypes[elementNumber - 1] = field.FieldType; + } + } + + var itemFieldCount = constructorParameterTypes.Length; + if (restField is not null) itemFieldCount--; + + for (var i = 0; i < itemFieldCount; i++) + { + languageTupleElementTypes.Add(constructorParameterTypes[i]); + } + + if (restField is not null) + { + constructorParameterTypes[constructorParameterTypes.Length - 1] = restField.FieldType; + } + + constructors.Add(currentValueTupleType.GetConstructor(constructorParameterTypes)!); + + if (restField is null) break; + + currentValueTupleType = restField.FieldType; + if (!IsValueTuple(currentValueTupleType)) + { + throw new InvalidOperationException("The Rest field of a ValueTuple must contain a nested ValueTuple of arity 1 or greater."); + } + } + + var stringEnumLocal = (LocalBuilder?)null; + + for (var i = 0; i < languageTupleElementTypes.Count; i++) + { + var targetType = languageTupleElementTypes[i]; + + if (i < length) + { + LoadReaderValueOrBranchToDBNullLabel( + il, + startBound + i, + ref stringEnumLocal, + valueCopyLocal: null, + reader.GetFieldType(startBound + i), + targetType, + out var isDbNullLabel, out bool popWhenNull); + + var finishLabel = il.DefineLabel(); + il.Emit(OpCodes.Br_S, finishLabel); + il.MarkLabel(isDbNullLabel); + if (popWhenNull) + { + il.Emit(OpCodes.Pop); + } + + LoadDefaultValue(il, targetType); + + il.MarkLabel(finishLabel); + } + else + { + LoadDefaultValue(il, targetType); + } + } + + for (var i = constructors.Count - 1; i >= 0; i--) + { + il.Emit(OpCodes.Newobj, constructors[i]); + } + + if (nullableUnderlyingType is not null) + { + var nullableTupleConstructor = valueTupleType.GetConstructor([nullableUnderlyingType]); + + il.Emit(OpCodes.Newobj, nullableTupleConstructor!); + } + + il.Emit(OpCodes.Box, valueTupleType); + il.Emit(OpCodes.Ret); + } + + private static void GenerateDeserializerFromMap(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing, ILGenerator il) + { + var currentIndexDiagnosticLocal = il.DeclareLocal(typeof(int)); + var returnValueLocal = il.DeclareLocal(type); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc, currentIndexDiagnosticLocal); + var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray(); ITypeMap typeMap = GetTypeMap(type); int index = startBound; - ConstructorInfo specializedConstructor = null; + ConstructorInfo? specializedConstructor = null; -#if !NETSTANDARD1_3 bool supportInitialize = false; -#endif - Dictionary structLocals = null; - if (type.IsValueType()) + Dictionary? structLocals = null; + if (type.IsValueType) { - il.Emit(OpCodes.Ldloca_S, (byte)1); + il.Emit(OpCodes.Ldloca, returnValueLocal); il.Emit(OpCodes.Initobj, type); } else @@ -3122,12 +3495,12 @@ private static Func GetTypeDeserializerImpl( } var explicitConstr = typeMap.FindExplicitConstructor(); - if (explicitConstr != null) + if (explicitConstr is not null) { var consPs = explicitConstr.GetParameters(); foreach (var p in consPs) { - if (!p.ParameterType.IsValueType()) + if (!p.ParameterType.IsValueType) { il.Emit(OpCodes.Ldnull); } @@ -3138,20 +3511,18 @@ private static Func GetTypeDeserializerImpl( } il.Emit(OpCodes.Newobj, explicitConstr); - il.Emit(OpCodes.Stloc_1); -#if !NETSTANDARD1_3 + il.Emit(OpCodes.Stloc, returnValueLocal); supportInitialize = typeof(ISupportInitialize).IsAssignableFrom(type); if (supportInitialize) { - il.Emit(OpCodes.Ldloc_1); - il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.BeginInit)), null); + il.Emit(OpCodes.Ldloc, returnValueLocal); + il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.BeginInit))!, null); } -#endif } else { var ctor = typeMap.FindConstructor(names, types); - if (ctor == null) + if (ctor is null) { string proposedTypes = "(" + string.Join(", ", types.Select((t, i) => t.FullName + " " + names[i]).ToArray()) + ")"; throw new InvalidOperationException($"A parameterless default constructor or one matching signature {proposedTypes} is required for {type.FullName} materialization"); @@ -3160,15 +3531,13 @@ private static Func GetTypeDeserializerImpl( if (ctor.GetParameters().Length == 0) { il.Emit(OpCodes.Newobj, ctor); - il.Emit(OpCodes.Stloc_1); -#if !NETSTANDARD1_3 + il.Emit(OpCodes.Stloc, returnValueLocal); supportInitialize = typeof(ISupportInitialize).IsAssignableFrom(type); if (supportInitialize) { - il.Emit(OpCodes.Ldloc_1); - il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.BeginInit)), null); + il.Emit(OpCodes.Ldloc, returnValueLocal); + il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.BeginInit))!, null); } -#endif } else { @@ -3178,157 +3547,65 @@ private static Func GetTypeDeserializerImpl( } il.BeginExceptionBlock(); - if (type.IsValueType()) + if (type.IsValueType) { - il.Emit(OpCodes.Ldloca_S, (byte)1);// [target] + il.Emit(OpCodes.Ldloca, returnValueLocal); // [target] } - else if (specializedConstructor == null) + else if (specializedConstructor is null) { - il.Emit(OpCodes.Ldloc_1);// [target] + il.Emit(OpCodes.Ldloc, returnValueLocal); // [target] } - var members = IsValueTuple(type) ? GetValueTupleMembers(type, names) : ((specializedConstructor != null - ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) - : names.Select(n => typeMap.GetMember(n))).ToList()); + var members = Array.ConvertAll(names, specializedConstructor is not null + ? n => typeMap.GetConstructorParameter(specializedConstructor, n) + : n => typeMap.GetMember(n)); // stack is now [target] - bool first = true; var allDone = il.DefineLabel(); - int enumDeclareLocal = -1, valueCopyLocal = il.DeclareLocal(typeof(object)).LocalIndex; + var stringEnumLocal = (LocalBuilder?)null; + var valueCopyDiagnosticLocal = il.DeclareLocal(typeof(object)); bool applyNullSetting = Settings.ApplyNullValues; foreach (var item in members) { - if (item != null) + if (item is not null) { - if (specializedConstructor == null) + if (specializedConstructor is null) il.Emit(OpCodes.Dup); // stack is now [target][target] - Label isDbNullLabel = il.DefineLabel(); Label finishLabel = il.DefineLabel(); - - il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader] - EmitInt32(il, index); // stack is now [target][target][reader][index] - il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index] - il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index] - il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object] - il.Emit(OpCodes.Dup); // stack is now [target][target][value-as-object][value-as-object] - StoreLocal(il, valueCopyLocal); - Type colType = reader.GetFieldType(index); Type memberType = item.MemberType; - if (memberType == typeof(char) || memberType == typeof(char?)) - { - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod( - memberType == typeof(char) ? nameof(SqlMapper.ReadChar) : nameof(SqlMapper.ReadNullableChar), BindingFlags.Static | BindingFlags.Public), null); // stack is now [target][target][typed-value] - } - else - { - il.Emit(OpCodes.Dup); // stack is now [target][target][value][value] - il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null] - il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object] - - // unbox nullable enums as the primitive, i.e. byte etc + // Save off the current index for access if an exception is thrown + EmitInt32(il, index); + il.Emit(OpCodes.Stloc, currentIndexDiagnosticLocal); - var nullUnderlyingType = Nullable.GetUnderlyingType(memberType); - var unboxType = nullUnderlyingType?.IsEnum() == true ? nullUnderlyingType : memberType; - - if (unboxType.IsEnum()) - { - Type numericType = Enum.GetUnderlyingType(unboxType); - if (colType == typeof(string)) - { - if (enumDeclareLocal == -1) - { - enumDeclareLocal = il.DeclareLocal(typeof(string)).LocalIndex; - } - il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [target][target][string] - StoreLocal(il, enumDeclareLocal); // stack is now [target][target] - il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][enum-type-token] - il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)), null);// stack is now [target][target][enum-type] - LoadLocal(il, enumDeclareLocal); // stack is now [target][target][enum-type][string] - il.Emit(OpCodes.Ldc_I4_1); // stack is now [target][target][enum-type][string][true] - il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [target][target][enum-as-object] - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] - } - else - { - FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType); - } + LoadReaderValueOrBranchToDBNullLabel(il, index, ref stringEnumLocal, valueCopyDiagnosticLocal, reader.GetFieldType(index), memberType, out var isDbNullLabel, out bool popWhenNull); - if (nullUnderlyingType != null) - { - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][typed-value] - } - } - else if (memberType.FullName == LinqBinary) - { - il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [target][target][byte-array] - il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) }));// stack is now [target][target][binary] - } - else - { - TypeCode dataTypeCode = TypeExtensions.GetTypeCode(colType), unboxTypeCode = TypeExtensions.GetTypeCode(unboxType); - bool hasTypeHandler; - if ((hasTypeHandler = typeHandlers.ContainsKey(unboxType)) || colType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == TypeExtensions.GetTypeCode(nullUnderlyingType)) - { - if (hasTypeHandler) - { -#pragma warning disable 618 - il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache.Parse)), null); // stack is now [target][target][typed-value] -#pragma warning restore 618 - } - else - { - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] - } - } - else - { - // not a direct match; need to tweak the unbox - FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null); - if (nullUnderlyingType != null) - { - il.Emit(OpCodes.Newobj, unboxType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][typed-value] - } - } - } - } - if (specializedConstructor == null) + if (specializedConstructor is null) { // Store the value in the property/field - if (item.Property != null) + if (item.Property is not null) { - il.Emit(type.IsValueType() ? OpCodes.Call : OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); + il.Emit(type.IsValueType ? OpCodes.Call : OpCodes.Callvirt, DefaultTypeMap.GetPropertySetterOrThrow(item.Property, type)); } else { - il.Emit(OpCodes.Stfld, item.Field); // stack is now [target] + il.Emit(OpCodes.Stfld, item.Field!); // stack is now [target] } } il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target] - il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value] - if (specializedConstructor != null) + il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][(and possibly value)] + if (popWhenNull) il.Emit(OpCodes.Pop); // stack is now [target][target] + if (specializedConstructor is not null) { - il.Emit(OpCodes.Pop); - if (item.MemberType.IsValueType()) - { - int localIndex = il.DeclareLocal(item.MemberType).LocalIndex; - LoadLocalAddress(il, localIndex); - il.Emit(OpCodes.Initobj, item.MemberType); - LoadLocal(il, localIndex); - } - else - { - il.Emit(OpCodes.Ldnull); - } + LoadDefaultValue(il, item.MemberType); } - else if (applyNullSetting && (!memberType.IsValueType() || Nullable.GetUnderlyingType(memberType) != null)) + else if (applyNullSetting && (!memberType.IsValueType || Nullable.GetUnderlyingType(memberType) is not null)) { - il.Emit(OpCodes.Pop); // stack is now [target][target] // can load a null with this value - if (memberType.IsValueType()) + if (memberType.IsValueType) { // must be Nullable for some T GetTempLocal(il, ref structLocals, memberType, true); // stack is now [target][target][null] } @@ -3338,27 +3615,29 @@ private static Func GetTypeDeserializerImpl( } // Store the value in the property/field - if (item.Property != null) + if (item.Property is not null) { - il.Emit(type.IsValueType() ? OpCodes.Call : OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); + il.Emit(type.IsValueType ? OpCodes.Call : OpCodes.Callvirt, DefaultTypeMap.GetPropertySetterOrThrow(item.Property, type)); // stack is now [target] } else { - il.Emit(OpCodes.Stfld, item.Field); // stack is now [target] + il.Emit(OpCodes.Stfld, item.Field!); // stack is now [target] } } else { - il.Emit(OpCodes.Pop); // stack is now [target][target] il.Emit(OpCodes.Pop); // stack is now [target] } if (first && returnNullIfFirstMissing) { il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldnull); // stack is now [null] - il.Emit(OpCodes.Stloc_1); + if (!type.IsValueType) // for struct, the retval is already initialized as default + { + il.Emit(OpCodes.Ldnull); // stack is now [null] + il.Emit(OpCodes.Stloc, returnValueLocal); + } il.Emit(OpCodes.Br, allDone); } @@ -3367,52 +3646,199 @@ private static Func GetTypeDeserializerImpl( first = false; index++; } - if (type.IsValueType()) + if (type.IsValueType) { il.Emit(OpCodes.Pop); } else { - if (specializedConstructor != null) + if (specializedConstructor is not null) { il.Emit(OpCodes.Newobj, specializedConstructor); } - il.Emit(OpCodes.Stloc_1); // stack is empty -#if !NETSTANDARD1_3 + il.Emit(OpCodes.Stloc, returnValueLocal); // stack is empty if (supportInitialize) { - il.Emit(OpCodes.Ldloc_1); - il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.EndInit)), null); + il.Emit(OpCodes.Ldloc, returnValueLocal); + il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.EndInit))!, null); } -#endif } il.MarkLabel(allDone); il.BeginCatchBlock(typeof(Exception)); // stack is Exception - il.Emit(OpCodes.Ldloc_0); // stack is Exception, index + il.Emit(OpCodes.Ldloc, currentIndexDiagnosticLocal); // stack is Exception, index il.Emit(OpCodes.Ldarg_0); // stack is Exception, index, reader - LoadLocal(il, valueCopyLocal); // stack is Exception, index, reader, value - il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.ThrowDataException)), null); + il.Emit(OpCodes.Ldloc, valueCopyDiagnosticLocal); // stack is Exception, index, reader, value + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.ThrowDataException))!, null); il.EndExceptionBlock(); - il.Emit(OpCodes.Ldloc_1); // stack is [rval] - if (type.IsValueType()) + il.Emit(OpCodes.Ldloc, returnValueLocal); // stack is [rval] + if (type.IsValueType) { il.Emit(OpCodes.Box, type); } il.Emit(OpCodes.Ret); + } - var funcType = System.Linq.Expressions.Expression.GetFuncType(typeof(IDataReader), returnType); - return (Func)dm.CreateDelegate(funcType); + private static void LoadDefaultValue(ILGenerator il, Type type) + { + if (type.IsValueType) + { + var local = il.DeclareLocal(type); + il.Emit(OpCodes.Ldloca, local); + il.Emit(OpCodes.Initobj, type); + il.Emit(OpCodes.Ldloc, local); + } + else + { + il.Emit(OpCodes.Ldnull); + } } - private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type from, Type to, Type via) + private static void LoadReaderValueViaGetFieldValue(ILGenerator il, int index, Type memberType, LocalBuilder? valueCopyLocal, Label isDbNullLabel, out bool popWhenNull) { - MethodInfo op; + popWhenNull = false; + var underlyingType = Nullable.GetUnderlyingType(memberType) ?? memberType; + + // for consistency, always do a null check (the GetValue approach always tests for DbNull and jumps) + il.Emit(OpCodes.Ldarg_0); // stack is now [...][reader] + EmitInt32(il, index); // stack is now [...][reader][index] + il.Emit(OpCodes.Callvirt, isDbNull); // stack is now [...][bool] + il.Emit(OpCodes.Brtrue_S, isDbNullLabel); + + // DB reports not null; read the value + il.Emit(OpCodes.Ldarg_0); // stack is now [...][reader] + EmitInt32(il, index); // stack is now [...][reader][index] + il.Emit(OpCodes.Callvirt, getFieldValueT.MakeGenericMethod(underlyingType)); // stack is now [...][T] + if (valueCopyLocal is not null) + { + il.Emit(OpCodes.Dup); // stack is now [...][T][T] + if (underlyingType.IsValueType) + { + il.Emit(OpCodes.Box, underlyingType); // stack is now [...][T][value-as-object] + } + il.Emit(OpCodes.Stloc, valueCopyLocal); // stack is now [...][T] + } + if (underlyingType != memberType) + { + // Nullable; wrap it + il.Emit(OpCodes.Newobj, memberType.GetConstructor([underlyingType])!); // stack is now [...][T?] + } + } + + private static void LoadReaderValueOrBranchToDBNullLabel(ILGenerator il, int index, ref LocalBuilder? stringEnumLocal, LocalBuilder? valueCopyLocal, Type colType, Type memberType, out Label isDbNullLabel, out bool popWhenNull) + { + isDbNullLabel = il.DefineLabel(); + if (UseGetFieldValue(memberType)) + { + LoadReaderValueViaGetFieldValue(il, index, memberType, valueCopyLocal, isDbNullLabel, out popWhenNull); + return; + } + + popWhenNull = true; + il.Emit(OpCodes.Ldarg_0); // stack is now [...][reader] + EmitInt32(il, index); // stack is now [...][reader][index] + // default impl: use GetValue + il.Emit(OpCodes.Callvirt, getItem); // stack is now [...][value-as-object] + + if (valueCopyLocal is not null) + { + il.Emit(OpCodes.Dup); // stack is now [...][value-as-object][value-as-object] + il.Emit(OpCodes.Stloc, valueCopyLocal); // stack is now [...][value-as-object] + } + + if (memberType == typeof(char) || memberType == typeof(char?)) + { + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod( + memberType == typeof(char) ? nameof(SqlMapper.ReadChar) : nameof(SqlMapper.ReadNullableChar), BindingFlags.Static | BindingFlags.Public)!, null); // stack is now [...][typed-value] + } + else + { + il.Emit(OpCodes.Dup); // stack is now [...][value-as-object][value-as-object] + il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [...][value-as-object][DBNull or null] + il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [...][value-as-object] + + // unbox nullable enums as the primitive, i.e. byte etc + + var nullUnderlyingType = Nullable.GetUnderlyingType(memberType); + var unboxType = nullUnderlyingType?.IsEnum == true ? nullUnderlyingType : memberType; + + if (unboxType.IsEnum) + { + if (Settings.PreferTypeHandlersForEnums && typeHandlers.ContainsKey(unboxType)) + { +#pragma warning disable 618 + il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache.Parse))!, null); // stack is now [...][typed-value] +#pragma warning restore 618 + } + else + { + Type numericType = Enum.GetUnderlyingType(unboxType); + if (colType == typeof(string)) + { + stringEnumLocal ??= il.DeclareLocal(typeof(string)); + il.Emit(OpCodes.Castclass, typeof(string)); // stack is now [...][string] + il.Emit(OpCodes.Stloc, stringEnumLocal); // stack is now [...] + il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [...][enum-type-token] + il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null);// stack is now [...][enum-type] + il.Emit(OpCodes.Ldloc, stringEnumLocal); // stack is now [...][enum-type][string] + il.Emit(OpCodes.Ldc_I4_1); // stack is now [...][enum-type][string][true] + il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [...][enum-as-object] + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [...][typed-value] + } + else + { + FlexibleConvertBoxedFromHeadOfStack(il, colType, unboxType, numericType); + } + } + + if (nullUnderlyingType is not null) + { + il.Emit(OpCodes.Newobj, memberType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value] + } + } + else if (memberType.FullName == LinqBinary) + { + il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [...][byte-array] + il.Emit(OpCodes.Newobj, memberType.GetConstructor([typeof(byte[])])!);// stack is now [...][binary] + } + else + { + TypeCode dataTypeCode = Type.GetTypeCode(colType), unboxTypeCode = Type.GetTypeCode(unboxType); + bool hasTypeHandler; + if ((hasTypeHandler = typeHandlers.ContainsKey(unboxType)) || colType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == Type.GetTypeCode(nullUnderlyingType)) + { + if (hasTypeHandler) + { +#pragma warning disable 618 + il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod(nameof(TypeHandlerCache.Parse))!, null); // stack is now [...][typed-value] +#pragma warning restore 618 + } + else + { + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [...][typed-value] + } + } + else + { + // not a direct match; need to tweak the unbox + FlexibleConvertBoxedFromHeadOfStack(il, colType, nullUnderlyingType ?? unboxType, null); + if (nullUnderlyingType is not null) + { + il.Emit(OpCodes.Newobj, unboxType.GetConstructor([nullUnderlyingType])!); // stack is now [...][typed-value] + } + } + } + } + } + + private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type from, Type to, Type? via) + { + MethodInfo? op; if (from == (via ?? to)) { il.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value] } - else if ((op = GetOperator(from, to)) != null) + else if ((op = GetOperator(from, to)) is not null) { // this is handy for things like decimal <===> double il.Emit(OpCodes.Unbox_Any, from); // stack is now [target][target][data-typed-value] @@ -3421,8 +3847,8 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro else { bool handled = false; - OpCode opCode = default(OpCode); - switch (TypeExtensions.GetTypeCode(from)) + OpCode opCode = default; + switch (Type.GetTypeCode(from)) { case TypeCode.Boolean: case TypeCode.Byte: @@ -3436,23 +3862,23 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro case TypeCode.Single: case TypeCode.Double: handled = true; - switch (TypeExtensions.GetTypeCode(via ?? to)) + switch (Type.GetTypeCode(via ?? to)) { case TypeCode.Byte: - opCode = OpCodes.Conv_Ovf_I1_Un; break; + opCode = OpCodes.Conv_Ovf_U1_Un; break; case TypeCode.SByte: opCode = OpCodes.Conv_Ovf_I1; break; case TypeCode.UInt16: - opCode = OpCodes.Conv_Ovf_I2_Un; break; + opCode = OpCodes.Conv_Ovf_U2_Un; break; case TypeCode.Int16: opCode = OpCodes.Conv_Ovf_I2; break; case TypeCode.UInt32: - opCode = OpCodes.Conv_Ovf_I4_Un; break; + opCode = OpCodes.Conv_Ovf_U4_Un; break; case TypeCode.Boolean: // boolean is basically an int, at least at this level case TypeCode.Int32: opCode = OpCodes.Conv_Ovf_I4; break; case TypeCode.UInt64: - opCode = OpCodes.Conv_Ovf_I8_Un; break; + opCode = OpCodes.Conv_Ovf_U8_Un; break; case TypeCode.Int64: opCode = OpCodes.Conv_Ovf_I8; break; case TypeCode.Single: @@ -3480,16 +3906,17 @@ private static void FlexibleConvertBoxedFromHeadOfStack(ILGenerator il, Type fro else { il.Emit(OpCodes.Ldtoken, via ?? to); // stack is now [target][target][value][member-type-token] - il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle)), null); // stack is now [target][target][value][member-type] - il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), new Type[] { typeof(object), typeof(Type) }), null); // stack is now [target][target][boxed-member-type-value] + il.EmitCall(OpCodes.Call, typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!, null); // stack is now [target][target][value][member-type] + il.EmitCall(OpCodes.Call, InvariantCulture, null); // stack is now [target][target][value][member-type][culture] + il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod(nameof(Convert.ChangeType), [typeof(object), typeof(Type), typeof(IFormatProvider)])!, null); // stack is now [target][target][boxed-member-type-value] il.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value] } } } - private static MethodInfo GetOperator(Type from, Type to) + private static MethodInfo? GetOperator(Type from, Type to) { - if (to == null) return null; + if (to is null) return null; MethodInfo[] fromMethods, toMethods; return ResolveOperator(fromMethods = from.GetMethods(BindingFlags.Static | BindingFlags.Public), from, to, "op_Implicit") ?? ResolveOperator(toMethods = to.GetMethods(BindingFlags.Static | BindingFlags.Public), from, to, "op_Implicit") @@ -3497,7 +3924,7 @@ private static MethodInfo GetOperator(Type from, Type to) ?? ResolveOperator(toMethods, from, to, "op_Explicit"); } - private static MethodInfo ResolveOperator(MethodInfo[] methods, Type from, Type to, string name) + private static MethodInfo? ResolveOperator(MethodInfo[] methods, Type from, Type to, string name) { for (int i = 0; i < methods.Length; i++) { @@ -3509,90 +3936,55 @@ private static MethodInfo ResolveOperator(MethodInfo[] methods, Type from, Type return null; } - private static void LoadLocal(ILGenerator il, int index) - { - if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException(nameof(index)); - switch (index) - { - case 0: il.Emit(OpCodes.Ldloc_0); break; - case 1: il.Emit(OpCodes.Ldloc_1); break; - case 2: il.Emit(OpCodes.Ldloc_2); break; - case 3: il.Emit(OpCodes.Ldloc_3); break; - default: - if (index <= 255) - { - il.Emit(OpCodes.Ldloc_S, (byte)index); - } - else - { - il.Emit(OpCodes.Ldloc, (short)index); - } - break; - } - } - - private static void StoreLocal(ILGenerator il, int index) - { - if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException(nameof(index)); - switch (index) - { - case 0: il.Emit(OpCodes.Stloc_0); break; - case 1: il.Emit(OpCodes.Stloc_1); break; - case 2: il.Emit(OpCodes.Stloc_2); break; - case 3: il.Emit(OpCodes.Stloc_3); break; - default: - if (index <= 255) - { - il.Emit(OpCodes.Stloc_S, (byte)index); - } - else - { - il.Emit(OpCodes.Stloc, (short)index); - } - break; - } - } - - private static void LoadLocalAddress(ILGenerator il, int index) - { - if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException(nameof(index)); - - if (index <= 255) - { - il.Emit(OpCodes.Ldloca_S, (byte)index); - } - else - { - il.Emit(OpCodes.Ldloca, (short)index); - } - } + /// + /// For internal use only + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static void ThrowNullCustomQueryParameter(string name) + => throw new InvalidOperationException($"Member '{name}' is an {nameof(ICustomQueryParameter)} and cannot be null"); /// /// Throws a data exception, only used internally /// /// The exception to throw. - /// The index the exception occured at. - /// The reader the exception occured in. + /// The index the exception occurred at. + /// The reader the exception occurred in. /// The value that caused the exception. [Obsolete(ObsoleteInternalUsageOnly, false)] - public static void ThrowDataException(Exception ex, int index, IDataReader reader, object value) + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static void ThrowDataException(Exception ex, int index, IDataReader reader, object? value) { Exception toThrow; try { string name = "(n/a)", formattedValue = "(n/a)"; - if (reader != null && index >= 0 && index < reader.FieldCount) + if (reader is not null && index >= 0 && index < reader.FieldCount) { name = reader.GetName(index); + if (name == string.Empty) + { + // Otherwise we throw (=value) below, which isn't intuitive + name = "(Unnamed Column)"; + } try { - if (value == null || value is DBNull) + if (value is null && ex is InvalidCastException) + { + formattedValue = "n/a - " + ex.Message; // provide some context + } + else if (value is null || value is DBNull) { formattedValue = ""; } else { - formattedValue = Convert.ToString(value) + " - " + TypeExtensions.GetTypeCode(value.GetType()); + formattedValue = Convert.ToString(value) + " - " + Identify(value.GetType()); + } + static string Identify(Type type) + { + var tc = Type.GetTypeCode(type); + return tc == TypeCode.Object ? type.Name : tc.ToString(); } } catch (Exception valEx) @@ -3650,7 +4042,6 @@ public static IEqualityComparer ConnectionStringComparer private static IEqualityComparer connectionStringComparer = StringComparer.Ordinal; -#if !NETSTANDARD1_3 /// /// Key used to indicate the type name associated with a DataTable. /// @@ -3661,7 +4052,8 @@ public static IEqualityComparer ConnectionStringComparer /// /// The to create this parameter for. /// The name of the type this parameter is for. - public static ICustomQueryParameter AsTableValuedParameter(this DataTable table, string typeName = null) => + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static ICustomQueryParameter AsTableValuedParameter(this DataTable table, string? typeName = null) => new TableValuedParameter(table, typeName); /// @@ -3671,7 +4063,7 @@ public static ICustomQueryParameter AsTableValuedParameter(this DataTable table, /// The name of the type this table is for. public static void SetTypeName(this DataTable table, string typeName) { - if (table != null) + if (table is not null) { if (string.IsNullOrEmpty(typeName)) table.ExtendedProperties.Remove(DataTableTypeNameKey); @@ -3684,25 +4076,25 @@ public static void SetTypeName(this DataTable table, string typeName) /// Fetch the type name associated with a . /// /// The that has a type name associated with it. - public static string GetTypeName(this DataTable table) => + public static string? GetTypeName(this DataTable table) => table?.ExtendedProperties[DataTableTypeNameKey] as string; -#endif /// /// Used to pass a IEnumerable<SqlDataRecord> as a TableValuedParameter. /// /// The list of records to convert to TVPs. /// The sql parameter type name. - public static ICustomQueryParameter AsTableValuedParameter(this IEnumerable list, string typeName = null) => - new SqlDataRecordListTVPParameter(list, typeName); + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Grandfathered")] + public static ICustomQueryParameter AsTableValuedParameter(this IEnumerable list, string? typeName = null) where T : IDataRecord => + new SqlDataRecordListTVPParameter(list, typeName!); // one per thread [ThreadStatic] - private static StringBuilder perThreadStringBuilderCache; + private static StringBuilder? perThreadStringBuilderCache; private static StringBuilder GetStringBuilder() { var tmp = perThreadStringBuilderCache; - if (tmp != null) + if (tmp is not null) { perThreadStringBuilderCache = null; tmp.Length = 0; @@ -3711,11 +4103,11 @@ private static StringBuilder GetStringBuilder() return new StringBuilder(); } - private static string __ToStringRecycle(this StringBuilder obj) + private static string ToStringRecycle(this StringBuilder obj) { - if (obj == null) return ""; + if (obj is null) return ""; var s = obj.ToString(); - perThreadStringBuilderCache = perThreadStringBuilderCache ?? obj; + perThreadStringBuilderCache ??= obj; return s; } } diff --git a/Dapper/TableValuedParameter.cs b/Dapper/TableValuedParameter.cs index 3d307e2df..0c4e40b71 100644 --- a/Dapper/TableValuedParameter.cs +++ b/Dapper/TableValuedParameter.cs @@ -1,8 +1,5 @@ -using System; -using System.Data; -using System.Reflection; +using System.Data; -#if !NETSTANDARD1_3 namespace Dapper { /// @@ -11,7 +8,7 @@ namespace Dapper internal sealed class TableValuedParameter : SqlMapper.ICustomQueryParameter { private readonly DataTable table; - private readonly string typeName; + private readonly string? typeName; /// /// Create a new instance of . @@ -24,23 +21,12 @@ public TableValuedParameter(DataTable table) : this(table, null) { /* run base * /// /// The to create this parameter for. /// The name of the type this parameter is for. - public TableValuedParameter(DataTable table, string typeName) + public TableValuedParameter(DataTable table, string? typeName) { this.table = table; this.typeName = typeName; } - private static readonly Action setTypeName; - static TableValuedParameter() - { - var prop = typeof(System.Data.SqlClient.SqlParameter).GetProperty("TypeName", BindingFlags.Instance | BindingFlags.Public); - if (prop != null && prop.PropertyType == typeof(string) && prop.CanWrite) - { - setTypeName = (Action) - Delegate.CreateDelegate(typeof(Action), prop.GetSetMethod()); - } - } - void SqlMapper.ICustomQueryParameter.AddParameter(IDbCommand command, string name) { var param = command.CreateParameter(); @@ -49,21 +35,16 @@ void SqlMapper.ICustomQueryParameter.AddParameter(IDbCommand command, string nam command.Parameters.Add(param); } - internal static void Set(IDbDataParameter parameter, DataTable table, string typeName) + internal static void Set(IDbDataParameter parameter, DataTable? table, string? typeName) { #pragma warning disable 0618 parameter.Value = SqlMapper.SanitizeParameterValue(table); #pragma warning restore 0618 - if (string.IsNullOrEmpty(typeName) && table != null) + if (string.IsNullOrEmpty(typeName) && table is not null) { typeName = table.GetTypeName(); } - if (!string.IsNullOrEmpty(typeName) && (parameter is System.Data.SqlClient.SqlParameter sqlParam)) - { - setTypeName?.Invoke(sqlParam, typeName); - sqlParam.SqlDbType = SqlDbType.Structured; - } + if (!string.IsNullOrEmpty(typeName)) StructuredHelper.ConfigureTVP(parameter, typeName); } } } -#endif \ No newline at end of file diff --git a/Dapper/TypeExtensions.cs b/Dapper/TypeExtensions.cs index 81ac91f2f..eb0efcbc9 100644 --- a/Dapper/TypeExtensions.cs +++ b/Dapper/TypeExtensions.cs @@ -1,96 +1,11 @@ using System; using System.Reflection; -using System.Collections.Generic; namespace Dapper { internal static class TypeExtensions { - public static string Name(this Type type) => -#if NETSTANDARD1_3 || NETCOREAPP1_0 - type.GetTypeInfo().Name; -#else - type.Name; -#endif - - public static bool IsValueType(this Type type) => -#if NETSTANDARD1_3 || NETCOREAPP1_0 - type.GetTypeInfo().IsValueType; -#else - type.IsValueType; -#endif - - public static bool IsEnum(this Type type) => -#if NETSTANDARD1_3 || NETCOREAPP1_0 - type.GetTypeInfo().IsEnum; -#else - type.IsEnum; -#endif - - public static bool IsGenericType(this Type type) => -#if NETSTANDARD1_3 || NETCOREAPP1_0 - type.GetTypeInfo().IsGenericType; -#else - type.IsGenericType; -#endif - - public static bool IsInterface(this Type type) => -#if NETSTANDARD1_3 || NETCOREAPP1_0 - type.GetTypeInfo().IsInterface; -#else - type.IsInterface; -#endif - -#if NETSTANDARD1_3 || NETCOREAPP1_0 - public static IEnumerable GetCustomAttributes(this Type type, bool inherit) - { - return type.GetTypeInfo().GetCustomAttributes(inherit); - } - - public static TypeCode GetTypeCode(Type type) - { - if (type == null) return TypeCode.Empty; - if (typeCodeLookup.TryGetValue(type, out TypeCode result)) return result; - - if (type.IsEnum()) - { - type = Enum.GetUnderlyingType(type); - if (typeCodeLookup.TryGetValue(type, out result)) return result; - } - return TypeCode.Object; - } - - private static readonly Dictionary typeCodeLookup = new Dictionary - { - [typeof(bool)] = TypeCode.Boolean, - [typeof(byte)] = TypeCode.Byte, - [typeof(char)] = TypeCode.Char, - [typeof(DateTime)] = TypeCode.DateTime, - [typeof(decimal)] = TypeCode.Decimal, - [typeof(double)] = TypeCode.Double, - [typeof(short)] = TypeCode.Int16, - [typeof(int)] = TypeCode.Int32, - [typeof(long)] = TypeCode.Int64, - [typeof(object)] = TypeCode.Object, - [typeof(sbyte)] = TypeCode.SByte, - [typeof(float)] = TypeCode.Single, - [typeof(string)] = TypeCode.String, - [typeof(ushort)] = TypeCode.UInt16, - [typeof(uint)] = TypeCode.UInt32, - [typeof(ulong)] = TypeCode.UInt64, - }; -#else - public static TypeCode GetTypeCode(Type type) => Type.GetTypeCode(type); -#endif - - public static MethodInfo GetPublicInstanceMethod(this Type type, string name, Type[] types) - { -#if NETSTANDARD1_3 || NETCOREAPP1_0 - var method = type.GetMethod(name, types); - return (method?.IsPublic == true && !method.IsStatic) ? method : null; -#else - return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public, null, types, null); -#endif - } + public static MethodInfo? GetPublicInstanceMethod(this Type type, string name, Type[] types) + => type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public, null, types, null); } } diff --git a/Dapper/UdtTypeHandler.cs b/Dapper/UdtTypeHandler.cs index c0a3a1872..503485927 100644 --- a/Dapper/UdtTypeHandler.cs +++ b/Dapper/UdtTypeHandler.cs @@ -5,7 +5,6 @@ namespace Dapper { public static partial class SqlMapper { -#if !NETSTANDARD1_3 && !NETSTANDARD2_0 /// /// A type handler for data-types that are supported by the underlying provider, but which need /// a well-known UdtTypeName to be specified @@ -23,7 +22,7 @@ public UdtTypeHandler(string udtTypeName) this.udtTypeName = udtTypeName; } - object ITypeHandler.Parse(Type destinationType, object value) + object? ITypeHandler.Parse(Type destinationType, object value) { return value is DBNull ? null : value; } @@ -33,13 +32,8 @@ void ITypeHandler.SetValue(IDbDataParameter parameter, object value) #pragma warning disable 0618 parameter.Value = SanitizeParameterValue(value); #pragma warning restore 0618 - if (parameter is System.Data.SqlClient.SqlParameter && !(value is DBNull)) - { - ((System.Data.SqlClient.SqlParameter)parameter).SqlDbType = SqlDbType.Udt; - ((System.Data.SqlClient.SqlParameter)parameter).UdtTypeName = udtTypeName; - } + if(!(value is DBNull)) StructuredHelper.ConfigureUDT(parameter, udtTypeName); } } -#endif } } diff --git a/Dapper/WrappedReader.cs b/Dapper/WrappedReader.cs index a17d783af..04321f976 100644 --- a/Dapper/WrappedReader.cs +++ b/Dapper/WrappedReader.cs @@ -1,112 +1,364 @@ using System; +using System.Collections; +using System.Collections.ObjectModel; using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; namespace Dapper { - internal class WrappedReader : IWrappedDataReader + internal sealed class DisposedReader : DbDataReader { - private IDataReader reader; - private IDbCommand cmd; + internal static readonly DisposedReader Instance = new(); + private DisposedReader() { } + public override int Depth => 0; + public override int FieldCount => 0; + public override bool IsClosed => true; + public override bool HasRows => false; + public override int RecordsAffected => -1; + public override int VisibleFieldCount => 0; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T ThrowDisposed() => throw new ObjectDisposedException(nameof(DbDataReader)); + [MethodImpl(MethodImplOptions.NoInlining)] + private async static Task ThrowDisposedAsync() + { + var result = ThrowDisposed(); + await Task.Yield(); // will never hit this - already thrown and handled + return result; + } + public override void Close() { } + public override DataTable GetSchemaTable() => ThrowDisposed(); + +#if NET5_0_OR_GREATER + [Obsolete("This Remoting API is not supported and throws PlatformNotSupportedException.", DiagnosticId = "SYSLIB0010", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] +#endif + public override object InitializeLifetimeService() => ThrowDisposed(); + protected override void Dispose(bool disposing) { } + public override bool GetBoolean(int ordinal) => ThrowDisposed(); + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => ThrowDisposed(); + public override float GetFloat(int ordinal) => ThrowDisposed(); + public override short GetInt16(int ordinal) => ThrowDisposed(); + public override byte GetByte(int ordinal) => ThrowDisposed(); + public override char GetChar(int ordinal) => ThrowDisposed(); + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => ThrowDisposed(); + public override string GetDataTypeName(int ordinal) => ThrowDisposed(); + public override DateTime GetDateTime(int ordinal) => ThrowDisposed(); + protected override DbDataReader GetDbDataReader(int ordinal) => ThrowDisposed(); + public override decimal GetDecimal(int ordinal) => ThrowDisposed(); + public override double GetDouble(int ordinal) => ThrowDisposed(); + public override IEnumerator GetEnumerator() => ThrowDisposed(); + public override Type GetFieldType(int ordinal) => ThrowDisposed(); + public override T GetFieldValue(int ordinal) => ThrowDisposed(); + public override Task GetFieldValueAsync(int ordinal, CancellationToken cancellationToken) => ThrowDisposedAsync(); + public override Guid GetGuid(int ordinal) => ThrowDisposed(); + public override int GetInt32(int ordinal) => ThrowDisposed(); + public override long GetInt64(int ordinal) => ThrowDisposed(); + public override string GetName(int ordinal) => ThrowDisposed(); + public override int GetOrdinal(string name) => ThrowDisposed(); + public override Type GetProviderSpecificFieldType(int ordinal) => ThrowDisposed(); + public override object GetProviderSpecificValue(int ordinal) => ThrowDisposed(); + public override int GetProviderSpecificValues(object[] values) => ThrowDisposed(); + public override Stream GetStream(int ordinal) => ThrowDisposed(); + public override string GetString(int ordinal) => ThrowDisposed(); + public override TextReader GetTextReader(int ordinal) => ThrowDisposed(); + public override object GetValue(int ordinal) => ThrowDisposed(); + public override int GetValues(object[] values) => ThrowDisposed(); + public override bool IsDBNull(int ordinal) => ThrowDisposed(); + public override Task IsDBNullAsync(int ordinal, CancellationToken cancellationToken) => ThrowDisposedAsync(); + public override bool NextResult() => ThrowDisposed(); + public override bool Read() => ThrowDisposed(); + public override Task NextResultAsync(CancellationToken cancellationToken) => ThrowDisposedAsync(); + public override Task ReadAsync(CancellationToken cancellationToken) => ThrowDisposedAsync(); + public override object this[int ordinal] => ThrowDisposed(); + public override object this[string name] => ThrowDisposed(); + } - public IDataReader Reader + internal sealed class DbWrappedReader : DbDataReader, IWrappedDataReader + { + // the purpose of wrapping here is to allow closing a reader to *also* close + // the command, without having to explicitly hand the command back to the + // caller + public static DbDataReader Create(IDbCommand? cmd, DbDataReader reader) { - get - { - var tmp = reader; - if (tmp == null) throw new ObjectDisposedException(GetType().Name); - return tmp; - } + if (cmd is null) return reader; // no need to wrap if no command + + if (reader is not null) return new DbWrappedReader(cmd, reader); + cmd.Dispose(); + return null!; // GIGO } - IDbCommand IWrappedDataReader.Command + private DbDataReader _reader; + private IDbCommand _cmd; + + IDataReader IWrappedDataReader.Reader => _reader; + + IDbCommand IWrappedDataReader.Command => _cmd; + + public DbWrappedReader(IDbCommand cmd, DbDataReader reader) { - get + _cmd = cmd; + _reader = reader; + } + + public override bool HasRows => _reader.HasRows; + + public override void Close() => _reader.Close(); + public override DataTable? GetSchemaTable() => _reader.GetSchemaTable(); + +#if NET5_0_OR_GREATER + [Obsolete("This Remoting API is not supported and throws PlatformNotSupportedException.", DiagnosticId = "SYSLIB0010", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] +#endif + public override object InitializeLifetimeService() => _reader.InitializeLifetimeService(); + + public override int Depth => _reader.Depth; + + public override bool IsClosed => _reader.IsClosed; + + public override bool NextResult() => _reader.NextResult(); + + public override bool Read() => _reader.Read(); + + public override int RecordsAffected => _reader.RecordsAffected; + + protected override void Dispose(bool disposing) + { + if (disposing) { - var tmp = cmd; - if (tmp == null) throw new ObjectDisposedException(GetType().Name); - return tmp; + _reader.Dispose(); + _reader = DisposedReader.Instance; // all future ops are no-ops + _cmd?.Dispose(); + _cmd = null!; } } - public WrappedReader(IDbCommand cmd, IDataReader reader) + public override int FieldCount => _reader.FieldCount; + + public override bool GetBoolean(int i) => _reader.GetBoolean(i); + + public override byte GetByte(int i) => _reader.GetByte(i); + + public override long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => + _reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); + + public override char GetChar(int i) => _reader.GetChar(i); + + public override long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => + _reader.GetChars(i, fieldoffset, buffer, bufferoffset, length); + + public override string GetDataTypeName(int i) => _reader.GetDataTypeName(i); + + public override DateTime GetDateTime(int i) => _reader.GetDateTime(i); + + public override decimal GetDecimal(int i) => _reader.GetDecimal(i); + + public override double GetDouble(int i) => _reader.GetDouble(i); + + public override Type GetFieldType(int i) => _reader.GetFieldType(i); + + public override float GetFloat(int i) => _reader.GetFloat(i); + + public override Guid GetGuid(int i) => _reader.GetGuid(i); + + public override short GetInt16(int i) => _reader.GetInt16(i); + + public override int GetInt32(int i) => _reader.GetInt32(i); + + public override long GetInt64(int i) => _reader.GetInt64(i); + + public override string GetName(int i) => _reader.GetName(i); + + public override int GetOrdinal(string name) => _reader.GetOrdinal(name); + + public override string GetString(int i) => _reader.GetString(i); + + public override object GetValue(int i) => _reader.GetValue(i); + + public override int GetValues(object[] values) => _reader.GetValues(values); + + public override bool IsDBNull(int i) => _reader.IsDBNull(i); + + public override object this[string name] => _reader[name]; + + public override object this[int i] => _reader[i]; + + public override T GetFieldValue(int ordinal) => _reader.GetFieldValue(ordinal); + public override Task GetFieldValueAsync(int ordinal, CancellationToken cancellationToken) => _reader.GetFieldValueAsync(ordinal, cancellationToken); + public override IEnumerator GetEnumerator() => _reader.GetEnumerator(); + public override Type GetProviderSpecificFieldType(int ordinal) => _reader.GetProviderSpecificFieldType(ordinal); + public override object GetProviderSpecificValue(int ordinal) => _reader.GetProviderSpecificValue(ordinal); + public override int GetProviderSpecificValues(object[] values) => _reader.GetProviderSpecificValues(values); + public override Stream GetStream(int ordinal) => _reader.GetStream(ordinal); + public override TextReader GetTextReader(int ordinal) => _reader.GetTextReader(ordinal); + public override Task IsDBNullAsync(int ordinal, CancellationToken cancellationToken) => _reader.IsDBNullAsync(ordinal, cancellationToken); + public override Task NextResultAsync(CancellationToken cancellationToken) => _reader.NextResultAsync(cancellationToken); + public override Task ReadAsync(CancellationToken cancellationToken) => _reader.ReadAsync(cancellationToken); + public override int VisibleFieldCount => _reader.VisibleFieldCount; + protected override DbDataReader GetDbDataReader(int ordinal) => _reader.GetData(ordinal); + +#if NET5_0_OR_GREATER + public override Task CloseAsync() => _reader.CloseAsync(); + + public override ValueTask DisposeAsync() => _reader.DisposeAsync(); + + public override Task> GetColumnSchemaAsync(CancellationToken cancellationToken = default) => _reader.GetColumnSchemaAsync(cancellationToken); + + public override Task GetSchemaTableAsync(CancellationToken cancellationToken = default) => base.GetSchemaTableAsync(cancellationToken); +#endif + } + + internal sealed class WrappedBasicReader : DbDataReader + { + private IDataReader _reader; + + public WrappedBasicReader(IDataReader reader) { - this.cmd = cmd; - this.reader = reader; + Debug.Assert(reader is not DbDataReader); // or we wouldn't be here! + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); } - void IDataReader.Close() => reader?.Close(); + public override bool HasRows => true; // have to assume that we do + public override void Close() => _reader.Close(); + public override DataTable? GetSchemaTable() => _reader.GetSchemaTable(); - int IDataReader.Depth => Reader.Depth; +#if NET5_0_OR_GREATER + [Obsolete("This Remoting API is not supported and throws PlatformNotSupportedException.", DiagnosticId = "SYSLIB0010", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] +#endif + public override object InitializeLifetimeService() => throw new NotSupportedException(); - DataTable IDataReader.GetSchemaTable() => Reader.GetSchemaTable(); + public override int Depth => _reader.Depth; - bool IDataReader.IsClosed => reader?.IsClosed ?? true; + public override bool IsClosed => _reader.IsClosed; - bool IDataReader.NextResult() => Reader.NextResult(); + public override bool NextResult() => _reader.NextResult(); - bool IDataReader.Read() => Reader.Read(); + public override bool Read() => _reader.Read(); - int IDataReader.RecordsAffected => Reader.RecordsAffected; + public override int RecordsAffected => _reader.RecordsAffected; - void IDisposable.Dispose() + protected override void Dispose(bool disposing) { - reader?.Close(); - reader?.Dispose(); - reader = null; - cmd?.Dispose(); - cmd = null; + if (disposing) + { + _reader.Dispose(); + _reader = DisposedReader.Instance; // all future ops are no-ops + } } - int IDataRecord.FieldCount => Reader.FieldCount; + public override int FieldCount => _reader.FieldCount; + + public override bool GetBoolean(int i) => _reader.GetBoolean(i); + + public override byte GetByte(int i) => _reader.GetByte(i); + + public override long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => + _reader.GetBytes(i, fieldOffset, buffer!, bufferoffset, length); - bool IDataRecord.GetBoolean(int i) => Reader.GetBoolean(i); + public override char GetChar(int i) => _reader.GetChar(i); - byte IDataRecord.GetByte(int i) => Reader.GetByte(i); + public override long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => + _reader.GetChars(i, fieldoffset, buffer!, bufferoffset, length); - long IDataRecord.GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) => - Reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); + public override string GetDataTypeName(int i) => _reader.GetDataTypeName(i); - char IDataRecord.GetChar(int i) => Reader.GetChar(i); + public override DateTime GetDateTime(int i) => _reader.GetDateTime(i); - long IDataRecord.GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) => - Reader.GetChars(i, fieldoffset, buffer, bufferoffset, length); + public override decimal GetDecimal(int i) => _reader.GetDecimal(i); - IDataReader IDataRecord.GetData(int i) => Reader.GetData(i); + public override double GetDouble(int i) => _reader.GetDouble(i); - string IDataRecord.GetDataTypeName(int i) => Reader.GetDataTypeName(i); + public override Type GetFieldType(int i) => _reader.GetFieldType(i); - DateTime IDataRecord.GetDateTime(int i) => Reader.GetDateTime(i); + public override float GetFloat(int i) => _reader.GetFloat(i); - decimal IDataRecord.GetDecimal(int i) => Reader.GetDecimal(i); + public override Guid GetGuid(int i) => _reader.GetGuid(i); - double IDataRecord.GetDouble(int i) => Reader.GetDouble(i); + public override short GetInt16(int i) => _reader.GetInt16(i); - Type IDataRecord.GetFieldType(int i) => Reader.GetFieldType(i); + public override int GetInt32(int i) => _reader.GetInt32(i); - float IDataRecord.GetFloat(int i) => Reader.GetFloat(i); + public override long GetInt64(int i) => _reader.GetInt64(i); - Guid IDataRecord.GetGuid(int i) => Reader.GetGuid(i); + public override string GetName(int i) => _reader.GetName(i); - short IDataRecord.GetInt16(int i) => Reader.GetInt16(i); + public override int GetOrdinal(string name) => _reader.GetOrdinal(name); - int IDataRecord.GetInt32(int i) => Reader.GetInt32(i); + public override string GetString(int i) => _reader.GetString(i); - long IDataRecord.GetInt64(int i) => Reader.GetInt64(i); + public override object GetValue(int i) => _reader.GetValue(i); - string IDataRecord.GetName(int i) => Reader.GetName(i); + public override int GetValues(object[] values) => _reader.GetValues(values); - int IDataRecord.GetOrdinal(string name) => Reader.GetOrdinal(name); + public override bool IsDBNull(int i) => _reader.IsDBNull(i); - string IDataRecord.GetString(int i) => Reader.GetString(i); + public override object this[string name] => _reader[name]; - object IDataRecord.GetValue(int i) => Reader.GetValue(i); + public override object this[int i] => _reader[i]; - int IDataRecord.GetValues(object[] values) => Reader.GetValues(values); + public override T GetFieldValue(int ordinal) + { + var value = _reader.GetValue(ordinal); + if (value is DBNull) + { + value = null; + } + return (T)value!; + } + public override Task GetFieldValueAsync(int ordinal, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(GetFieldValue(ordinal)); + } + public override IEnumerator GetEnumerator() => _reader is IEnumerable e ? e.GetEnumerator() + : throw new NotImplementedException(); + public override Type GetProviderSpecificFieldType(int ordinal) => _reader.GetFieldType(ordinal); + public override object GetProviderSpecificValue(int ordinal) => _reader.GetValue(ordinal); + public override int GetProviderSpecificValues(object[] values) => _reader.GetValues(values); + public override Stream GetStream(int ordinal) => throw new NotSupportedException(); + public override TextReader GetTextReader(int ordinal) => throw new NotSupportedException(); + public override Task IsDBNullAsync(int ordinal, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(_reader.IsDBNull(ordinal)); + } + public override Task NextResultAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(_reader.NextResult()); + } + public override Task ReadAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(_reader.Read()); + } + public override int VisibleFieldCount => _reader.FieldCount; + protected override DbDataReader GetDbDataReader(int ordinal) => throw new NotSupportedException(); - bool IDataRecord.IsDBNull(int i) => Reader.IsDBNull(i); +#if NET5_0_OR_GREATER + public override Task CloseAsync() + { + _reader.Close(); + return Task.CompletedTask; + } - object IDataRecord.this[string name] => Reader[name]; + public override ValueTask DisposeAsync() + { + _reader.Dispose(); + return default; + } + + public override Task> GetColumnSchemaAsync(CancellationToken cancellationToken = default) + => throw new NotSupportedException(); - object IDataRecord.this[int i] => Reader[i]; + public override Task GetSchemaTableAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(_reader.GetSchemaTable()); + } +#endif } } diff --git a/Dapper/XmlHandlers.cs b/Dapper/XmlHandlers.cs index a29b45dba..e33d64d09 100644 --- a/Dapper/XmlHandlers.cs +++ b/Dapper/XmlHandlers.cs @@ -6,7 +6,7 @@ namespace Dapper { internal abstract class XmlTypeHandler : SqlMapper.StringTypeHandler { - public override void SetValue(IDbDataParameter parameter, T value) + public override void SetValue(IDbDataParameter parameter, T? value) { base.SetValue(parameter, value); parameter.DbType = DbType.Xml; diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..227e4eed5 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,49 @@ + + + 2019 Stack Exchange, Inc. + + true + true + ../Dapper.snk + + $(AssemblyName) + https://dapperlib.github.io/Dapper/ + https://github.com/DapperLib/Dapper + Apache-2.0 + Dapper.png + git + https://github.com/DapperLib/Dapper + false + $(NOWARN);IDE0056;IDE0057;IDE0079;DX1001 + true + embedded + en-US + false + true + true + 13 + false + true + readme.md + true + + + + true + true + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..bbbfb2d97 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,9 @@ + + + + $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..252f417b9 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Directory.build.props b/Directory.build.props deleted file mode 100644 index 3200957e1..000000000 --- a/Directory.build.props +++ /dev/null @@ -1,34 +0,0 @@ - - - 1.50.2 - - 2017 Stack Exchange, Inc. - - true - true - ../Dapper.snk - - $(AssemblyName) - https://stackexchange.github.io/Dapper/ - https://github.com/StackExchange/Dapper - http://www.apache.org/licenses/LICENSE-2.0 - git - https://github.com/StackExchange/Dapper - - true - embedded - en-US - false - 2.3.0-beta5-build3769 - - - - - - - - - - - - \ No newline at end of file diff --git a/License.txt b/License.txt index 0f3335224..aa7849358 100644 --- a/License.txt +++ b/License.txt @@ -1 +1,6 @@ -http://www.apache.org/licenses/LICENSE-2.0 \ No newline at end of file +The Dapper library and tools are licenced under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +The Dapper logo is copyright Marc Gravell 2021 onwards; it is fine to use the Dapper logo when referencing the Dapper library and utilities, but +the Dapper logo (including derivatives) must not be used in a way that misrepresents an external product or library as being affiliated or endorsed +with Dapper. For example, you must not use the Dapper logo as the package icon on your own external tool (even if it uses Dapper internally), +without written permission. If in doubt: ask. \ No newline at end of file diff --git a/NonCLA.md b/NonCLA.md new file mode 100644 index 000000000..f5c9414ef --- /dev/null +++ b/NonCLA.md @@ -0,0 +1,35 @@ +# Dapper - the "Non CLA" CLA + +IANAL. YANAL (I hope). Let's keep this simple; if you want to contribute to Dapper, great! Let's just check a few things for the record. + +By accepting this agreement, you're saying: + +## You're allowed to contribute this code + +The code needs to be yours (without being owned by some employer, etc), or contributed with the owner's knowledge and permission, or +in accordance with a licence that clearly allows the code to be reused in line with this project's licence (http://www.apache.org/licenses/LICENSE-2.0), +and in that last case: a cross-reference back to the origin would be nice. + +(for the pedants: "licence" and "license" to be used interchangeably here; language is fun) + +## It isn't "your code" any more + +Contributed code belongs to the Dapper project, not you the contributor. That means +Dapper can use it, not use it, remove it, or +edit it in any way - *even changing the spacing and variable names*. I know: shocking. + +## No backsies + +Contributing code to Dapper is permanent; you can't later demand that we remove your code because... well, anything. Even if one +of the maintainers wears socks that look *really ugly*. + +## For gratis + +Contributing code to Dapper gets you the bugfix or feature or whatever that you want; you have our thanks and appreciation, but unless we've +agreed something separately: that's it. No turning up unannounced at the tri-annual BBQ, or demanding... again, anything. + +--- + +That's it. Basically "don't make our life harder". + +Thanks! diff --git a/Readme.md b/Readme.md index 0932f6026..d7bc4afaa 100644 --- a/Readme.md +++ b/Readme.md @@ -4,25 +4,74 @@ Dapper - a simple object mapper for .Net Release Notes ------------- +Located at [https://github.com/DapperLib/Dapper/releases](https://github.com/DapperLib/Dapper/releases/) -[Located at stackexchange.github.io/Dapper](https://stackexchange.github.io/Dapper/) +Packages +-------- + +MyGet Pre-release feed: https://www.myget.org/gallery/dapper + +| Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet | +| ------- | ------------ | ----------------- | --------- | ----- | +| [Dapper](https://www.nuget.org/packages/Dapper/) | [![Dapper](https://img.shields.io/nuget/v/Dapper.svg)](https://www.nuget.org/packages/Dapper/) | [![Dapper](https://img.shields.io/nuget/vpre/Dapper.svg)](https://www.nuget.org/packages/Dapper/) | [![Dapper](https://img.shields.io/nuget/dt/Dapper.svg)](https://www.nuget.org/packages/Dapper/) | [![Dapper MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper) | +| [Dapper.EntityFramework](https://www.nuget.org/packages/Dapper.EntityFramework/) | [![Dapper.EntityFramework](https://img.shields.io/nuget/v/Dapper.EntityFramework.svg)](https://www.nuget.org/packages/Dapper.EntityFramework/) | [![Dapper.EntityFramework](https://img.shields.io/nuget/vpre/Dapper.EntityFramework.svg)](https://www.nuget.org/packages/Dapper.EntityFramework/) | [![Dapper.EntityFramework](https://img.shields.io/nuget/dt/Dapper.EntityFramework.svg)](https://www.nuget.org/packages/Dapper.EntityFramework/) | [![Dapper.EntityFramework MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.EntityFramework.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.EntityFramework) | +| [Dapper.EntityFramework.StrongName](https://www.nuget.org/packages/Dapper.EntityFramework.StrongName/) | [![Dapper.EntityFramework.StrongName](https://img.shields.io/nuget/v/Dapper.EntityFramework.StrongName.svg)](https://www.nuget.org/packages/Dapper.EntityFramework.StrongName/) | [![Dapper.EntityFramework.StrongName](https://img.shields.io/nuget/vpre/Dapper.EntityFramework.StrongName.svg)](https://www.nuget.org/packages/Dapper.EntityFramework.StrongName/) | [![Dapper.EntityFramework.StrongName](https://img.shields.io/nuget/dt/Dapper.EntityFramework.StrongName.svg)](https://www.nuget.org/packages/Dapper.EntityFramework.StrongName/) | [![Dapper.EntityFramework.StrongName MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.EntityFramework.StrongName.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.EntityFramework.StrongName) | +| [Dapper.Rainbow](https://www.nuget.org/packages/Dapper.Rainbow/) | [![Dapper.Rainbow](https://img.shields.io/nuget/v/Dapper.Rainbow.svg)](https://www.nuget.org/packages/Dapper.Rainbow/) | [![Dapper.Rainbow](https://img.shields.io/nuget/vpre/Dapper.Rainbow.svg)](https://www.nuget.org/packages/Dapper.Rainbow/) | [![Dapper.Rainbow](https://img.shields.io/nuget/dt/Dapper.Rainbow.svg)](https://www.nuget.org/packages/Dapper.Rainbow/) | [![Dapper.Rainbow MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.Rainbow.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.Rainbow) | +| [Dapper.SqlBuilder](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder](https://img.shields.io/nuget/v/Dapper.SqlBuilder.svg)](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder](https://img.shields.io/nuget/vpre/Dapper.SqlBuilder.svg)](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder](https://img.shields.io/nuget/dt/Dapper.SqlBuilder.svg)](https://www.nuget.org/packages/Dapper.SqlBuilder/) | [![Dapper.SqlBuilder MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.SqlBuilder.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.SqlBuilder) | +| [Dapper.StrongName](https://www.nuget.org/packages/Dapper.StrongName/) | [![Dapper.StrongName](https://img.shields.io/nuget/v/Dapper.StrongName.svg)](https://www.nuget.org/packages/Dapper.StrongName/) | [![Dapper.StrongName](https://img.shields.io/nuget/vpre/Dapper.StrongName.svg)](https://www.nuget.org/packages/Dapper.StrongName/) | [![Dapper.StrongName](https://img.shields.io/nuget/dt/Dapper.StrongName.svg)](https://www.nuget.org/packages/Dapper.StrongName/) | [![Dapper.StrongName MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.StrongName.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.StrongName) | + +Package Purposes: +* Dapper + * The core library +* Dapper.EntityFramework + * Extension handlers for EntityFramework +* Dapper.EntityFramework.StrongName + * Extension handlers for EntityFramework +* Dapper.Rainbow + * Micro-ORM implemented on Dapper, provides CRUD helpers ([readme](Dapper.Rainbow/readme.md)) +* Dapper.SqlBuilder + * Component for building SQL queries dynamically and composably + +Sponsors +-------- + +Dapper was originally developed for and by Stack Overflow, but is F/OSS. Sponsorship is welcome and invited - see the sponsor link at the top of the page. +A huge thanks to everyone (individuals or organisations) who have sponsored Dapper, but a massive thanks in particular to: +- [Dapper Plus](https://dapper-plus.net/) is a major sponsor and is proud to contribute to the development of Dapper ([read more](https://dapperlib.github.io/Dapper/dapperplus)) +- [AWS](https://github.com/aws) who sponsored Dapper from Oct 2023 via the [.NET on AWS Open Source Software Fund](https://github.com/aws/dotnet-foss) + +Dapper Plus logo Features -------- -Dapper is a [NuGet library](https://www.nuget.org/packages/Dapper) that you can add in to your project that will extend your `IDbConnection` interface. +Dapper is a [NuGet library](https://www.nuget.org/packages/Dapper) that you can add in to your project that will enhance your ADO.NET connections via +extension methods on your `DbConnection` instance. This provides a simple and efficient API for invoking SQL, with support for both synchronous and +asynchronous data access, and allows both buffered and non-buffered queries. -It provides 3 helpers: +It provides multiple helpers, but the key APIs are: -Execute a query and map the results to a strongly typed List ------------------------------------------------------------- +``` csharp +// insert/update/delete etc +var count = connection.Execute(sql [, args]); -```csharp -public static IEnumerable Query(this IDbConnection cnn, string sql, object param = null, SqlTransaction transaction = null, bool buffered = true) +// multi-row query +IEnumerable rows = connection.Query(sql [, args]); + +// single-row query ({Single|First}[OrDefault]) +T row = connection.QuerySingle(sql [, args]); ``` -Example usage: -```csharp +where `args` can be (among other things): + +- a simple POCO (including anonyomous types) for named parameters +- a `Dictionary` +- a `DynamicParameters` instance + +Execute a query and map it to a list of typed objects +------------------------------------------------------- + +``` csharp public class Dog { public int? Age { get; set; } @@ -31,8 +80,8 @@ public class Dog public float? Weight { get; set; } public int IgnoredProperty { get { return 1; } } -} - +} + var guid = Guid.NewGuid(); var dog = connection.Query("select Age = @Age, Id = @Id", new { Age = (int?)null, Id = guid }); @@ -44,15 +93,12 @@ Assert.Equal(guid, dog.First().Id); Execute a query and map it to a list of dynamic objects ------------------------------------------------------- -```csharp -public static IEnumerable Query (this IDbConnection cnn, string sql, object param = null, SqlTransaction transaction = null, bool buffered = true) -``` This method will execute SQL and return a dynamic list. Example usage: ```csharp -var rows = connection.Query("select 1 A, 2 B union all select 3, 4"); +var rows = connection.Query("select 1 A, 2 B union all select 3, 4").AsList(); Assert.Equal(1, (int)rows[0].A); Assert.Equal(2, (int)rows[0].B); @@ -63,20 +109,16 @@ Assert.Equal(4, (int)rows[1].B); Execute a Command that returns no results ----------------------------------------- -```csharp -public static int Execute(this IDbConnection cnn, string sql, object param = null, SqlTransaction transaction = null) -``` - Example usage: ```csharp var count = connection.Execute(@" - set nocount on - create table #t(i int) - set nocount off - insert #t - select @a a union all select @b - set nocount on + set nocount on + create table #t(i int) + set nocount off + insert #t + select @a a union all select @b + set nocount on drop table #t", new {a=1, b=2 }); Assert.Equal(2, count); ``` @@ -94,73 +136,133 @@ var count = connection.Execute(@"insert MyTable(colA, colB) values (@a, @b)", ); Assert.Equal(3, count); // 3 rows inserted: "1,1", "2,2" and "3,3" ``` -This works for any parameter that implements IEnumerable for some T. - -Performance ------------ - -A key feature of Dapper is performance. The following metrics show how long it takes to execute 500 SELECT statements against a DB and map the data returned to objects. - -The performance tests are broken in to 3 lists: - -- POCO serialization for frameworks that support pulling static typed objects from the DB. Using raw SQL. -- Dynamic serialization for frameworks that support returning dynamic lists of objects. -- Typical framework usage. Often typical framework usage differs from the optimal usage performance wise. Often it will not involve writing SQL. - -### Performance of SELECT mapping over 500 iterations - POCO serialization - +Another example usage when you _already_ have an existing collection: +```csharp +var foos = new List +{ + { new Foo { A = 1, B = 1 } } + { new Foo { A = 2, B = 2 } } + { new Foo { A = 3, B = 3 } } +}; -| Method | Duration | Remarks | -| --------------------------------------------------- | -------- | ------- | -| Hand coded (using a `SqlDataReader`) | 47ms | -| Dapper `ExecuteMapperQuery` | 49ms | -| [ServiceStack.OrmLite](https://github.com/ServiceStack/ServiceStack.OrmLite) (QueryById) | 50ms | -| [PetaPoco](http://www.toptensoftware.com/petapoco/) | 52ms | [Can be faster](http://www.toptensoftware.com/blog/posts/94-PetaPoco-More-Speed) | -| BLToolkit | 80ms | -| SubSonic CodingHorror | 107ms | -| NHibernate SQL | 104ms | -| Linq 2 SQL `ExecuteQuery` | 181ms | -| Entity framework `ExecuteStoreQuery` | 631ms | - -### Performance of SELECT mapping over 500 iterations - dynamic serialization - -| Method | Duration | Remarks | -| -------------------------------------------------------- | -------- | ------- | -| Dapper `ExecuteMapperQuery` (dynamic) | 48ms | -| [Massive](https://github.com/FransBouma/Massive) | 52ms | -| [Simple.Data](https://github.com/markrendle/Simple.Data) | 95ms | - +var count = connection.Execute(@"insert MyTable(colA, colB) values (@a, @b)", foos); +Assert.Equal(foos.Count, count); +``` -### Performance of SELECT mapping over 500 iterations - typical usage +This works for any parameter that implements `IEnumerable` for some T. -| Method | Duration | Remarks | -| ------------------------------------- | -------- | ------- | -| Linq 2 SQL CompiledQuery | 81ms | Not super typical involves complex code | -| NHibernate HQL | 118ms | -| Linq 2 SQL | 559ms | -| Entity framework | 859ms | -| SubSonic ActiveRecord.SingleOrDefault | 3619ms | +Performance +----------- +A key feature of Dapper is performance. The following metrics show how long it takes to execute a `SELECT` statement against a DB (in various config, each labeled) and map the data returned to objects. -Performance benchmarks are available [here](https://github.com/StackExchange/Dapper/tree/master/Dapper.Tests.Performance). +The benchmarks can be found in [Dapper.Tests.Performance](https://github.com/DapperLib/Dapper/tree/main/benchmarks/Dapper.Tests.Performance) (contributions welcome!) and can be run via: +```bash +dotnet run --project .\benchmarks\Dapper.Tests.Performance\ -c Release -f net8.0 -- -f * --join +``` +Output from the latest run is: +``` ini +BenchmarkDotNet v0.13.7, Windows 10 (10.0.19045.3693/22H2/2022Update) +Intel Core i7-3630QM CPU 2.40GHz (Ivy Bridge), 1 CPU, 8 logical and 4 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX + ShortRun : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX -Feel free to submit patches that include other ORMs - when running benchmarks, be sure to compile in Release and not attach a debugger (ctrl F5). +``` +| ORM | Method | Return | Mean | StdDev | Error | Gen0 | Gen1 | Gen2 | Allocated | +|-------------------- |------------------------------- |------------- |----------:|----------:|----------:|--------:|-------:|-------:|----------:| +| Dapper cache impact | ExecuteParameters_Cache | Void | 96.75 us | 0.668 us | 1.010 us | 0.6250 | - | - | 2184 B | +| Dapper cache impact | QueryFirstParameters_Cache | Void | 96.86 us | 0.493 us | 0.746 us | 0.8750 | - | - | 2824 B | +| Hand Coded | SqlCommand | Post | 119.70 us | 0.706 us | 1.067 us | 1.3750 | 1.0000 | 0.1250 | 7584 B | +| Hand Coded | DataTable | dynamic | 126.64 us | 1.239 us | 1.873 us | 3.0000 | - | - | 9576 B | +| SqlMarshal | SqlCommand | Post | 132.36 us | 1.008 us | 1.523 us | 2.0000 | 1.0000 | 0.2500 | 11529 B | +| Dapper | QueryFirstOrDefault | Post | 133.73 us | 1.301 us | 2.186 us | 1.7500 | 1.5000 | - | 11608 B | +| Mighty | Query | dynamic | 133.92 us | 1.075 us | 1.806 us | 2.0000 | 1.7500 | - | 12710 B | +| LINQ to DB | Query | Post | 134.24 us | 1.068 us | 1.614 us | 1.7500 | 1.2500 | - | 10904 B | +| RepoDB | ExecuteQuery | Post | 135.83 us | 1.839 us | 3.091 us | 1.7500 | 1.5000 | - | 11649 B | +| Dapper | 'Query (buffered)' | Post | 136.14 us | 1.755 us | 2.653 us | 2.0000 | 1.5000 | - | 11888 B | +| Mighty | Query | Post | 137.96 us | 1.485 us | 2.244 us | 2.2500 | 1.2500 | - | 12201 B | +| Dapper | QueryFirstOrDefault | dynamic | 139.04 us | 1.507 us | 2.279 us | 3.5000 | - | - | 11648 B | +| Mighty | SingleFromQuery | dynamic | 139.74 us | 2.521 us | 3.811 us | 2.0000 | 1.7500 | - | 12710 B | +| Dapper | 'Query (buffered)' | dynamic | 140.13 us | 1.382 us | 2.090 us | 2.0000 | 1.5000 | - | 11968 B | +| ServiceStack | SingleById | Post | 140.76 us | 1.147 us | 2.192 us | 2.5000 | 1.2500 | 0.2500 | 15248 B | +| Dapper | 'Contrib Get' | Post | 141.09 us | 1.394 us | 2.108 us | 2.0000 | 1.5000 | - | 12440 B | +| Mighty | SingleFromQuery | Post | 141.17 us | 1.941 us | 2.935 us | 1.7500 | 1.5000 | - | 12201 B | +| Massive | 'Query (dynamic)' | dynamic | 142.01 us | 4.957 us | 7.494 us | 2.0000 | 1.5000 | - | 12342 B | +| LINQ to DB | 'First (Compiled)' | Post | 144.59 us | 1.295 us | 1.958 us | 1.7500 | 1.5000 | - | 12128 B | +| RepoDB | QueryField | Post | 148.31 us | 1.742 us | 2.633 us | 2.0000 | 1.5000 | 0.5000 | 13938 B | +| Norm | 'Read<> (tuples)' | ValueTuple`8 | 148.58 us | 2.172 us | 3.283 us | 2.0000 | 1.7500 | - | 12745 B | +| Norm | 'Read<()> (named tuples)' | ValueTuple`8 | 150.60 us | 0.658 us | 1.106 us | 2.2500 | 2.0000 | 1.2500 | 14562 B | +| RepoDB | Query | Post | 152.34 us | 2.164 us | 3.271 us | 2.2500 | 1.5000 | 0.2500 | 14106 B | +| RepoDB | QueryDynamic | Post | 154.15 us | 4.108 us | 6.210 us | 2.2500 | 1.7500 | 0.5000 | 13930 B | +| RepoDB | QueryWhere | Post | 155.90 us | 1.953 us | 3.282 us | 2.5000 | 0.5000 | - | 14858 B | +| Dapper cache impact | ExecuteNoParameters_NoCache | Void | 162.35 us | 1.584 us | 2.394 us | - | - | - | 760 B | +| Dapper cache impact | ExecuteNoParameters_Cache | Void | 162.42 us | 2.740 us | 4.142 us | - | - | - | 760 B | +| Dapper cache impact | QueryFirstNoParameters_Cache | Void | 164.35 us | 1.206 us | 1.824 us | 0.2500 | - | - | 1520 B | +| DevExpress.XPO | FindObject | Post | 165.87 us | 1.012 us | 1.934 us | 8.5000 | - | - | 28099 B | +| Dapper cache impact | QueryFirstNoParameters_NoCache | Void | 173.87 us | 1.178 us | 1.781 us | 0.5000 | - | - | 1576 B | +| LINQ to DB | First | Post | 175.21 us | 2.292 us | 3.851 us | 2.0000 | 0.5000 | - | 14041 B | +| EF 6 | SqlQuery | Post | 175.36 us | 2.259 us | 3.415 us | 4.0000 | 0.7500 | - | 24209 B | +| Norm | 'Read<> (class)' | Post | 186.37 us | 1.305 us | 2.496 us | 3.0000 | 0.5000 | - | 17579 B | +| DevExpress.XPO | GetObjectByKey | Post | 186.78 us | 3.407 us | 5.151 us | 4.5000 | 1.0000 | - | 30114 B | +| Dapper | 'Query (unbuffered)' | dynamic | 194.62 us | 1.335 us | 2.019 us | 1.7500 | 1.5000 | - | 12048 B | +| Dapper | 'Query (unbuffered)' | Post | 195.01 us | 0.888 us | 1.343 us | 2.0000 | 1.5000 | - | 12008 B | +| DevExpress.XPO | Query | Post | 199.46 us | 5.500 us | 9.243 us | 10.0000 | - | - | 32083 B | +| Belgrade | FirstOrDefault | Task`1 | 228.70 us | 2.181 us | 3.665 us | 4.5000 | 0.5000 | - | 20555 B | +| EF Core | 'First (Compiled)' | Post | 265.45 us | 17.745 us | 26.828 us | 2.0000 | - | - | 7521 B | +| NHibernate | Get | Post | 276.02 us | 8.029 us | 12.139 us | 6.5000 | 1.0000 | - | 29885 B | +| NHibernate | HQL | Post | 277.74 us | 13.032 us | 19.703 us | 8.0000 | 1.0000 | - | 31886 B | +| NHibernate | Criteria | Post | 300.22 us | 14.908 us | 28.504 us | 13.0000 | 1.0000 | - | 57562 B | +| EF 6 | First | Post | 310.55 us | 27.254 us | 45.799 us | 13.0000 | - | - | 43309 B | +| EF Core | First | Post | 317.12 us | 1.354 us | 2.046 us | 3.5000 | - | - | 11306 B | +| EF Core | SqlQuery | Post | 322.34 us | 23.990 us | 40.314 us | 5.0000 | - | - | 18195 B | +| NHibernate | SQL | Post | 325.54 us | 3.937 us | 7.527 us | 22.0000 | 1.0000 | - | 80007 B | +| EF 6 | 'First (No Tracking)' | Post | 331.14 us | 27.760 us | 46.649 us | 12.0000 | 1.0000 | - | 50237 B | +| EF Core | 'First (No Tracking)' | Post | 337.82 us | 27.814 us | 46.740 us | 3.0000 | 1.0000 | - | 17986 B | +| NHibernate | LINQ | Post | 604.74 us | 5.549 us | 10.610 us | 10.0000 | - | - | 46061 B | +| Dapper cache impact | ExecuteParameters_NoCache | Void | 623.42 us | 3.978 us | 6.684 us | 3.0000 | 2.0000 | - | 10001 B | +| Dapper cache impact | QueryFirstParameters_NoCache | Void | 630.77 us | 3.027 us | 4.576 us | 3.0000 | 2.0000 | - | 10640 B | + +Feel free to submit patches that include other ORMs - when running benchmarks, be sure to compile in Release and not attach a debugger (Ctrl+F5). Alternatively, you might prefer Frans Bouma's [RawDataAccessBencher](https://github.com/FransBouma/RawDataAccessBencher) test suite or [OrmBenchmark](https://github.com/InfoTechBridge/OrmBenchmark). Parameterized queries --------------------- -Parameters are passed in as anonymous classes. This allow you to name your parameters easily and gives you the ability to simply cut-and-paste SQL snippets and run them in Query analyzer. +Parameters are usually passed in as anonymous classes. This allows you to name your parameters easily and gives you the ability to simply cut-and-paste SQL snippets and run them in your db platform's Query analyzer. ```csharp -new {A = 1, B = "b"} // A will be mapped to the param @A, B to the param @B +new {A = 1, B = "b"} // A will be mapped to the param @A, B to the param @B ``` +Parameters can also be built up dynamically using the DynamicParameters class. This allows for building a dynamic SQL statement while still using parameters for safety and performance. +```csharp + var sqlPredicates = new List(); + var queryParams = new DynamicParameters(); + if (boolExpression) + { + sqlPredicates.Add("column1 = @param1"); + queryParams.Add("param1", dynamicValue1, System.Data.DbType.Guid); + } else { + sqlPredicates.Add("column2 = @param2"); + queryParams.Add("param2", dynamicValue2, System.Data.DbType.String); + } +``` + +DynamicParameters also supports copying multiple parameters from existing objects of different types. + +```csharp + var queryParams = new DynamicParameters(objectOfType1); + queryParams.AddDynamicParams(objectOfType2); +``` + +When an object that implements the `IDynamicParameters` interface passed into `Execute` or `Query` functions, parameter values will be extracted via this interface. Obviously, the most likely object class to use for this purpose would be the built-in `DynamicParameters` class. + List Support ------------ -Dapper allows you to pass in IEnumerable and will automatically parameterize your query. +Dapper allows you to pass in `IEnumerable` and will automatically parameterize your query. For example: @@ -179,7 +281,7 @@ Literal replacements Dapper supports literal replacements for bool and numeric types. ```csharp -connection.Query("select * from User where UserId = {=Id}", new {Id = 1})); +connection.Query("select * from User where UserTypeId = {=Admin}", new { UserTypeId.Admin }); ``` The literal replacement is not sent as a parameter; this allows better plans and filtered index usage but should usually be used sparingly and after testing. This feature is particularly useful when the value being injected @@ -187,9 +289,9 @@ is actually a fixed value (for example, a fixed "category id", "status code" or Buffered vs Unbuffered readers --------------------- -Dapper's default behavior is to execute your sql and buffer the entire reader on return. This is ideal in most cases as it minimizes shared locks in the db and cuts down on db network time. +Dapper's default behavior is to execute your SQL and buffer the entire reader on return. This is ideal in most cases as it minimizes shared locks in the db and cuts down on db network time. -However when executing huge queries you may need to minimize memory footprint and only load objects as needed. To do so pass, buffered: false into the Query method. +However when executing huge queries you may need to minimize memory footprint and only load objects as needed. To do so pass, `buffered: false` into the `Query` method. Multi Mapping --------------------- @@ -217,7 +319,7 @@ class User Now let us say that we want to map a query that joins both the posts and the users table. Until now if we needed to combine the result of 2 queries, we'd need a new object to express it but it makes more sense in this case to put the `User` object inside the `Post` object. -This is the user case for multi mapping. You tell dapper that the query returns a `Post` and a `User` object and then give it a function describing what you want to do with each of the rows containing both a `Post` and a `User` object. In our case, we want to take the user object and put it inside the post object. So we write the function: +This is the use case for multi mapping. You tell dapper that the query returns a `Post` and a `User` object and then give it a function describing what you want to do with each of the rows containing both a `Post` and a `User` object. In our case, we want to take the user object and put it inside the post object. So we write the function: ```csharp (post, user) => { post.Owner = user; return post; } @@ -232,11 +334,11 @@ The 3 type arguments to the `Query` method specify what objects dapper should us Everything put together, looks like this: ```csharp -var sql = -@"select * from #Posts p -left join #Users u on u.Id = p.OwnerId +var sql = +@"select * from #Posts p +left join #Users u on u.Id = p.OwnerId Order by p.Id"; - + var data = connection.Query(sql, (post, user) => { post.Owner = user; return post;}); var post = data.First(); @@ -255,19 +357,19 @@ Dapper allows you to process multiple result grids in a single query. Example: ```csharp -var sql = +var sql = @" select * from Customers where CustomerId = @id select * from Orders where CustomerId = @id select * from Returns where CustomerId = @id"; - + using (var multi = connection.QueryMultiple(sql, new {id=selectedId})) { var customer = multi.Read().Single(); var orders = multi.Read().ToList(); var returns = multi.Read().ToList(); ... -} +} ``` Stored Procedures @@ -275,7 +377,7 @@ Stored Procedures Dapper fully supports stored procs: ```csharp -var user = cnn.Query("spGetUser", new {Id = 1}, +var user = cnn.Query("spGetUser", new {Id = 1}, commandType: CommandType.StoredProcedure).SingleOrDefault(); ``` @@ -287,10 +389,10 @@ p.Add("@a", 11); p.Add("@b", dbType: DbType.Int32, direction: ParameterDirection.Output); p.Add("@c", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue); -cnn.Execute("spMagicProc", p, commandType: CommandType.StoredProcedure); +cnn.Execute("spMagicProc", p, commandType: CommandType.StoredProcedure); int b = p.Get("@b"); -int c = p.Get("@c"); +int c = p.Get("@c"); ``` Ansi Strings and varchar @@ -298,10 +400,10 @@ Ansi Strings and varchar Dapper supports varchar params, if you are executing a where clause on a varchar column using a param be sure to pass it in this way: ```csharp -Query("select * from Thing where Name = @Name", new {Name = new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = true }); +Query("select * from Thing where Name = @Name", new {Name = new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = true }}); ``` -On SQL Server it is crucial to use the unicode when querying unicode and ansi when querying non unicode. +On SQL Server it is crucial to use the unicode when querying unicode and ANSI when querying non unicode. Type Switching Per Row --------------------- @@ -320,46 +422,54 @@ using (var reader = connection.ExecuteReader("select * from Shapes")) var circleParser = reader.GetRowParser(typeof(Circle)); var squareParser = reader.GetRowParser(typeof(Square)); var triangleParser = reader.GetRowParser(typeof(Triangle)); - - var typeColumnIndex = reader.GetOrdinal("Type"); - - while (reader.Read()) + + var typeColumnIndex = reader.GetOrdinal("Type"); + + while (reader.Read()) { IShape shape; var type = (ShapeType)reader.GetInt32(typeColumnIndex); switch (type) { - case ShapeType.Circle: + case ShapeType.Circle: shape = circleParser(reader); break; case ShapeType.Square: shape = squareParser(reader); break; - case ShapeType.Triangle: + case ShapeType.Triangle: shape = triangleParser(reader); break; - default: + default: throw new NotImplementedException(); } - + shapes.Add(shape); } } ``` +User Defined Variables in MySQL/MariaDB +--------------------- +In order to use Non-parameter SQL variables with MySql Connector, you have to add the following option to your connection string: + +`Allow User Variables=True` + +Make sure you don't provide Dapper with a property to map. + Limitations and caveats --------------------- -Dapper caches information about every query it runs, this allow it to materialize objects quickly and process parameters quickly. The current implementation caches this information in a ConcurrentDictionary object. The objects it stores are never flushed. If you are generating SQL strings on the fly without using parameters it is possible you will hit memory issues. We may convert the dictionaries to an LRU Cache. +Dapper caches information about every query it runs, this allows it to materialize objects quickly and process parameters quickly. The current implementation caches this information in a `ConcurrentDictionary` object. Statements that are only used once are routinely flushed from this cache. Still, if you are generating SQL strings on the fly without using parameters it is possible you may hit memory issues. -Dapper's simplicity means that many feature that ORMs ship with are stripped out. It worries about the 95% scenario, and gives you the tools you need most of the time. It doesn't attempt to solve every problem. +Dapper's simplicity means that many features that ORMs ship with are stripped out. It worries about the 95% scenario, and gives you the tools you need most of the time. It doesn't attempt to solve every problem. Will Dapper work with my DB provider? --------------------- -Dapper has no DB specific implementation details, it works across all .NET ADO providers including [SQLite](https://www.sqlite.org/), SQL CE, Firebird, Oracle, MySQL, PostgreSQL and SQL Server. +Dapper has no DB specific implementation details, it works across all .NET ADO providers including [SQLite](https://www.sqlite.org/), SQL CE, Firebird, Oracle, MariaDB, MySQL, PostgreSQL and SQL Server. Do you have a comprehensive list of examples? --------------------- -Dapper has a comprehensive test suite in the [test project](https://github.com/StackExchange/dapper-dot-net/blob/master/Dapper.Tests) +Dapper has a comprehensive test suite in the [test project](https://github.com/DapperLib/Dapper/tree/main/tests/Dapper.Tests). Who is using this? --------------------- diff --git a/appveyor.yml b/appveyor.yml index e23831f3a..5ec88faaf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -image: Visual Studio 2017 +image: Visual Studio 2022 skip_branch_with_pr: true skip_tags: true @@ -6,40 +6,51 @@ skip_commits: files: - '**/*.md' +install: +- ps: | + Invoke-WebRequest -Uri 'https://dot.net/v1/dotnet-install.ps1' -UseBasicParsing -OutFile "$env:temp\dotnet-install.ps1" + & $env:temp\dotnet-install.ps1 -Architecture x64 -Version '10.0.102' -InstallDir "$env:ProgramFiles\dotnet" + environment: Appveyor: true # Postgres - POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 + POSTGRES_PATH: C:\Program Files\PostgreSQL\16 PGUSER: postgres PGPASSWORD: Password12! POSTGRES_ENV_POSTGRES_USER: postgres POSTGRES_ENV_POSTGRES_PASSWORD: Password12! POSTGRES_ENV_POSTGRES_DB: test # MySQL - MYSQL_PATH: C:\Program Files\MySql\MySQL Server 5.7 + MYSQL_PATH: C:\Program Files\MySQL\MySQL Server 8.0 MYSQL_PWD: Password12! MYSQL_ENV_MYSQL_USER: root MYSQL_ENV_MYSQL_PASSWORD: Password12! MYSQL_ENV_MYSQL_DATABASE: test -services: - - mssql2016 - - mysql - - postgresql + # Connection strings for tests: + MySqlConnectionString: Server=localhost;Database=test;Uid=root;Pwd=Password12! + OLEDBConnectionString: Provider=SQLOLEDB;Data Source=(local)\SQL2019;Initial Catalog=tempdb;User Id=sa;Password=Password12! + PostgesConnectionString: Server=localhost;Port=5432;User Id=postgres;Password=Password12!;Database=test + SqlServerConnectionString: Server=(local)\SQL2019;Database=tempdb;User ID=sa;Password=Password12! init: - git config --global core.autocrlf input - SET PATH=%POSTGRES_PATH%\bin;%MYSQL_PATH%\bin;%PATH% - + - net start MSSQL$SQL2019 + - net start postgresql-x64-16 + - ps: Start-Service MySQL80 + nuget: disable_publish_on_pr: true -build_script: +before_build: # Postgres - createdb test # MySQL - - mysql -e "create database test;" --user=root + - '"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql" -e "create database test;" --user=root' + +build_script: # Our stuff - - ps: .\build.ps1 -BuildNumber "$env:APPVEYOR_BUILD_NUMBER" -Version "$env:APPVEYOR_REPO_TAG_NAME" -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages $true + - ps: .\build.ps1 -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages $true test: off artifacts: @@ -49,14 +60,14 @@ deploy: - provider: NuGet server: https://www.myget.org/F/stackoverflow/api/v2 on: - branch: master + branch: main api_key: secure: P/UHxq2DEs0GI1SoDXDesHjRVsSVgdywz5vmsnhFQQY5aJgO3kP+QfhwfhXz19Rw symbol_server: https://www.myget.org/F/stackoverflow/symbols/api/v2/package - provider: NuGet server: https://www.myget.org/F/dapper/api/v2 on: - branch: master + branch: main api_key: secure: PV7ERAltWWLhy7AT2h+Vb5c1BM9/WFgvggb+rKyQ8hDg3fYqpZauYdidOOgt2lp4 symbol_server: https://www.myget.org/F/dapper/api/v2/package \ No newline at end of file diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.Belgrade.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Belgrade.cs new file mode 100644 index 000000000..d35e278eb --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Belgrade.cs @@ -0,0 +1,45 @@ +using BenchmarkDotNet.Attributes; +using Belgrade.SqlClient.SqlDb; +using Belgrade.SqlClient; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Dapper.Tests.Performance +{ + [Description("Belgrade")] + public class BelgradeBenchmarks : BenchmarkBase + { + private QueryMapper _mapper; + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + _mapper = new QueryMapper(ConnectionString); + } + + [Benchmark(Description = "FirstOrDefault")] + public Task FirstOrDefault() + { + Step(); + return _mapper.Sql("SELECT TOP 1 * FROM Posts WHERE Id = @Id").Param("Id", i).FirstOrDefault( + reader => new Post + { + Id = reader.GetInt32(0), + Text = reader.GetString(1), + CreationDate = reader.GetDateTime(2), + LastChangeDate = reader.GetDateTime(3), + + Counter1 = reader.IsDBNull(4) ? null : (int?)reader.GetInt32(4), + Counter2 = reader.IsDBNull(5) ? null : (int?)reader.GetInt32(5), + Counter3 = reader.IsDBNull(6) ? null : (int?)reader.GetInt32(6), + Counter4 = reader.IsDBNull(7) ? null : (int?)reader.GetInt32(7), + Counter5 = reader.IsDBNull(8) ? null : (int?)reader.GetInt32(8), + Counter6 = reader.IsDBNull(9) ? null : (int?)reader.GetInt32(9), + Counter7 = reader.IsDBNull(10) ? null : (int?)reader.GetInt32(10), + Counter8 = reader.IsDBNull(11) ? null : (int?)reader.GetInt32(11), + Counter9 = reader.IsDBNull(12) ? null : (int?)reader.GetInt32(12), + }); + } + } +} diff --git a/Dapper.Tests.Performance/Benchmarks.Dapper.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Dapper.cs similarity index 85% rename from Dapper.Tests.Performance/Benchmarks.Dapper.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.Dapper.cs index e51c0c18b..40f3eb8d7 100644 --- a/Dapper.Tests.Performance/Benchmarks.Dapper.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Dapper.cs @@ -1,9 +1,11 @@ using BenchmarkDotNet.Attributes; using Dapper.Contrib.Extensions; +using System.ComponentModel; using System.Linq; namespace Dapper.Tests.Performance { + [Description("Dapper")] public class DapperBenchmarks : BenchmarkBase { [GlobalSetup] @@ -19,7 +21,7 @@ public Post QueryBuffered() return _connection.Query("select * from Posts where Id = @Id", new { Id = i }, buffered: true).First(); } - [Benchmark(Description = "Query (buffered)")] + [Benchmark(Description = "Query (buffered)")] public dynamic QueryBufferedDynamic() { Step(); @@ -33,7 +35,7 @@ public Post QueryUnbuffered() return _connection.Query("select * from Posts where Id = @Id", new { Id = i }, buffered: false).First(); } - [Benchmark(Description = "Query (unbuffered)")] + [Benchmark(Description = "Query (unbuffered)")] public dynamic QueryUnbufferedDynamic() { Step(); @@ -47,7 +49,7 @@ public Post QueryFirstOrDefault() return _connection.QueryFirstOrDefault("select * from Posts where Id = @Id", new { Id = i }); } - [Benchmark(Description = "QueryFirstOrDefault")] + [Benchmark(Description = "QueryFirstOrDefault")] public dynamic QueryFirstOrDefaultDynamic() { Step(); diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.Dashing.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Dashing.cs new file mode 100644 index 000000000..ca89b6715 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Dashing.cs @@ -0,0 +1,34 @@ +#if NET4X +using System.ComponentModel; +using BenchmarkDotNet.Attributes; +using Dapper.Tests.Performance.Dashing; +using Dashing; + +namespace Dapper.Tests.Performance +{ + [Description("Dashing")] + public class DashingBenchmarks : BenchmarkBase + { + private ISession Session; + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + var configuration = new DashingConfiguration(); + var database = new SqlDatabase(configuration, ConnectionString); + Session = database.BeginTransactionLessSession(_connection); + } + + // This needs love to be compatible with current SDKs (weaving doesn't work and shouldn't be used here anyway (competition). + // I'll file an issue with Dashing to see if someone can help me out here since I can't figure out from the docs how to + // make it work correctly. + //[Benchmark(Description = "Get")] + public Dashing.Post Get() + { + Step(); + return Session.Get(i); + } + } +} +#endif diff --git a/Dapper.Tests.Performance/Benchmarks.EntityFramework.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.EntityFramework.cs similarity index 75% rename from Dapper.Tests.Performance/Benchmarks.EntityFramework.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.EntityFramework.cs index f145a24d1..6660782d3 100644 --- a/Dapper.Tests.Performance/Benchmarks.EntityFramework.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.EntityFramework.cs @@ -1,8 +1,10 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using System.ComponentModel; using System.Linq; namespace Dapper.Tests.Performance { + [Description("EF 6")] public class EF6Benchmarks : BenchmarkBase { private EntityFramework.EFContext Context; @@ -14,8 +16,8 @@ public void Setup() Context = new EntityFramework.EFContext(_connection); } - [Benchmark(Description = "Normal")] - public Post Normal() + [Benchmark(Description = "First")] + public Post First() { Step(); return Context.Posts.First(p => p.Id == i); @@ -28,11 +30,11 @@ public Post SqlQuery() return Context.Database.SqlQuery("select * from Posts where Id = {0}", i).First(); } - [Benchmark(Description = "No Tracking")] + [Benchmark(Description = "First (No Tracking)")] public Post NoTracking() { Step(); return Context.Posts.AsNoTracking().First(p => p.Id == i); } } -} \ No newline at end of file +} diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.EntityFrameworkCore.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.EntityFrameworkCore.cs new file mode 100644 index 000000000..2c8b811b2 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.EntityFrameworkCore.cs @@ -0,0 +1,53 @@ +using BenchmarkDotNet.Attributes; +using Dapper.Tests.Performance.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using System; +using System.ComponentModel; +using System.Linq; + +namespace Dapper.Tests.Performance +{ + [Description("EF Core")] + public class EFCoreBenchmarks : BenchmarkBase + { + private EFCoreContext Context; + + private static readonly Func compiledQuery = + EF.CompileQuery((EFCoreContext ctx, int id) => ctx.Posts.First(p => p.Id == id)); + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + Context = new EFCoreContext(ConnectionString); + } + + [Benchmark(Description = "First")] + public Post First() + { + Step(); + return Context.Posts.First(p => p.Id == i); + } + + [Benchmark(Description = "First (Compiled)")] + public Post Compiled() + { + Step(); + return compiledQuery(Context, i); + } + + [Benchmark(Description = "SqlQuery")] + public Post SqlQuery() + { + Step(); + return Context.Posts.FromSqlRaw("select * from Posts where Id = {0}", i).First(); + } + + [Benchmark(Description = "First (No Tracking)")] + public Post NoTracking() + { + Step(); + return Context.Posts.AsNoTracking().First(p => p.Id == i); + } + } +} diff --git a/Dapper.Tests.Performance/Benchmarks.HandCoded.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs similarity index 51% rename from Dapper.Tests.Performance/Benchmarks.HandCoded.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs index e100b32f8..3fcfc6d3d 100644 --- a/Dapper.Tests.Performance/Benchmarks.HandCoded.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.HandCoded.cs @@ -1,30 +1,25 @@ using BenchmarkDotNet.Attributes; using System; +using System.ComponentModel; using System.Data; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace Dapper.Tests.Performance { + [Description("Hand Coded")] public class HandCodedBenchmarks : BenchmarkBase { private SqlCommand _postCommand; private SqlParameter _idParam; -#if !NETCOREAPP1_0 private DataTable _table; -#endif [GlobalSetup] public void Setup() { BaseSetup(); - _postCommand = new SqlCommand() - { - Connection = _connection, - CommandText = @"select Id, [Text], [CreationDate], LastChangeDate, - Counter1,Counter2,Counter3,Counter4,Counter5,Counter6,Counter7,Counter8,Counter9 from Posts where Id = @Id" - }; + _postCommand = new SqlCommand("select Top 1 * from Posts where Id = @Id", _connection); _idParam = _postCommand.Parameters.Add("@Id", SqlDbType.Int); -#if !NETCOREAPP1_0 + _postCommand.Prepare(); _table = new DataTable { Columns = @@ -44,34 +39,34 @@ public void Setup() {"Counter9", typeof (int)}, } }; -#endif } - [Benchmark(Description = "SqlCommand", Baseline = true)] + [Benchmark(Description = "SqlCommand")] public Post SqlCommand() { Step(); _idParam.Value = i; - using (var reader = _postCommand.ExecuteReader()) + using (var reader = _postCommand.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { reader.Read(); - var post = new Post(); - post.Id = reader.GetInt32(0); - post.Text = reader.GetNullableString(1); - post.CreationDate = reader.GetDateTime(2); - post.LastChangeDate = reader.GetDateTime(3); + return new Post + { + Id = reader.GetInt32(0), + Text = reader.GetNullableString(1), + CreationDate = reader.GetDateTime(2), + LastChangeDate = reader.GetDateTime(3), - post.Counter1 = reader.GetNullableValue(4); - post.Counter2 = reader.GetNullableValue(5); - post.Counter3 = reader.GetNullableValue(6); - post.Counter4 = reader.GetNullableValue(7); - post.Counter5 = reader.GetNullableValue(8); - post.Counter6 = reader.GetNullableValue(9); - post.Counter7 = reader.GetNullableValue(10); - post.Counter8 = reader.GetNullableValue(11); - post.Counter9 = reader.GetNullableValue(12); - return post; + Counter1 = reader.GetNullableValue(4), + Counter2 = reader.GetNullableValue(5), + Counter3 = reader.GetNullableValue(6), + Counter4 = reader.GetNullableValue(7), + Counter5 = reader.GetNullableValue(8), + Counter6 = reader.GetNullableValue(9), + Counter7 = reader.GetNullableValue(10), + Counter8 = reader.GetNullableValue(11), + Counter9 = reader.GetNullableValue(12) + }; } } @@ -80,13 +75,13 @@ public dynamic DataTableDynamic() { Step(); _idParam.Value = i; + _table.Rows.Clear(); var values = new object[13]; - using (var reader = _postCommand.ExecuteReader()) + using (var reader = _postCommand.ExecuteReader(CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { reader.Read(); reader.GetValues(values); - _table.Rows.Add(values); - return _table.Rows[_table.Rows.Count - 1]; + return _table.Rows.Add(values); } } } diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.Linq2DB.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Linq2DB.cs new file mode 100644 index 000000000..cb9aaacd2 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Linq2DB.cs @@ -0,0 +1,48 @@ +using BenchmarkDotNet.Attributes; + +using System; +using System.Linq; +using Dapper.Tests.Performance.Linq2Db; +using LinqToDB; +using LinqToDB.Data; +using System.ComponentModel; + +namespace Dapper.Tests.Performance +{ + [Description("LINQ to DB")] + public class LinqToDBBenchmarks : BenchmarkBase // note To not 2 because the "2" confuses BDN CLI + { + private Linq2DBContext _dbContext; + + private static readonly Func compiledQuery = CompiledQuery.Compile((Linq2DBContext db, int id) => db.Posts.First(c => c.Id == id)); + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + DataConnection.DefaultSettings = new Linq2DBSettings(_connection.ConnectionString); + _dbContext = new Linq2DBContext(); + } + + [Benchmark(Description = "First")] + public Post First() + { + Step(); + return _dbContext.Posts.First(p => p.Id == i); + } + + [Benchmark(Description = "First (Compiled)")] + public Post Compiled() + { + Step(); + return compiledQuery(_dbContext, i); + } + + [Benchmark(Description = "Query")] + public Post Query() + { + Step(); + return _dbContext.Query("select * from Posts where Id = @id", new { id = i }).First(); + } + } +} diff --git a/Dapper.Tests.Performance/Benchmarks.Linq2Sql.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Linq2Sql.cs similarity index 74% rename from Dapper.Tests.Performance/Benchmarks.Linq2Sql.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.Linq2Sql.cs index 64889f1e1..d6cc55049 100644 --- a/Dapper.Tests.Performance/Benchmarks.Linq2Sql.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Linq2Sql.cs @@ -1,14 +1,18 @@ +#if NET4X using BenchmarkDotNet.Attributes; using Dapper.Tests.Performance.Linq2Sql; using System; +using System.ComponentModel; using System.Data.Linq; using System.Linq; namespace Dapper.Tests.Performance { - public class Linq2SqlBenchmarks : BenchmarkBase + [Description("LINQ to SQL")] + public class LinqToSqlBenchmarks : BenchmarkBase // note To not 2 because the "2" confuses BDN CLI { private DataClassesDataContext Linq2SqlContext; + private static readonly Func compiledQuery = CompiledQuery.Compile((DataClassesDataContext ctx, int id) => ctx.Posts.First(p => p.Id == id)); @@ -19,14 +23,14 @@ public void Setup() Linq2SqlContext = new DataClassesDataContext(_connection); } - [Benchmark(Description = "Normal")] - public Linq2Sql.Post Normal() + [Benchmark(Description = "First")] + public Linq2Sql.Post First() { Step(); return Linq2SqlContext.Posts.First(p => p.Id == i); } - [Benchmark(Description = "Compiled")] + [Benchmark(Description = "First (Compiled)")] public Linq2Sql.Post Compiled() { Step(); @@ -40,4 +44,5 @@ public Post ExecuteQuery() return Linq2SqlContext.ExecuteQuery("select * from Posts where Id = {0}", i).First(); } } -} \ No newline at end of file +} +#endif diff --git a/Dapper.Tests.Performance/Benchmarks.Massive.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Massive.cs similarity index 77% rename from Dapper.Tests.Performance/Benchmarks.Massive.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.Massive.cs index ac680ed90..a0ee11e10 100644 --- a/Dapper.Tests.Performance/Benchmarks.Massive.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Massive.cs @@ -1,9 +1,11 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using Massive; +using System.ComponentModel; using System.Linq; namespace Dapper.Tests.Performance { + [Description("Massive")] public class MassiveBenchmarks : BenchmarkBase { private DynamicModel _model; @@ -12,6 +14,7 @@ public class MassiveBenchmarks : BenchmarkBase public void Setup() { BaseSetup(); + RegisterSqlFactory(); _model = new DynamicModel(ConnectionString); } @@ -22,4 +25,4 @@ public dynamic QueryDynamic() return _model.Query("select * from Posts where Id = @0", _connection, i).First(); } } -} \ No newline at end of file +} diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.Mighty.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Mighty.cs new file mode 100644 index 000000000..738188151 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Mighty.cs @@ -0,0 +1,55 @@ +using BenchmarkDotNet.Attributes; +using Mighty; +using System.ComponentModel; +using System.Linq; + +namespace Dapper.Tests.Performance +{ + [Description("Mighty")] + public class MightyBenchmarks : BenchmarkBase + { + private MightyOrm _model; + private MightyOrm _dynamicModel; + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + + // Mighty needs the connection string to contain the ProviderName in addition to everything else for Reasons. + // However, it appears the SQL Server driver chokes on it if it's in the full connection string, so we programatically add it here. + var connectionString = $"{ConnectionStringSettings.ConnectionString};ProviderName={ConnectionStringSettings.ProviderName}"; + + _model = new MightyOrm(connectionString); + _dynamicModel = new MightyOrm(connectionString); + } + + [Benchmark(Description = "Query")] + public Post Query() + { + Step(); + return _model.Query("select * from Posts where Id = @0", _connection, i).First(); + } + + [Benchmark(Description = "Query")] + public dynamic QueryDynamic() + { + Step(); + return _dynamicModel.Query("select * from Posts where Id = @0", _connection, i).First(); + } + + [Benchmark(Description = "SingleFromQuery")] + public Post SingleFromQuery() + { + Step(); + return _model.SingleFromQuery("select * from Posts where Id = @0", _connection, i); + } + + [Benchmark(Description = "SingleFromQuery")] + public dynamic SingleFromQueryDynamic() + { + Step(); + return _dynamicModel.SingleFromQuery("select * from Posts where Id = @0", _connection, i); + } + } +} diff --git a/Dapper.Tests.Performance/Benchmarks.NHibernate.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.NHibernate.cs similarity index 100% rename from Dapper.Tests.Performance/Benchmarks.NHibernate.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.NHibernate.cs diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.Norm.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Norm.cs new file mode 100644 index 000000000..34fed76d8 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Norm.cs @@ -0,0 +1,41 @@ +#if !NET4X +using BenchmarkDotNet.Attributes; +using System.ComponentModel; +using System.Linq; +using Norm; +using System; + +namespace Dapper.Tests.Performance +{ + [Description("Norm")] + public class NormBenchmarks : BenchmarkBase + { + [GlobalSetup] + public void Setup() + { + BaseSetup(); + } + + [Benchmark(Description = "Read<> (class)")] + public Post Read() + { + Step(); + return _connection.Read("select * from Posts where Id = @Id", i).First(); + } + + [Benchmark(Description = "Read<> (tuples)")] + public (int, string, DateTime, DateTime, int?, int?, int?, int?, int?, int?, int?, int?) ReadSimpleValues() + { + Step(); + return _connection.Read("select * from Posts where Id = @Id", i).First(); + } + + [Benchmark(Description = "Read<()> (named tuples)")] + public (int Id, string Text, DateTime CreationDate, DateTime LastChangeDate, int? Counter1, int? Counter2, int? Counter3, int? Counter4, int? Counter5, int? Counter6, int? Counter7, int? Counter8) ReadTuple() + { + Step(); + return _connection.Read<(int Id, string Text, DateTime CreationDate, DateTime LastChangeDate, int? Counter1, int? Counter2, int? Counter3, int? Counter4, int? Counter5, int? Counter6, int? Counter7, int? Counter8)>("select * from Posts where Id = @Id", i).First(); + } + } +} +#endif diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs new file mode 100644 index 000000000..2af4f52b3 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs @@ -0,0 +1,64 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using PetaPoco; +using System.ComponentModel; +using System.Linq; + +namespace Dapper.Tests.Performance +{ +#if !NET5_0_OR_GREATER +/* +System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. + ---> System.InvalidProgramException: Common Language Runtime detected an invalid program. + at System.Reflection.Emit.DynamicMethod.CreateDelegate(Type delegateType, Object target) + at PetaPoco.Database.PocoData.GetFactory[T](String key, Boolean ForceDateTimesToUtc, IDataReader r) in /_/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs:line 1127 + at PetaPoco.Database.Fetch[T](String sql, Object[] args) in /_/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs:line 458 + at Dapper.Tests.Performance.PetaPocoBenchmarks.FetchFast() in /_/benchmarks/Dapper.Tests.Performance/Benchmarks.PetaPoco.cs:line 38 + at BenchmarkDotNet.Autogenerated.Runnable_42.WorkloadActionUnroll(Int64 invokeCount) in /_/benchmarks/Dapper.Tests.Performance/bin/Release/net8.0/5e0d07b1-6b4c-4578-a0eb-d46563cab999/5e0d07b1-6b4c-4578-a0eb-d46563cab999.notcs:line 49834 + at BenchmarkDotNet.Engines.Engine.RunIteration(IterationData data) + at BenchmarkDotNet.Engines.EngineFactory.Jit(Engine engine, Int32 jitIndex, Int32 invokeCount, Int32 unrollFactor) + at BenchmarkDotNet.Engines.EngineFactory.CreateReadyToRun(EngineParameters engineParameters) + at BenchmarkDotNet.Autogenerated.Runnable_42.Run(IHost host, String benchmarkName) in /_/benchmarks/Dapper.Tests.Performance/bin/Release/net8.0/5e0d07b1-6b4c-4578-a0eb-d46563cab999/5e0d07b1-6b4c-4578-a0eb-d46563cab999.notcs:line 49238 + at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor) + at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr) + --- End of inner exception stack trace --- + at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr) + at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) + at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) + at BenchmarkDotNet.Autogenerated.UniqueProgramName.AfterAssemblyLoadingAttached(String[] args) in /_/benchmarks/Dapper.Tests.Performance/bin/Release/net8.0/5e0d07b1-6b4c-4578-a0eb-d46563cab999/5e0d07b1-6b4c-4578-a0eb-d46563cab999.notcs:line 57 +*/ + [Description("PetaPoco")] + public class PetaPocoBenchmarks : BenchmarkBase + { + private Database _db, _dbFast; + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + RegisterSqlFactory(); + _db = new Database(ConnectionString, "System.Data.SqlClient"); + _db.OpenSharedConnection(); + _dbFast = new Database(ConnectionString, "System.Data.SqlClient"); + _dbFast.OpenSharedConnection(); + _dbFast.EnableAutoSelect = false; + _dbFast.EnableNamedParams = false; + _dbFast.ForceDateTimesToUtc = false; + } + + [Benchmark(Description = "Fetch")] + public Post Fetch() + { + Step(); + return _db.Fetch("SELECT * from Posts where Id=@0", i).First(); + } + + [Benchmark(Description = "Fetch (Fast)")] + public Post FetchFast() + { + Step(); + return _dbFast.Fetch("SELECT * from Posts where Id=@0", i).First(); + } + } +#endif +} diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs new file mode 100644 index 000000000..dd762afcf --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.RepoDB.cs @@ -0,0 +1,67 @@ +using System.ComponentModel; +using System.Linq; +using BenchmarkDotNet.Attributes; +using RepoDb; +using RepoDb.DbHelpers; +using RepoDb.DbSettings; +using RepoDb.StatementBuilders; + +namespace Dapper.Tests.Performance +{ + [Description("RepoDB")] + public class RepoDbBenchmarks : BenchmarkBase + { + [GlobalSetup] + public void Setup() + { + BaseSetup(); + GlobalConfiguration.Setup().UseSqlServer(); + + // We need this since benchmarks using System.Data.SqlClient + var dbSetting = new SqlServerDbSetting(); + DbSettingMapper + .Add(dbSetting, true); + DbHelperMapper + .Add(new SqlServerDbHelper(), true); + StatementBuilderMapper + .Add(new SqlServerStatementBuilder(dbSetting), true); + + ClassMapper.Add("Posts"); + } + + [Benchmark(Description = "Query")] + public Post Query() + { + Step(); + return _connection.Query(i).First(); + } + + [Benchmark(Description = "QueryWhere")] + public Post QueryWhere() + { + Step(); + return _connection.Query(x => x.Id == i).First(); + } + + [Benchmark(Description = "QueryDynamic")] + public Post QueryDynamic() + { + Step(); + return _connection.Query(new { Id = i }).First(); + } + + [Benchmark(Description = "QueryField")] + public Post QueryField() + { + Step(); + return _connection.Query([new(nameof(Post.Id), i)]).First(); + } + + [Benchmark(Description = "ExecuteQuery")] + public Post ExecuteQuery() + { + Step(); + return _connection.ExecuteQuery("select * from Posts where Id = @Id", new { Id = i }).First(); + } + } +} diff --git a/Dapper.Tests.Performance/Benchmarks.ServiceStack.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.ServiceStack.cs similarity index 74% rename from Dapper.Tests.Performance/Benchmarks.ServiceStack.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.ServiceStack.cs index 41426e64b..d3bd0e024 100644 --- a/Dapper.Tests.Performance/Benchmarks.ServiceStack.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.ServiceStack.cs @@ -1,9 +1,11 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using ServiceStack.OrmLite; +using System.ComponentModel; using System.Data; namespace Dapper.Tests.Performance { + [Description("ServiceStack")] public class ServiceStackBenchmarks : BenchmarkBase { private IDbConnection _db; @@ -16,11 +18,11 @@ public void Setup() _db = dbFactory.Open(); } - [Benchmark(Description = "SingleById")] + [Benchmark(Description = "SingleById")] public Post Query() { Step(); return _db.SingleById(i); } } -} \ No newline at end of file +} diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.SqlMarshal.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.SqlMarshal.cs new file mode 100644 index 000000000..e1f98976f --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.SqlMarshal.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using System.ComponentModel; + +namespace Dapper.Tests.Performance +{ + [Description("SqlMarshal")] + public partial class SqlMarshalBenchmarks : BenchmarkBase + { + [GlobalSetup] + public void Setup() + { + BaseSetup(); + } + + [Benchmark(Description = "SqlCommand")] + public Post SqlCommand() + { + Step(); + return ReadPost("select Top 1 * from Posts where Id = @id", i); + } + + [SqlMarshal("")] + private partial Post ReadPost([RawSql]string sql, int id); + } +} diff --git a/Dapper.Tests.Performance/Benchmarks.Susanoo.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.Susanoo.cs similarity index 83% rename from Dapper.Tests.Performance/Benchmarks.Susanoo.cs rename to benchmarks/Dapper.Tests.Performance/Benchmarks.Susanoo.cs index 934d3b644..81ebab56d 100644 --- a/Dapper.Tests.Performance/Benchmarks.Susanoo.cs +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.Susanoo.cs @@ -1,18 +1,23 @@ +#if NET4X using BenchmarkDotNet.Attributes; using Susanoo; using Susanoo.Processing; +using System.ComponentModel; using System.Data; using System.Linq; namespace Dapper.Tests.Performance { + [Description("Susanoo")] public class SusanooBenchmarks : BenchmarkBase { private DatabaseManager _db; + private static readonly ISingleResultSetCommandProcessor _cmd = CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) .DefineResults() .Realize(); + private static readonly ISingleResultSetCommandProcessor _cmdDynamic = CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) .DefineResults() @@ -25,7 +30,7 @@ public void Setup() _db = new DatabaseManager(_connection); } - [Benchmark(Description = "Mapping Cache")] + [Benchmark(Description = "Execute (Cache)")] public Post MappingCache() { Step(); @@ -35,7 +40,7 @@ public Post MappingCache() .Execute(_db, new { Id = i }).First(); } - [Benchmark(Description = "Mapping Cache (dynamic)")] + [Benchmark(Description = "Execute (Cache)")] public dynamic MappingCacheDynamic() { Step(); @@ -45,18 +50,19 @@ public dynamic MappingCacheDynamic() .Execute(_db, new { Id = i }).First(); } - [Benchmark(Description = "Mapping Static")] + [Benchmark(Description = "Execute (Static)")] public Post MappingStatic() { Step(); return _cmd.Execute(_db, new { Id = i }).First(); } - [Benchmark(Description = "Mapping Static (dynamic)")] + [Benchmark(Description = "Execut (Static)")] public dynamic MappingStaticDynamic() { Step(); return _cmdDynamic.Execute(_db, new { Id = i }).First(); } } -} \ No newline at end of file +} +#endif diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.XPO.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.XPO.cs new file mode 100644 index 000000000..d4bf36ce5 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.XPO.cs @@ -0,0 +1,62 @@ +using BenchmarkDotNet.Attributes; + +using System; +using System.Linq; +using System.ComponentModel; +using DevExpress.Xpo; +using DevExpress.Data.Filtering; + +namespace Dapper.Tests.Performance +{ + [Description("DevExpress.XPO")] + public class XpoBenchmarks : BenchmarkBase + { + public UnitOfWork _session; + + [GlobalSetup] + public void Setup() + { + BaseSetup(); + IDataLayer dataLayer = XpoDefault.GetDataLayer(_connection, DevExpress.Xpo.DB.AutoCreateOption.SchemaAlreadyExists); + dataLayer.Dictionary.GetDataStoreSchema(typeof(Xpo.Post)); + _session = new UnitOfWork(dataLayer, dataLayer) + { + IdentityMapBehavior = IdentityMapBehavior.Strong + }; + _session.TypesManager.EnsureIsTypedObjectValid(); + } + + [GlobalCleanup] + public void Cleanup() + { + _session.Dispose(); + } + + [Benchmark(Description = "GetObjectByKey")] + public Xpo.Post GetObjectByKey() + { + Step(); + return _session.GetObjectByKey(i, true); + } + + [Benchmark(Description = "FindObject")] + public Xpo.Post FindObject() + { + Step(); + CriteriaOperator _findCriteria = new BinaryOperator() + { + OperatorType = BinaryOperatorType.Equal, + LeftOperand = new OperandProperty("Id"), + RightOperand = new ConstantValue(i) + }; + return _session.FindObject(_findCriteria); + } + + [Benchmark(Description = "Query")] + public Xpo.Post Query() + { + Step(); + return _session.Query().First(p => p.Id == i); + } + } +} diff --git a/benchmarks/Dapper.Tests.Performance/Benchmarks.cs b/benchmarks/Dapper.Tests.Performance/Benchmarks.cs new file mode 100644 index 000000000..8a92b5e23 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Benchmarks.cs @@ -0,0 +1,37 @@ +using BenchmarkDotNet.Attributes; +using System; +using System.Configuration; +using Microsoft.Data.SqlClient; + +namespace Dapper.Tests.Performance +{ + [BenchmarkCategory("ORM")] + public abstract class BenchmarkBase + { + protected static readonly Random _rand = new Random(); + protected SqlConnection _connection; + public static ConnectionStringSettings ConnectionStringSettings { get; } = ConfigurationManager.ConnectionStrings["Main"]; + public static string ConnectionString { get; } = ConnectionStringSettings.ConnectionString; + protected int i; + + protected void BaseSetup() + { + i = 0; + _connection = new SqlConnection(ConnectionString); + _connection.Open(); + } + + protected void RegisterSqlFactory() + { +#if NETCOREAPP + System.Data.Common.DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance); +#endif + } + + protected void Step() + { + i++; + if (i > 5000) i = 1; + } + } +} diff --git a/benchmarks/Dapper.Tests.Performance/Config.cs b/benchmarks/Dapper.Tests.Performance/Config.cs new file mode 100644 index 000000000..b25eb3b82 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Config.cs @@ -0,0 +1,46 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Order; +using Dapper.Tests.Performance.Helpers; + +namespace Dapper.Tests.Performance +{ + public class Config : ManualConfig + { + public const int Iterations = 500; + + public Config() + { + AddLogger(ConsoleLogger.Default); + + AddExporter(CsvExporter.Default); + AddExporter(MarkdownExporter.GitHub); + AddExporter(HtmlExporter.Default); + + var md = MemoryDiagnoser.Default; + AddDiagnoser(md); + AddColumn(new ORMColum()); + AddColumn(TargetMethodColumn.Method); + AddColumn(new ReturnColum()); + AddColumn(StatisticColumn.Mean); + AddColumn(StatisticColumn.StdDev); + AddColumn(StatisticColumn.Error); + AddColumn(BaselineRatioColumn.RatioMean); + AddColumnProvider(DefaultColumnProviders.Metrics); + + AddJob(Job.ShortRun + .WithLaunchCount(1) + .WithWarmupCount(2) + .WithUnrollFactor(Iterations) + .WithIterationCount(10) + ); + Orderer = new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest); + Options |= ConfigOptions.JoinSummary; + } + } +} diff --git a/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj b/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj new file mode 100644 index 000000000..51c28957a --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj @@ -0,0 +1,70 @@ + + + Dapper.Tests.Performance + Dapper Core Performance Suite + Exe + net472;net10.0 + false + $(NoWarn);IDE0063;IDE0034;IDE0059;IDE0060 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(DefineConstants);NET4X + + + + + + + + + + + + + + + diff --git a/benchmarks/Dapper.Tests.Performance/DapperCacheImpact.cs b/benchmarks/Dapper.Tests.Performance/DapperCacheImpact.cs new file mode 100644 index 000000000..5386a2833 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/DapperCacheImpact.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using BenchmarkDotNet.Attributes; + +namespace Dapper.Tests.Performance +{ + [Description("Dapper cache impact")] + [MemoryDiagnoser] + public class DapperCacheImpact : BenchmarkBase + { + [GlobalSetup] + public void Setup() => BaseSetup(); + + private readonly object args = new { Id = 42, Name = "abc" }; + + public class Foo + { + public int Id { get; set; } + public string Name { get; set; } + } + + // note: custom BDN setup means [Params] is awkward; unroll manually instead + [Benchmark] + public void ExecuteNoParameters_Cache() => _connection.Execute(new CommandDefinition("select '42' as Id, 'abc' as Name", flags: CommandFlags.None)); + [Benchmark] + public void ExecuteParameters_Cache() => _connection.Execute(new CommandDefinition("select @id as Id, @name as Name", args, flags: CommandFlags.None)); + [Benchmark] + public void QueryFirstNoParameters_Cache() => _connection.QueryFirst(new CommandDefinition("select '42' as Id, 'abc' as Name", flags: CommandFlags.None)); + [Benchmark] + public void QueryFirstParameters_Cache() => _connection.QueryFirst(new CommandDefinition("select @id as Id, @name as Name", args, flags: CommandFlags.None)); + [Benchmark] + public void ExecuteNoParameters_NoCache() => _connection.Execute(new CommandDefinition("select '42' as Id, 'abc' as Name", flags: CommandFlags.NoCache)); + [Benchmark] + public void ExecuteParameters_NoCache() => _connection.Execute(new CommandDefinition("select @id as Id, @name as Name", args, flags: CommandFlags.NoCache)); + [Benchmark] + public void QueryFirstNoParameters_NoCache() => _connection.QueryFirst(new CommandDefinition("select '42' as Id, 'abc' as Name", flags: CommandFlags.NoCache)); + [Benchmark] + public void QueryFirstParameters_NoCache() => _connection.QueryFirst(new CommandDefinition("select @id as Id, @name as Name", args, flags: CommandFlags.NoCache)); + } +} diff --git a/benchmarks/Dapper.Tests.Performance/Dashing/DashingConfiguration.cs b/benchmarks/Dapper.Tests.Performance/Dashing/DashingConfiguration.cs new file mode 100644 index 000000000..03d8f6f34 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Dashing/DashingConfiguration.cs @@ -0,0 +1,12 @@ +using Dashing.Configuration; + +namespace Dapper.Tests.Performance.Dashing +{ + public class DashingConfiguration : BaseConfiguration + { + public DashingConfiguration() + { + Add(); + } + } +} diff --git a/Dapper.Tests.Performance/Post.cs b/benchmarks/Dapper.Tests.Performance/Dashing/Post.cs similarity index 77% rename from Dapper.Tests.Performance/Post.cs rename to benchmarks/Dapper.Tests.Performance/Dashing/Post.cs index 78c71ccd7..e6a253ea5 100644 --- a/Dapper.Tests.Performance/Post.cs +++ b/benchmarks/Dapper.Tests.Performance/Dashing/Post.cs @@ -1,13 +1,9 @@ using System; -using Soma.Core; -namespace Dapper.Tests.Performance +namespace Dapper.Tests.Performance.Dashing { - [ServiceStack.DataAnnotations.Alias("Posts")] - [Table(Name = "Posts")] public class Post { - [Id(IdKind.Identity)] public int Id { get; set; } public string Text { get; set; } public DateTime CreationDate { get; set; } diff --git a/Dapper.Tests.Performance/EntityFramework/EFContext.cs b/benchmarks/Dapper.Tests.Performance/EntityFramework/EFContext.cs similarity index 100% rename from Dapper.Tests.Performance/EntityFramework/EFContext.cs rename to benchmarks/Dapper.Tests.Performance/EntityFramework/EFContext.cs diff --git a/Dapper.Tests.Performance/EntityFrameworkCore/EFCoreContext.cs b/benchmarks/Dapper.Tests.Performance/EntityFrameworkCore/EFCoreContext.cs similarity index 100% rename from Dapper.Tests.Performance/EntityFrameworkCore/EFCoreContext.cs rename to benchmarks/Dapper.Tests.Performance/EntityFrameworkCore/EFCoreContext.cs diff --git a/Dapper.Tests.Performance/Helpers/ORMColum.cs b/benchmarks/Dapper.Tests.Performance/Helpers/ORMColum.cs similarity index 51% rename from Dapper.Tests.Performance/Helpers/ORMColum.cs rename to benchmarks/Dapper.Tests.Performance/Helpers/ORMColum.cs index a2e610185..574ba7894 100644 --- a/Dapper.Tests.Performance/Helpers/ORMColum.cs +++ b/benchmarks/Dapper.Tests.Performance/Helpers/ORMColum.cs @@ -1,4 +1,6 @@ -using BenchmarkDotNet.Columns; +using System.ComponentModel; +using System.Reflection; +using BenchmarkDotNet.Columns; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; @@ -10,9 +12,14 @@ public class ORMColum : IColumn public string ColumnName { get; } = "ORM"; public string Legend => "The object/relational mapper being tested"; - public bool IsDefault(Summary summary, Benchmark benchmark) => false; - public string GetValue(Summary summary, Benchmark benchmark) => benchmark.Target.Method.DeclaringType.Name.Replace("Benchmarks", string.Empty); - public string GetValue(Summary summary, Benchmark benchmark, ISummaryStyle style) => benchmark.Target.Method.DeclaringType.Name.Replace("Benchmarks", string.Empty); + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var type = benchmarkCase.Descriptor.WorkloadMethod.DeclaringType; + return type.GetCustomAttribute()?.Description ?? type.Name.Replace("Benchmarks", string.Empty); + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) => GetValue(summary, benchmarkCase); public bool IsAvailable(Summary summary) => true; public bool AlwaysShow => true; diff --git a/Dapper.Tests.Performance/Helpers/ReturnColum.cs b/benchmarks/Dapper.Tests.Performance/Helpers/ReturnColum.cs similarity index 59% rename from Dapper.Tests.Performance/Helpers/ReturnColum.cs rename to benchmarks/Dapper.Tests.Performance/Helpers/ReturnColum.cs index 26105c4fe..b646770c9 100644 --- a/Dapper.Tests.Performance/Helpers/ReturnColum.cs +++ b/benchmarks/Dapper.Tests.Performance/Helpers/ReturnColum.cs @@ -10,9 +10,14 @@ public class ReturnColum : IColumn public string ColumnName { get; } = "Return"; public string Legend => "The return type of the method"; - public bool IsDefault(Summary summary, Benchmark benchmark) => false; - public string GetValue(Summary summary, Benchmark benchmark) => benchmark.Target.Method.ReturnType.Name; - public string GetValue(Summary summary, Benchmark benchmark, ISummaryStyle style) => benchmark.Target.Method.ReturnType.Name; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var type = benchmarkCase.Descriptor.WorkloadMethod.ReturnType; + return type == typeof(object) ? "dynamic" : type.Name; + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) => GetValue(summary, benchmarkCase); public bool IsAvailable(Summary summary) => true; public bool AlwaysShow => true; diff --git a/Dapper.Tests.Performance/LegacyTests.cs b/benchmarks/Dapper.Tests.Performance/LegacyTests.cs similarity index 85% rename from Dapper.Tests.Performance/LegacyTests.cs rename to benchmarks/Dapper.Tests.Performance/LegacyTests.cs index 190b5d9f1..9bf4cdb46 100644 --- a/Dapper.Tests.Performance/LegacyTests.cs +++ b/benchmarks/Dapper.Tests.Performance/LegacyTests.cs @@ -1,26 +1,33 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.Linq; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using System.Diagnostics; using System.Linq; -//using BLToolkit.Data; // Note: this doesn't load in the new .csproj system...likely a bug +using Belgrade.SqlClient; +using Dapper.Contrib.Extensions; +using Dapper.Tests.Performance.Dashing; using Dapper.Tests.Performance.EntityFramework; -using Dapper.Tests.Performance.Linq2Sql; +using Dapper.Tests.Performance.EntityFrameworkCore; using Dapper.Tests.Performance.NHibernate; -using Dapper.Contrib.Extensions; +using Dashing; +using DevExpress.Xpo; +using DevExpress.Data.Filtering; using Massive; +using Microsoft.EntityFrameworkCore; using NHibernate.Criterion; -using NHibernate.Linq; using ServiceStack.OrmLite; using ServiceStack.OrmLite.Dapper; -using Susanoo; using System.Configuration; using System.Threading.Tasks; -using Dapper.Tests.Performance.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; +#if NET4X +using System.Data.Linq; +using Dapper.Tests.Performance.Linq2Sql; +using Dapper.Tests.Performance.Xpo; +using NHibernate.Linq; +using Susanoo; +#endif namespace Dapper.Tests.Performance { @@ -81,12 +88,16 @@ public async Task RunAsync(int iterations) } } + Console.WriteLine("|Time|Framework|"); foreach (var test in this.OrderBy(t => t.Watch.ElapsedMilliseconds)) { var ms = test.Watch.ElapsedMilliseconds.ToString(); + Console.Write("|"); Console.Write(ms); Program.WriteColor("ms ".PadRight(8 - ms.Length), ConsoleColor.DarkGray); - Console.WriteLine(test.Name); + Console.Write("|"); + Console.Write(test.Name); + Console.WriteLine("|"); } } } @@ -100,8 +111,10 @@ public static SqlConnection GetOpenConnection() return connection; } +#if NET4X private static DataClassesDataContext GetL2SContext(SqlConnection connection) => new DataClassesDataContext(connection); +#endif private static void Try(Action action, string blame) { @@ -115,6 +128,7 @@ private static void Try(Action action, string blame) } } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Intentional - just make sure we have something")] public async Task RunAsync(int iterations) { using (var connection = GetOpenConnection()) @@ -123,33 +137,6 @@ public async Task RunAsync(int iterations) #pragma warning disable RCS1121 // Use [] instead of calling 'First'. var tests = new Tests(); - // Linq2SQL - Try(() => - { - var l2scontext1 = GetL2SContext(connection); - tests.Add(id => l2scontext1.Posts.First(p => p.Id == id), "Linq2Sql: Normal"); - - var l2scontext2 = GetL2SContext(connection); - var compiledGetPost = CompiledQuery.Compile((Linq2Sql.DataClassesDataContext ctx, int id) => ctx.Posts.First(p => p.Id == id)); - tests.Add(id => compiledGetPost(l2scontext2, id), "Linq2Sql: Compiled"); - - var l2scontext3 = GetL2SContext(connection); - tests.Add(id => l2scontext3.ExecuteQuery("select * from Posts where Id = {0}", id).First(), "Linq2Sql: ExecuteQuery"); - }, "LINQ-to-SQL"); - - // Entity Framework - Try(() => - { - var entityContext = new EFContext(connection); - tests.Add(id => entityContext.Posts.First(p => p.Id == id), "Entity Framework"); - - var entityContext2 = new EFContext(connection); - tests.Add(id => entityContext2.Database.SqlQuery("select * from Posts where Id = {0}", id).First(), "Entity Framework: SqlQuery"); - - var entityContext3 = new EFContext(connection); - tests.Add(id => entityContext3.Posts.AsNoTracking().First(p => p.Id == id), "Entity Framework: No Tracking"); - }, "Entity Framework"); - // Entity Framework Core Try(() => { @@ -157,9 +144,9 @@ public async Task RunAsync(int iterations) tests.Add(id => entityContext.Posts.First(p => p.Id == id), "Entity Framework Core"); var entityContext2 = new EFCoreContext(ConnectionString); - tests.Add(id => entityContext2.Posts.FromSql("select * from Posts where Id = {0}", id).First(), "Entity Framework Core: FromSql"); + tests.Add(id => entityContext2.Posts.FromSqlRaw("select * from Posts where Id = {0}", id).First(), "Entity Framework Core: FromSql"); - var entityContext3 = new EFContext(connection); + var entityContext3 = new EFCoreContext(ConnectionString); tests.Add(id => entityContext3.Posts.AsNoTracking().First(p => p.Id == id), "Entity Framework Core: No Tracking"); }, "Entity Framework Core"); @@ -233,18 +220,11 @@ public async Task RunAsync(int iterations) tests.Add(id => nhSession5.Get(id), "NHibernate: Session.Get"); }, "NHibernate"); - // Simple.Data - Try(() => - { - var sdb = Simple.Data.Database.OpenConnection(ConnectionString); - tests.Add(id => sdb.Posts.FindById(id).FirstOrDefault(), "Simple.Data"); - }, "Simple.Data"); - // Belgrade Try(() => { var query = new Belgrade.SqlClient.SqlDb.QueryMapper(ConnectionString); - tests.AsyncAdd(id => query.ExecuteReader("SELECT TOP 1 * FROM Posts WHERE Id = " + id, + tests.AsyncAdd(id => query.Sql("SELECT TOP 1 * FROM Posts WHERE Id = @Id").Param("Id", id).Map( reader => { var post = new Post(); @@ -265,40 +245,6 @@ public async Task RunAsync(int iterations) }), "Belgrade Sql Client"); }, "Belgrade Sql Client"); - //Susanoo - Try(() => - { - var susanooDb = new DatabaseManager(connection); - - var susanooPreDefinedCommand = - CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) - .DefineResults() - .Realize(); - - var susanooDynamicPreDefinedCommand = - CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) - .DefineResults() - .Realize(); - - tests.Add(Id => - CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) - .DefineResults() - .Realize() - .Execute(susanooDb, new { Id }).First(), "Susanoo: Mapping Cache Retrieval"); - - tests.Add(Id => - CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) - .DefineResults() - .Realize() - .Execute(susanooDb, new { Id }).First(), "Susanoo: Dynamic Mapping Cache Retrieval"); - - tests.Add(Id => susanooDynamicPreDefinedCommand - .Execute(susanooDb, new { Id }).First(), "Susanoo: Dynamic Mapping Static"); - - tests.Add(Id => susanooPreDefinedCommand - .Execute(susanooDb, new { Id }).First(), "Susanoo: Mapping Static"); - }, "Susanoo"); - //ServiceStack's OrmLite: Try(() => { @@ -343,7 +289,6 @@ public async Task RunAsync(int iterations) } }, "Hand Coded"); -#if !NETSTANDARD1_3 var table = new DataTable { Columns = @@ -374,9 +319,103 @@ public async Task RunAsync(int iterations) table.Rows.Add(values); } }, "DataTable via IDataReader.GetValues"); -#endif }, "Hand Coded"); + // DevExpress.XPO + Try(() => + { + IDataLayer dataLayer = XpoDefault.GetDataLayer(connection, DevExpress.Xpo.DB.AutoCreateOption.SchemaAlreadyExists); + dataLayer.Dictionary.GetDataStoreSchema(typeof(Xpo.Post)); + UnitOfWork session = new UnitOfWork(dataLayer, dataLayer); + session.IdentityMapBehavior = IdentityMapBehavior.Strong; + session.TypesManager.EnsureIsTypedObjectValid(); + + tests.Add(id => session.Query().First(p => p.Id == id), "DevExpress.XPO: Query"); + tests.Add(id => session.GetObjectByKey(id, true), "DevExpress.XPO: GetObjectByKey"); + tests.Add(id => + { + CriteriaOperator findCriteria = new BinaryOperator() + { + OperatorType = BinaryOperatorType.Equal, + LeftOperand = new OperandProperty("Id"), + RightOperand = new ConstantValue(id) + }; + session.FindObject(findCriteria); + }, "DevExpress.XPO: FindObject"); + }, "DevExpress.XPO"); + + // Entity Framework + Try(() => + { + var entityContext = new EFContext(connection); + tests.Add(id => entityContext.Posts.First(p => p.Id == id), "Entity Framework"); + + var entityContext2 = new EFContext(connection); + tests.Add(id => entityContext2.Database.SqlQuery("select * from Posts where Id = {0}", id).First(), "Entity Framework: SqlQuery"); + + var entityContext3 = new EFContext(connection); + tests.Add(id => entityContext3.Posts.AsNoTracking().First(p => p.Id == id), "Entity Framework: No Tracking"); + }, "Entity Framework"); + +#if NET4X + // Linq2SQL + Try(() => + { + var l2scontext1 = GetL2SContext(connection); + tests.Add(id => l2scontext1.Posts.First(p => p.Id == id), "Linq2Sql: Normal"); + + var l2scontext2 = GetL2SContext(connection); + var compiledGetPost = CompiledQuery.Compile((Linq2Sql.DataClassesDataContext ctx, int id) => ctx.Posts.First(p => p.Id == id)); + tests.Add(id => compiledGetPost(l2scontext2, id), "Linq2Sql: Compiled"); + + var l2scontext3 = GetL2SContext(connection); + tests.Add(id => l2scontext3.ExecuteQuery("select * from Posts where Id = {0}", id).First(), "Linq2Sql: ExecuteQuery"); + }, "LINQ-to-SQL"); + + // Dashing + Try(() => + { + var config = new DashingConfiguration(); + var database = new SqlDatabase(config, ConnectionString); + var session = database.BeginTransactionLessSession(GetOpenConnection()); + tests.Add(id => session.Get(id), "Dashing Get"); + }, "Dashing"); + + //Susanoo + Try(() => + { + var susanooDb = new DatabaseManager(connection); + + var susanooPreDefinedCommand = + CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) + .DefineResults() + .Realize(); + + var susanooDynamicPreDefinedCommand = + CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) + .DefineResults() + .Realize(); + + tests.Add(Id => + CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) + .DefineResults() + .Realize() + .Execute(susanooDb, new { Id }).First(), "Susanoo: Mapping Cache Retrieval"); + + tests.Add(Id => + CommandManager.Instance.DefineCommand("SELECT * FROM Posts WHERE Id = @Id", CommandType.Text) + .DefineResults() + .Realize() + .Execute(susanooDb, new { Id }).First(), "Susanoo: Dynamic Mapping Cache Retrieval"); + + tests.Add(Id => susanooDynamicPreDefinedCommand + .Execute(susanooDb, new { Id }).First(), "Susanoo: Dynamic Mapping Static"); + + tests.Add(Id => susanooPreDefinedCommand + .Execute(susanooDb, new { Id }).First(), "Susanoo: Mapping Static"); + }, "Susanoo"); +#endif + // Subsonic isn't maintained anymore - doesn't import correctly //Try(() => // { diff --git a/benchmarks/Dapper.Tests.Performance/Linq2DB/ConnectionStringSettings.cs b/benchmarks/Dapper.Tests.Performance/Linq2DB/ConnectionStringSettings.cs new file mode 100644 index 000000000..cf2ebc062 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Linq2DB/ConnectionStringSettings.cs @@ -0,0 +1,12 @@ +using LinqToDB.Configuration; + +namespace Dapper.Tests.Performance.Linq2Db +{ + public class ConnectionStringSettings : IConnectionStringSettings + { + public string ConnectionString { get; set; } + public string Name { get; set; } + public string ProviderName { get; set; } + public bool IsGlobal => false; + } +} \ No newline at end of file diff --git a/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs b/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs new file mode 100644 index 000000000..43d491786 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DBContext.cs @@ -0,0 +1,9 @@ +using LinqToDB; + +namespace Dapper.Tests.Performance.Linq2Db +{ + public class Linq2DBContext : LinqToDB.Data.DataConnection + { + public ITable Posts => this.GetTable(); + } +} diff --git a/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DbSettings.cs b/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DbSettings.cs new file mode 100644 index 000000000..4c8103b76 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Linq2DB/Linq2DbSettings.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using LinqToDB.Configuration; + +namespace Dapper.Tests.Performance.Linq2Db +{ + public class Linq2DBSettings : ILinqToDBSettings + { + private readonly string _connectionString; + public IEnumerable DataProviders => Enumerable.Empty(); + + public string DefaultConfiguration => "SqlServer"; + public string DefaultDataProvider => "SqlServer"; + + public Linq2DBSettings(string connectionString) + { + _connectionString = connectionString; + } + + public IEnumerable ConnectionStrings + { + get + { + yield return + new ConnectionStringSettings + { + Name = "SqlServer", + ProviderName = "SqlServer", + ConnectionString = _connectionString + }; + } + } + } +} diff --git a/Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml b/benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml similarity index 100% rename from Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml rename to benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml diff --git a/Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml.layout b/benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml.layout similarity index 100% rename from Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml.layout rename to benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.dbml.layout diff --git a/Dapper.Tests.Performance/Linq2Sql/DataClasses.designer.cs b/benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.designer.cs similarity index 96% rename from Dapper.Tests.Performance/Linq2Sql/DataClasses.designer.cs rename to benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.designer.cs index c8694e839..9067e3155 100644 --- a/Dapper.Tests.Performance/Linq2Sql/DataClasses.designer.cs +++ b/benchmarks/Dapper.Tests.Performance/Linq2Sql/DataClasses.designer.cs @@ -1,4 +1,4 @@ -#if !NETSTANDARD1_3 +#if NET4X #pragma warning disable 1591 //------------------------------------------------------------------------------ // diff --git a/Dapper.Tests.Performance/Massive/Massive.cs b/benchmarks/Dapper.Tests.Performance/Massive/Massive.cs similarity index 96% rename from Dapper.Tests.Performance/Massive/Massive.cs rename to benchmarks/Dapper.Tests.Performance/Massive/Massive.cs index e564fe5d7..f4c1c8192 100644 --- a/Dapper.Tests.Performance/Massive/Massive.cs +++ b/benchmarks/Dapper.Tests.Performance/Massive/Massive.cs @@ -1,5 +1,4 @@ -#if !NETSTANDARD1_3 -using System; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Data; @@ -116,9 +115,9 @@ public class DynamicModel { private readonly DbProviderFactory _factory; #pragma warning disable 0649 -#pragma warning disable RCS1169 // Mark field as read-only. +#pragma warning disable RCS1169,IDE0044 // Mark field as read-only. private string _connectionString; -#pragma warning restore RCS1169 // Mark field as read-only. +#pragma warning restore RCS1169,IDE0044 // Mark field as read-only. #pragma warning restore 0649 public DynamicModel(string connectionStringName = "", string tableName = "", string primaryKeyField = "") @@ -323,7 +322,6 @@ public virtual DbCommand CreateUpdateCommand(object o, object key) var settings = (IDictionary)expando; var sbKeys = new StringBuilder(); const string stub = "UPDATE {0} SET {1} WHERE {2} = @{3}"; - var args = new List(); var result = CreateCommand(stub, null); int counter = 0; foreach (var item in settings) @@ -454,4 +452,3 @@ public virtual dynamic Single(object key, string columns = "*") } } #pragma warning restore RCS1141 // Add parameter to documentation comment. -#endif diff --git a/Dapper.Tests.Performance/NHibernate/NHibernateHelper.cs b/benchmarks/Dapper.Tests.Performance/NHibernate/NHibernateHelper.cs similarity index 77% rename from Dapper.Tests.Performance/NHibernate/NHibernateHelper.cs rename to benchmarks/Dapper.Tests.Performance/NHibernate/NHibernateHelper.cs index 9c349211f..7da6eb12d 100644 --- a/Dapper.Tests.Performance/NHibernate/NHibernateHelper.cs +++ b/benchmarks/Dapper.Tests.Performance/NHibernate/NHibernateHelper.cs @@ -1,4 +1,5 @@ -using NHibernate; +using System.Reflection; +using NHibernate; using NHibernate.Cfg; namespace Dapper.Tests.Performance.NHibernate @@ -14,9 +15,8 @@ private static ISessionFactory SessionFactory if (_sessionFactory == null) { var configuration = new Configuration(); - configuration.Configure(@".\NHibernate\hibernate.cfg.xml"); + configuration.Configure(Assembly.GetExecutingAssembly(), "Dapper.Tests.Performance.NHibernate.hibernate.cfg.xml"); configuration.AddAssembly(typeof(Post).Assembly); - configuration.AddXmlFile(@".\NHibernate\Post.hbm.xml"); _sessionFactory = configuration.BuildSessionFactory(); } @@ -29,4 +29,4 @@ public static IStatelessSession OpenSession() return SessionFactory.OpenStatelessSession(); } } -} \ No newline at end of file +} diff --git a/Dapper.Tests.Performance/NHibernate/Post.hbm.xml b/benchmarks/Dapper.Tests.Performance/NHibernate/Post.hbm.xml similarity index 100% rename from Dapper.Tests.Performance/NHibernate/Post.hbm.xml rename to benchmarks/Dapper.Tests.Performance/NHibernate/Post.hbm.xml diff --git a/Dapper.Tests.Performance/NHibernate/hibernate.cfg.xml b/benchmarks/Dapper.Tests.Performance/NHibernate/hibernate.cfg.xml similarity index 100% rename from Dapper.Tests.Performance/NHibernate/hibernate.cfg.xml rename to benchmarks/Dapper.Tests.Performance/NHibernate/hibernate.cfg.xml diff --git a/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs b/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs similarity index 96% rename from Dapper.Tests.Performance/PetaPoco/PetaPoco.cs rename to benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs index 9f13ea3f7..2ffa3887c 100644 --- a/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs +++ b/benchmarks/Dapper.Tests.Performance/PetaPoco/PetaPoco.cs @@ -1,5 +1,4 @@ -#if !NETSTANDARD1_3 -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -613,10 +612,12 @@ public static bool SplitSqlForPaging(string sql, out string sqlCount, out string throw new Exception("Unable to parse SQL statement for paged query"); // Setup the paged result - var result = new Page(); - result.CurrentPage = page; - result.ItemsPerPage = itemsPerPage; - result.TotalItems = ExecuteScalar(sqlCount, args); + var result = new Page + { + CurrentPage = page, + ItemsPerPage = itemsPerPage, + TotalItems = ExecuteScalar(sqlCount, args) + }; result.TotalPages = result.TotalItems / itemsPerPage; if ((result.TotalItems % itemsPerPage) != 0) result.TotalPages++; @@ -830,7 +831,7 @@ public int Update(string tableName, string primaryKeyName, object poco, object p // Don't update the primary key, but grab the value if we don't have it if (i.Key == primaryKeyName) { - primaryKeyValue = primaryKeyValue ?? i.Value.PropertyInfo.GetValue(poco, null); + primaryKeyValue ??= i.Value.PropertyInfo.GetValue(poco, null); continue; } @@ -1019,10 +1020,9 @@ public string LastCommand { get { - var sb = new StringBuilder(); if (_lastSql == null) return ""; - sb.Append(_lastSql); + var sb = new StringBuilder(_lastSql); if (_lastArgs != null) { sb.Append("\r\n\r\n"); @@ -1037,14 +1037,14 @@ public string LastCommand public static IMapper Mapper { get; set; } - internal class PocoColumn + internal sealed class PocoColumn { public string ColumnName; public PropertyInfo PropertyInfo; public bool ResultColumn; } - internal class PocoData + internal sealed class PocoData { public static PocoData ForType(Type t) { @@ -1271,7 +1271,7 @@ public Func GetFactory(string key, bool ForceDateTimesToUtc, // ShareableConnection represents either a shared connection used by a transaction, // or a one-off connection if not in a transaction. // Non-shared connections are disposed - private class ShareableConnection : IDisposable + private sealed class ShareableConnection : IDisposable { public ShareableConnection(Database db) { @@ -1443,4 +1443,3 @@ public void Build(StringBuilder sb, List args, Sql lhs) } } #pragma warning restore RCS1023 // Format empty block. -#endif diff --git a/benchmarks/Dapper.Tests.Performance/Post.cs b/benchmarks/Dapper.Tests.Performance/Post.cs new file mode 100644 index 000000000..514f24ee1 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/Post.cs @@ -0,0 +1,36 @@ +using System; + +namespace Dapper.Tests.Performance +{ + [ServiceStack.DataAnnotations.Alias("Posts")] + [LinqToDB.Mapping.Table(Name = "Posts")] + public class Post + { + [LinqToDB.Mapping.PrimaryKey, LinqToDB.Mapping.Identity] + public int Id { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public string Text { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.NotNull] + public DateTime CreationDate { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.NotNull] + public DateTime LastChangeDate { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter1 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter2 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter3 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter4 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter5 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter6 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter7 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter8 { get; set; } + [LinqToDB.Mapping.Column, LinqToDB.Mapping.Nullable] + public int? Counter9 { get; set; } + } +} diff --git a/Dapper.Tests.Performance/Program.cs b/benchmarks/Dapper.Tests.Performance/Program.cs similarity index 69% rename from Dapper.Tests.Performance/Program.cs rename to benchmarks/Dapper.Tests.Performance/Program.cs index f44d965b0..4ffe7de9a 100644 --- a/Dapper.Tests.Performance/Program.cs +++ b/benchmarks/Dapper.Tests.Performance/Program.cs @@ -1,9 +1,7 @@ using BenchmarkDotNet.Running; using System; -using System.Collections.Generic; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using System.Linq; -using System.Reflection; using static System.Console; namespace Dapper.Tests.Performance @@ -18,36 +16,24 @@ public static void Main(string[] args) #endif WriteLine("Welcome to Dapper's ORM performance benchmark suite, based on BenchmarkDotNet."); Write(" If you find a problem, please report it at: "); - WriteLineColor("https://github.com/StackExchange/Dapper", ConsoleColor.Blue); + WriteLineColor("https://github.com/DapperLib/Dapper", ConsoleColor.Blue); WriteLine(" Or if you're up to it, please submit a pull request! We welcome new additions."); WriteLine(); if (args.Length == 0) { WriteLine("Optional arguments:"); - WriteColor(" --all", ConsoleColor.Blue); + WriteColor(" (no args)", ConsoleColor.Blue); WriteLine(": run all benchmarks"); WriteColor(" --legacy", ConsoleColor.Blue); - WriteLine(": run the legacy benchmark suite/format", ConsoleColor.Gray); + WriteLineColor(": run the legacy benchmark suite/format", ConsoleColor.Gray); WriteLine(); } WriteLine("Using ConnectionString: " + BenchmarkBase.ConnectionString); EnsureDBSetup(); WriteLine("Database setup complete."); - if (args.Any(a => a == "--all")) - { - WriteLine("Iterations: " + BenchmarkBase.Iterations); - var benchmarks = new List(); - var benchTypes = Assembly.GetEntryAssembly().DefinedTypes.Where(t => t.IsSubclassOf(typeof(BenchmarkBase))); - WriteLineColor("Running full benchmarks suite", ConsoleColor.Green); - foreach (var b in benchTypes) - { - benchmarks.AddRange(BenchmarkConverter.TypeToBenchmarks(b)); - } - BenchmarkRunner.Run(benchmarks.ToArray(), null); - } - else if (args.Any(a => a == "--legacy")) + if (args.Any(a => a == "--legacy")) { var test = new LegacyTests(); const int iterations = 500; @@ -58,8 +44,8 @@ public static void Main(string[] args) } else { - WriteLine("Iterations: " + BenchmarkBase.Iterations); - BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); + WriteLine("Iterations: " + Config.Iterations); + new BenchmarkSwitcher(typeof(BenchmarkBase).Assembly).Run(args, new Config()); } } diff --git a/Dapper.Tests.Performance/SqlDataReaderHelper.cs b/benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs similarity index 75% rename from Dapper.Tests.Performance/SqlDataReaderHelper.cs rename to benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs index a71375826..f7c215cda 100644 --- a/Dapper.Tests.Performance/SqlDataReaderHelper.cs +++ b/benchmarks/Dapper.Tests.Performance/SqlDataReaderHelper.cs @@ -1,10 +1,12 @@ using System; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; +using System.Runtime.CompilerServices; namespace Dapper.Tests.Performance { public static class SqlDataReaderHelper { + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetNullableString(this SqlDataReader reader, int index) { object tmp = reader.GetValue(index); @@ -15,6 +17,7 @@ public static string GetNullableString(this SqlDataReader reader, int index) return null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? GetNullableValue(this SqlDataReader reader, int index) where T : struct { object tmp = reader.GetValue(index); diff --git a/benchmarks/Dapper.Tests.Performance/XPO/Post.cs b/benchmarks/Dapper.Tests.Performance/XPO/Post.cs new file mode 100644 index 000000000..84fde9331 --- /dev/null +++ b/benchmarks/Dapper.Tests.Performance/XPO/Post.cs @@ -0,0 +1,25 @@ +using System; +using DevExpress.Xpo; + +namespace Dapper.Tests.Performance.Xpo +{ + [Persistent("Posts")] + public class Post : XPLiteObject + { + [Key(false)] + public int Id { get; set; } + public string Text { get; set; } + public DateTime CreationDate { get; set; } + public DateTime LastChangeDate { get; set; } + public int? Counter1 { get; set; } + public int? Counter2 { get; set; } + public int? Counter3 { get; set; } + public int? Counter4 { get; set; } + public int? Counter5 { get; set; } + public int? Counter6 { get; set; } + public int? Counter7 { get; set; } + public int? Counter8 { get; set; } + public int? Counter9 { get; set; } + public Post(Session session) : base(session) { } + } +} diff --git a/Dapper.Tests.Performance/app.config b/benchmarks/Dapper.Tests.Performance/app.config similarity index 63% rename from Dapper.Tests.Performance/app.config rename to benchmarks/Dapper.Tests.Performance/app.config index 5ea174a5c..9e85395f8 100644 --- a/Dapper.Tests.Performance/app.config +++ b/benchmarks/Dapper.Tests.Performance/app.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/benchmarks/Directory.Build.props b/benchmarks/Directory.Build.props new file mode 100644 index 000000000..ce39a387e --- /dev/null +++ b/benchmarks/Directory.Build.props @@ -0,0 +1,33 @@ + + + + false + false + false + false + $(DefineConstants);WINDOWS + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 8d81e3f6b..59193ff75 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,130 +1,43 @@ [CmdletBinding(PositionalBinding=$false)] param( - [string] $Version, - [string] $BuildNumber, [bool] $CreatePackages, [bool] $RunTests = $true, [string] $PullRequestNumber ) -function CalculateVersion() { - if ($version) { - return $version - } - - $semVersion = ''; - $path = $pwd; - while (!$semVersion) { - if (Test-Path (Join-Path $path "semver.txt")) { - $semVersion = Get-Content (Join-Path $path "semver.txt") - break - } - if ($PSScriptRoot -eq $path) { - break - } - $path = Split-Path $path -Parent - } - - if (!$semVersion) { - Write-Error "semver.txt was not found in $pwd or any parent directory" - Exit 1 - } - - if ($semVersion -contains "-") { - return "$semVersion-$BuildNumber" #prerelease - } else { - return "$semVersion" #release - } - -} - -if ($BuildNumber -and $BuildNumber.Length -lt 5) { - $BuildNumber = $BuildNumber.PadLeft(5, "0") -} - Write-Host "Run Parameters:" -ForegroundColor Cyan -Write-Host "Version: $Version" -Write-Host "BuildNumber: $BuildNumber" -Write-Host "CreatePackages: $CreatePackages" -Write-Host "RunTests: $RunTests" -Write-Host "Base Version: $(CalculateVersion)" +Write-Host " CreatePackages: $CreatePackages" +Write-Host " RunTests: $RunTests" +Write-Host " dotnet --version:" (dotnet --version) $packageOutputFolder = "$PSScriptRoot\.nupkgs" -$projectsToBuild = - 'Dapper', - 'Dapper.StrongName', - 'Dapper.Contrib', - 'Dapper.EntityFramework', - 'Dapper.EntityFramework.StrongName', - 'Dapper.Rainbow', - 'Dapper.SqlBuilder' - -$testsToRun = - 'Dapper.Tests', - 'Dapper.Tests.Contrib' - -if (!$Version -and !$BuildNumber) { - Write-Host "ERROR: You must supply either a -Version or -BuildNumber argument. ` - Use -Version `"4.0.0`" for explicit version specification, or ` - Use -BuildNumber `"12345`" for generation using -" -ForegroundColor Yellow - Exit 1 -} if ($PullRequestNumber) { Write-Host "Building for a pull request (#$PullRequestNumber), skipping packaging." -ForegroundColor Yellow $CreatePackages = $false } -if ($RunTests) { - dotnet restore /ConsoleLoggerParameters:Verbosity=Quiet - foreach ($project in $testsToRun) { - Write-Host "Running tests: $project (all frameworks)" -ForegroundColor "Magenta" - Push-Location ".\$project" +Write-Host "Building all projects (Build.csproj traversal)..." -ForegroundColor "Magenta" +dotnet build ".\Build.csproj" -c Release /p:CI=true +Write-Host "Done building." -ForegroundColor "Green" - dotnet xunit - if ($LastExitCode -ne 0) { - Write-Host "Error with tests, aborting build." -Foreground "Red" - Pop-Location - Exit 1 - } - - Write-Host "Tests passed!" -ForegroundColor "Green" - Pop-Location +if ($RunTests) { + Write-Host "Running tests: Build.csproj" -ForegroundColor "Magenta" + dotnet test ".\Build.csproj" -c Release --no-build -p:TestTfmsInParallel=false + if ($LastExitCode -ne 0) { + Write-Host "Error with tests, aborting build." -Foreground "Red" + Exit 1 } + Write-Host "Tests passed!" -ForegroundColor "Green" } if ($CreatePackages) { - mkdir -Force $packageOutputFolder | Out-Null + New-Item -ItemType Directory -Path $packageOutputFolder -Force | Out-Null Write-Host "Clearing existing $packageOutputFolder..." -NoNewline Get-ChildItem $packageOutputFolder | Remove-Item Write-Host "done." -ForegroundColor "Green" Write-Host "Building all packages" -ForegroundColor "Green" + dotnet pack ".\Build.csproj" --no-build -c Release /p:PackageOutputPath=$packageOutputFolder /p:CI=true } - -foreach ($project in $projectsToBuild) { - Write-Host "Working on $project`:" -ForegroundColor "Magenta" - - Push-Location ".\$project" - - $semVer = CalculateVersion - $targets = "Restore" - - Write-Host " Restoring " -NoNewline -ForegroundColor "Magenta" - if ($CreatePackages) { - $targets += ";Pack" - Write-Host "and packing " -NoNewline -ForegroundColor "Magenta" - } - Write-Host "$project... (Version:" -NoNewline -ForegroundColor "Magenta" - Write-Host $semVer -NoNewline -ForegroundColor "Cyan" - Write-Host ")" -ForegroundColor "Magenta" - - - dotnet msbuild "/t:$targets" "/p:Configuration=Release" "/p:Version=$semVer" "/p:PackageOutputPath=$packageOutputFolder" "/p:CI=true" - - Pop-Location - - Write-Host "Done." -ForegroundColor "Green" - Write-Host "" -} -Write-Host "Build Complete." -ForegroundColor "Green" \ No newline at end of file +Write-Host "Build Complete." -ForegroundColor "Green" diff --git a/build.sh b/build.sh deleted file mode 100755 index 48be0b749..000000000 --- a/build.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -echo "" -echo "Installing dotnet cli..." -echo "" - -export DOTNET_INSTALL_DIR="./.dotnet/" - -tools/install.sh - -origPath=$PATH -export PATH="./dotnet/bin/:$PATH" - -if [ $? -ne 0 ]; then - echo >&2 ".NET Execution Environment installation has failed." - exit 1 -fi - -export DOTNET_HOME="$DOTNET_INSTALL_DIR/cli" -export PATH="$DOTNET_HOME/bin:$PATH" - -export autoGeneratedVersion=false - -# Generate version number if not set -if [[ -z "$BuildSemanticVersion" ]]; then - autoVersion="$((($(date +%s) - 1451606400)/60))-$(date +%S)" - export BuildSemanticVersion="rc2-$autoVersion" - autoGeneratedVersion=true - - echo "Set version to $BuildSemanticVersion" -fi - -sed -i '' "s/99.99.99-rc2/1.0.0-$BuildSemanticVersion/g" */*/project.json - -# Restore packages and build product -dotnet restore -v Minimal # Restore all packages - -# Build all -# Note the exclude: https://github.com/dotnet/cli/issues/1342 -for d in Dapper*/; do - if [ "$d" != "*.EntityFramework.StrongName" ]; then - echo "Building $d" - pushd "$d" - dotnet build -f netstandard1.3 - popd - fi -done - -# Run tests -for d in *.Tests*/; do - echo "Testing $d" - pushd "$d" - dotnet test -f netcoreapp1.0 - popd -done - -sed -i '' "s/1.0.0-$BuildSemanticVersion/99.99.99-rc2/g" */*/project.json - -if [ $autoGeneratedVersion ]; then - unset BuildSemanticVersion -fi - -export PATH=$origPath \ No newline at end of file diff --git a/docs/dapper-sponsor.png b/docs/dapper-sponsor.png new file mode 100644 index 000000000..b9023052b Binary files /dev/null and b/docs/dapper-sponsor.png differ diff --git a/docs/dapperplus.md b/docs/dapperplus.md new file mode 100644 index 000000000..b15817721 --- /dev/null +++ b/docs/dapperplus.md @@ -0,0 +1,13 @@ +# Dapper and Dapper Plus + +Dapper is the micro-ORM developed initially by Stack Overflow and now maintained independently, that offers simple, high performance +access to the ADO.NET API. + +Dapper Plus is a separate tool by ZZZ Projects, which builds on the path set by Dapper, offering features like bulk operations, +and a range of [documentation for Dapper](https://www.learndapper.com/). + +From 2024, Dapper Plus is now a major sponsor of Dapper, helping to secure ongoing quality development and support of the Dapper platform. +This sponsorship does not impact the ownership, license, or any other particulars of how Dapper operates. The core Dapper libraries continue +to be freely available and fully open source. + +Dapper Plus logo \ No newline at end of file diff --git a/docs/dapperplus.png b/docs/dapperplus.png new file mode 100644 index 000000000..2911442c4 Binary files /dev/null and b/docs/dapperplus.png differ diff --git a/docs/docs.csproj b/docs/docs.csproj new file mode 100644 index 000000000..d68df5c4a --- /dev/null +++ b/docs/docs.csproj @@ -0,0 +1,6 @@ + + + + net8.0 + + diff --git a/docs/index.md b/docs/index.md index f3b3c3409..e05950d63 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,10 @@ -# Dapper - a simple object mapper for .Net +# Dapper - a simple object mapper for .NET ## Overview -A brief guide is available [on github](https://github.com/StackExchange/dapper-dot-net/blob/master/Readme.md) +A brief guide is available [on github](https://github.com/DapperLib/Dapper/blob/main/Readme.md) -More examples coming soon on Stack Overflow docs. - -Questions on Stack Overflow should be tagged [`dapper`](http://stackoverflow.com/questions/tagged/dapper) +Questions on Stack Overflow should be tagged [`dapper`](https://stackoverflow.com/questions/tagged/dapper) ## Installation @@ -22,6 +20,153 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command ## Release Notes +**RELEASE NOTE TRACKING HAS MOVED TO GITHUB** + +See: https://github.com/DapperLib/Dapper/releases + +Archive only (no new entries): + +### 2.1.11 + +(note: new PRs will not be merged until they add release note wording here) + +- infer command text without any whitespace as stored-procedure (#1975 via @mgravell) +- add global `SupportLegacyParameterTokens` setting to enable or disable single-character parameter tokens (#1974 via @Giorgi) +- revert `$` addition for legacy parameter tokens (#1979 via @mgravell) +- change NRT annotation on `GetConstructorParameter` (#1980 via @mgravell, fixes #1969) + +### 2.1.4 + +- add untyped `GridReader.ReadUnbufferedAsync` API (#1958 via @mgravell) +- tweak NRT annotations on type-handler API (#1960 via @mgravell, fixes #1959) + +### 2.1.1 + +- add NRT annotations (#1928 via @mgravell) +- extend `GridReader` API to allow it to be subclassed by external consumers (#1928 via @mgravell) +- support `$` as a parameter prefix (#1952 via @Giorgi) +- add public API tracking (#1948 via @mgravell) + +### 2.0.151 + +- add global `FetchSize` setting for use with Oracle (#1946 via mgravell, fixes #1945) (also add some missing logic in `Settings.Reset()`) +- add underscore handling with constructors (#1786 via @jo-goro, fixes #818; also #1947 via mgravell) + +### 2.0.143 + +- add missing non-generic `AsyncEnumerable QueryUnbufferedAsync(...)` API (#1925 via mgravell, fixes #1922) +- formally mark all `struct` types as `readonly` (#1925 via mgravell) +- reinstate fallback support for `IDataReader`, and implement missing `DbDataReader` async APIs (#1913 via mgravell) + +### 2.0.138 + +- (#1910 via mgravell, fix #1907, #1263) + - add support for `SqlDecimal` and other types that need to be accessed via `DbDataReader.GetFieldValue` + - add an overload of `AddTypeMap` that supports `DbDataReader.GetFieldValue` for additional types + - acknowledge that in reality we only support `DbDataReader`; this has been true (via `DbConnection`) for `async` forever +- (#1912 via mgravell) + - add missing `AsyncEnumerable QueryUnbufferedAsync(...)` and `GridReader.ReadUnbufferedAsync(...)` APIs (.NET 5 and later) + - implement `IAsyncDisposable` on `GridReader` (.NET 5 and later) + +### 2.0.123 + +- Parameters can now be re-used on subsequent commands (#952 via jamescrowley) +- Array query support (`.Query`) on supported platforms (e.g. Postgres) (#1598 via DarkWanderer) +- `SqlMapper.HasTypeHandler` is made public for consumers (#1405 via brendangooden) +- Improves multi-mapping error message when a specified column in splitOn can't be found (#1664 via NickCraver) +- Improves `DbString.ToString()` (#1665 via NickCraver) +- `DbType` for date/time types is no longer explicitly specified (resolves `Npgsql` v6 issue) +- add `Settings.UseIncrementalPseudoPositionalParameterNames`, to support "snowflake" parameter naming conventions + +### 2.0.90 + +- logo added; license updated to mention logo usage (via mgravell) +- moved to DapperLib org; links updated (#1656) +- RepoDb benchmark added (#1626 via stevedesmond-ca) +- excise unrelated Soma tests (#1642 via kant2002) +- SqlMarshl benchmark added (#1646 via kant2002) +- documentation fixes (#1615 via Rollerss, #1604 via GitHubPang) + +### 2.0.78 + +- fix `DynamicParameters` loop bug - wrong index (#1443 via DamirAinullin) +- fix nullable tuple handling (#1400 via JulianRooze) +- support update set in `SqlBuilder` (#1404 via Wei) +- initialize collections with counts when possible (#1449 via DamirAinullin) +- general code cleanup (#1452, #1457, #1458, #1459 all via DamirAinullin) +- C# 9 and .NET 5 preparation/cleanup (#1572 via mgravell) +- GitHub action, Docker, AppVeyor work (build/test) work (#1563 via Tyrrrz, #1559 via craver, #1450 via craver) +- Test project rationalization (#1556 via craver) +- ClickHouse detection (#1462 via DarkWanderer) +- Switched to "main" branch (via craver) +- documentation fixed (#1596 via royal, #1560 via wswind, #1558 via paul42, #1507 via imba-tjd, #1508 via dogac00, 899c9feb via BlackjacketMack, 0b17133 via BlackjacketMack, 9b6c8c7d via bryancrosby, #1202 via craver) +- MightyOrm benchmark added (455b3f3b via cdonnellytx) +- EF6 performance test updates (#1361 via AlexBagnolini) + +### 2.0.35 + +- build tooling: enable "deterministic builds" and enable SDK roll-foward +- fix culture related formatting/parsing issue with Sqlite (#1363 via sebastienros) +- documentation fixes (#1357 via jawn) +- add tests for `SqlBuilder` (#1369 via shps951023) + +### 2.0.30 + +- upstream library updates; project (build) cleanup +- reinstated net461 build target +- add Dapper.ProviderTools library (to help with System vs Microsoft SqlClient migration, etc) +- fix double dictionary lookup (#1339 via DamirAinullin) +- fix bug with dynamic parameters accessing the wrong member (#1334 via DamirAinullin) +- fix explicit-key issue with `DeleteAsync` (#1309 via james-hester-ah) +- fix for `char` on Postgres (#1326 via jjonescz) +- documentation fixes (#1340 via jawn) +- test and benchmark fixes (#1337 via DamirAinullin, #1206 via yesmey, #1331 via andresrsanchez, #1335 via DamirAinullin) + +### 2.0.4 + +Primary changes: + +- remove the System.Data.SqlClient dependency, allowing consumers to use System.Data.SqlClient or Microsoft.Data.SqlClient (or neither, or both) as they choose + - this means that some users may need to *re-add* one of the above as a `` for their project to build, if they were previously relying on Dapper to provide System.Data.SqlClient + - the `AsTableValuedParameter(this IEnumerable)` extension method is now `AsTableValuedParameter(this IEnumerable) where T : IDataRecord`; this is a breaking change but should be code-compatible and just requires a rebuild +- unify the target platform at NetStandard2.0 (and .NET Framework 4.6.2 for the EF DB geometry/geography types) +- fix bug with `Identity` not enforcing type identity of multi-mapped types + +Other changes merged: + +- fix #1242, #1280, #1282 - fix value-tuple mapping +- fix #1295 - add `ExecuteReaderAsync` overload to expose `DbDataReader` +- fix #569 - handing of `IN` and similar clauses in some scenarios +- fix #1256 - make `Dispose()` polymorphic in "rainbow" +- fix #1257 - make the `.Connection` available in "rainbow" + +### 1.60.6 + +- improve performance of descriptor API + +### 1.60.5 + +- add descriptor API to `DapperRow` (enables UI binding with non-generic `Query()` API) + +### 1.60.1 + +- Fix [#1196](https://github.com/DapperLib/Dapper/issues/1196) - versioning fix only ([#1198](https://github.com/DapperLib/Dapper/pull/1198)) - assembly version is now locked at 1.60.0 to resolve some mismatch issues with .NET Core assembly loading/binding. + +### 1.50.7 + +- Fix [#1190](https://github.com/DapperLib/Dapper/issues/1190) - incorrect unmanaged pointer when processing parameters that are a boxed struct (rare error relating to GC) +- Fix [#1111](https://github.com/DapperLib/Dapper/issues/1111) - make `SqlMapper.Parse` consistent with `QueryImpl` +- Fix #111- - improve error message for invalid literal types +- Fix [#1149](https://github.com/DapperLib/Dapper/pull/1149) - improve error messages in "contrib" +- Improved detection of empty table-valued-parameters + +### 1.50.5 + +- Fixes empty result set hanging with `QueryAsync` +- `DapperRow` now implements `IReadOnlyDictionary` +- Improved error messages for `Async` when the provided `IDbConnection` is not a `DbConnection` +- Contrib: `GetAll` now handles nullable types + ### 1.50.4 - Added back missing .NET Standard functionality (restored in `netstandard2.0`) @@ -29,15 +174,15 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command ### 1.50.2 -- fix issue 569 (`in` expansions using ODBC pseudo-positional arguments) +- Fix issue [#569](https://github.com/DapperLib/Dapper/issues/569) (`in` expansions using ODBC pseudo-positional arguments) ### 1.50.1 -- change to how `string_split` is used for `InListStringSplitCount` +- Change to how `string_split` is used for `InListStringSplitCount` ### 1.50.0 -- no changes; stable release +- No changes; stable release ### 1.50.0-rc3 @@ -45,37 +190,37 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command ### 1.50.0-rc2b -- new `InListStringSplitCount` global setting; if set (non-negative), `in @foo` expansions (of at least the specified size) of primitive types (`int`, `tinyint`, `smallint`, `bigint`) are implemented via the SQL Server 2016 (compat level 130) `STRING_SPLIT` function -- fix for incorrect conversions in `GridReader` (#254) +- New `InListStringSplitCount` global setting; if set (non-negative), `in @foo` expansions (of at least the specified size) of primitive types (`int`, `tinyint`, `smallint`, `bigint`) are implemented via the SQL Server 2016 (compat level 130) `STRING_SPLIT` function +- Fix for incorrect conversions in `GridReader` ([#254](https://github.com/DapperLib/Dapper/issues/254)) ### 1.50.0-rc2 / 1.50.0-rc2a -- packaging for .NET Core rc2 +- Packaging for .NET Core rc2 ### 1.50-beta9 -- fix for `PadListExpansions` to work correctly with `not in` scenarios; now uses last non-null value instead of `null`; if none available, don't pad -- fix problems with single-result/single-row not being supported by all providers (basically: sqlite, #466) -- fix problems with enums - nulls (#467) and primitive values (#468) -- add support for C# 6 get-only properties (#473) -- add support for various xml types (#427) +- Fix for `PadListExpansions` to work correctly with `not in` scenarios; now uses last non-null value instead of `null`; if none available, don't pad +- Fix problems with single-result/single-row not being supported by all providers (basically: sqlite, [#466](https://github.com/DapperLib/Dapper/issues/466)) +- Fix problems with enums - nulls ([#467](https://github.com/DapperLib/Dapper/issues/467)) and primitive values ([#468](https://github.com/DapperLib/Dapper/issues/468)) +- Add support for C# 6 get-only properties ([#473](https://github.com/DapperLib/Dapper/issues/473)) +- Add support for various xml types ([#427](https://github.com/DapperLib/Dapper/issues/427)) ### 1.50-beta8 -- addition of `GetRowParser` extension method on `IDataReader` API - allows manual construction of discriminated unions, etc -- addition of `Settings.PadListExpansions` - reduces query-plan saturation by padding list expansions with `null` values (opt-in, because on some DB configurations this could change the meaning) *(note: bad choice of `null` revised in 1.50-beta9)* -- addition of `Settings.ApplyNullValues` - assigns (rather than ignores) `null` values when possible -- fix for #461 - ensure type-handlers work for constructor-based initialization -- fix for #455 - make the `LookupDbType` method available again +- Addition of `GetRowParser` extension method on `IDataReader` API - allows manual construction of discriminated unions, etc +- Addition of `Settings.PadListExpansions` - reduces query-plan saturation by padding list expansions with `null` values (opt-in, because on some DB configurations this could change the meaning) *(note: bad choice of `null` revised in 1.50-beta9)* +- Addition of `Settings.ApplyNullValues` - assigns (rather than ignores) `null` values when possible +- Fix for [#461](https://github.com/DapperLib/Dapper/issues/461) - ensure type-handlers work for constructor-based initialization +- Fix for [#455](https://github.com/DapperLib/Dapper/issues/455) - make the `LookupDbType` method available again ### 1.50-beta7 -- addition of `GetRowParser(Type)` (and refactor the backing store for readers to suit) -- column hash should consider type, not just name +- Addition of `GetRowParser(Type)` (and refactor the backing store for readers to suit) +- Column hash should consider type, not just name ### 1.50-beta6 -- fix for issue #424 - defensive `SqlDataRecord` handling +- Fix for issue [#424](https://github.com/DapperLib/Dapper/issues/424) - defensive `SqlDataRecord` handling ### 1.50-beta5 @@ -86,9 +231,9 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command ### 1.50-beta4 - Add `QueryFirstOrDefault` / `ReadFirstOrDefault` methods that optimize the single-row scenario -- remove some legacy `dynamic` usage from the async API -- make `DynamicTypeMap` public again (error during core-clr migration) -- use `Hashtable` again on core-clr +- Remove some legacy `dynamic` usage from the async API +- Make `DynamicTypeMap` public again (error during core-clr migration) +- Use `Hashtable` again on core-clr ### 1.50-beta3 @@ -97,61 +242,61 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command ### 1.50-beta2 - Core CLR now targets rc1 / 23516 -- various Core CLR fixes -- code cleanup and C# 6 usage (assorted) +- Various Core CLR fixes +- Code cleanup and C# 6 usage (assorted) ### 1.50-beta1 -- split `SqlMapper.cs` as it was becoming too unmaintainable; NuGet is now the only supported deployment channel -- remove down-level C# requirements, as "drop in the file" is no longer the expected usage +- Split `SqlMapper.cs` as it was becoming too unmaintainable; NuGet is now the only supported deployment channel +- Remove down-level C# requirements, as "drop in the file" is no longer the expected usage - `SqlMapper.Settings` added; provides high-level global configuration; initially `CommandTimeout` (@Irrational86) - improve error message if an array is used as a parameter in an invalid context -- add `Type[]` support for `GridReader.Read` scenarios (@NikolayGlynchak) -- support for custom type-maps in collection parameters (@gjsduarte) -- fix incorrect cast in `QueryAsync` (@phnx47, #346) -- fix incorrect null handling re `UdtTypeName` (@perliedman) -- support for `SqlDataRecord` (@sqmgh) -- allow `DbString` default for `IsAnsi` to be specified (@kppullin) +- Add `Type[]` support for `GridReader.Read` scenarios (@NikolayGlynchak) +- Support for custom type-maps in collection parameters (@gjsduarte) +- Fix incorrect cast in `QueryAsync` (@phnx47, [#346](https://github.com/DapperLib/Dapper/issues/346)) +- Fix incorrect null handling re `UdtTypeName` (@perliedman) +- Support for `SqlDataRecord` (@sqmgh) +- Allow `DbString` default for `IsAnsi` to be specified (@kppullin) - provide `TypeMapProvider` with lazy func-based initialization (@garyhuntddn) -- core-clr updated to beta-8 and various cleanups/fixes -- built using core-clr build tools +- Core-clr updated to beta-8 and various cleanups/fixes +- Built using core-clr build tools ### 1.42 -- fix bug with dynamic parameters where `.Get` is called before the command is executed +- Fix bug with dynamic parameters where `.Get` is called before the command is executed ### 1.41-beta5 -- core-clr packaging build and workarounds -- fix bug with literal `{=val}` boolean replacements +- Core-clr packaging build and workarounds +- Fix bug with literal `{=val}` boolean replacements ### 1.41-beta4 -- core-clr packaging build -- improve mapping to enum members (@BrianJolly) +- Core-clr packaging build +- Improve mapping to enum members (@BrianJolly) ### 1.41-beta -- core-clr packaging build +- Core-clr packaging build ### 1.41-alpha -- introduces dnx (core-clr) experimental changes -- adds `SqlBuilder` project -- improve error message when incorrectly accessing parameter values +- Introduces dnx (core-clr) experimental changes +- Adds `SqlBuilder` project +- Improve error message when incorrectly accessing parameter values ### 1.40 -- workaround for broken `GetValues()` on Mono; add `AsList()` +- Workaround for broken `GetValues()` on Mono; add `AsList()` ### 1.39 -- fix case on SQL CLR types; grid-reader should respect no-cache flags; make parameter inclusion case-insensitive +- Fix case on SQL CLR types; grid-reader should respect no-cache flags; make parameter inclusion case-insensitive ### 1.38 -- specify constructor explicitly; allow value-type parameters (albeit: boxed) +- Specify constructor explicitly; allow value-type parameters (albeit: boxed) ### 1.37 @@ -159,13 +304,13 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command ### 1.36 -- Fix Issue #192 (expanded parameter naming glitch) and Issue #178 (execute reader now wraps the command/reader pair, to extend the command lifetime; note that the underlying command/reader are available by casting to `IWrappedDataReader`) +- Fix Issue [#192](https://github.com/DapperLib/Dapper/issues/192) (expanded parameter naming glitch) and Issue [#178](https://github.com/DapperLib/Dapper/issues/178) (execute reader now wraps the command/reader pair, to extend the command lifetime; note that the underlying command/reader are available by casting to `IWrappedDataReader`) ### 1.35 -- Fix Issue #151 (Execute should work with `ExpandoObject` etc); Fix Issue #182 (better support for db-type when using `object` values); -- output expressions / callbacks in dynamic args (via Derek); arbitrary number of types in multi-mapping (via James Holwell); -- fix `DbString`/Oracle bug (via Mauro Cerutti); new support for **named positional arguments** +- Fix Issue [#151](https://github.com/DapperLib/Dapper/issues/151) (Execute should work with `ExpandoObject` etc); Fix Issue #182 (better support for db-type when using `object` values); +- Output expressions / callbacks in dynamic args (via Derek); arbitrary number of types in multi-mapping (via James Holwell); +- Fix `DbString`/Oracle bug (via Mauro Cerutti); new support for **named positional arguments** ### 1.34 diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 000000000..e201fdcd8 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,28 @@ +# Dapper + +Dapper is a simple micro-ORM used to simplify working with ADO.NET; if you like SQL but dislike the boilerplate of ADO.NET: Dapper is for you! + +As a simple example: + +``` c# +string region = ... +var customers = connection.Query( + "select * from Customers where Region = @region", // SQL + new { region } // parameters + ).AsList(); +``` + +But all the execute/single-row/scalar/async/etc functionality you would expect: is there as extension methods on your `DbConnection`. + +See [GitHub](https://github.com/DapperLib/Dapper) for more information and examples. + +Sponsors +-------- + +Dapper was originally developed for and by Stack Overflow, but is F/OSS. Sponsorship is welcome and invited - see the sponsor link at the top of the page. +A huge thanks to everyone (individuals or organisations) who have sponsored Dapper, but a massive thanks in particular to: + +- [Dapper Plus](https://dapper-plus.net/) is a major sponsor and is proud to contribute to the development of Dapper ([read more](https://dapperlib.github.io/Dapper/dapperplus)) +- [AWS](https://github.com/aws) who sponsored Dapper from Oct 2023 via the [.NET on AWS Open Source Software Fund](https://github.com/aws/dotnet-foss) + +[![Dapper Plus logo](https://raw.githubusercontent.com/DapperLib/Dapper/main/docs/dapper-sponsor.png)](https://dapper-plus.net/) diff --git a/game b/game deleted file mode 100644 index b90753f27..000000000 --- a/game +++ /dev/null @@ -1 +0,0 @@ -It's 's fault! \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 000000000..1e3e94a0b --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.102", + "rollForward": "latestMajor" + } +} diff --git a/semver.txt b/semver.txt deleted file mode 100644 index 9bcab4a46..000000000 --- a/semver.txt +++ /dev/null @@ -1 +0,0 @@ -1.50.5-alpha1 \ No newline at end of file diff --git a/signatures/version1/cla.json b/signatures/version1/cla.json new file mode 100644 index 000000000..18d5487f3 --- /dev/null +++ b/signatures/version1/cla.json @@ -0,0 +1,3 @@ +{ + "signedContributors": [] +} \ No newline at end of file diff --git a/tests/Dapper.Tests/App.config b/tests/Dapper.Tests/App.config new file mode 100644 index 000000000..bf437bd25 --- /dev/null +++ b/tests/Dapper.Tests/App.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Dapper.Tests/AsyncTests.cs b/tests/Dapper.Tests/AsyncTests.cs similarity index 62% rename from Dapper.Tests/AsyncTests.cs rename to tests/Dapper.Tests/AsyncTests.cs index 682b4a0c7..9c3ec4721 100644 --- a/Dapper.Tests/AsyncTests.cs +++ b/tests/Dapper.Tests/AsyncTests.cs @@ -1,18 +1,42 @@ -using System.Linq; +using System; +using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Diagnostics; -using System; -using System.Threading.Tasks; +using System.Linq; using System.Threading; -using System.Data.SqlClient; +using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Dapper.Tests { - public class Tests : TestBase + [Collection(NonParallelDefinition.Name)] + public sealed class SystemSqlClientAsyncTests : AsyncTests { } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] + public sealed class MicrosoftSqlClientAsyncTests : AsyncTests { } +#endif + + [Collection(NonParallelDefinition.Name)] + public sealed class SystemSqlClientAsyncQueryCacheTests : AsyncQueryCacheTests + { + public SystemSqlClientAsyncQueryCacheTests(ITestOutputHelper log) : base(log) { } + } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] + public sealed class MicrosoftSqlClientAsyncQueryCacheTests : AsyncQueryCacheTests + { + public MicrosoftSqlClientAsyncQueryCacheTests(ITestOutputHelper log) : base(log) { } + } +#endif + + + public abstract class AsyncTests : TestBase where TProvider : SqlServerDatabaseProvider { - private SqlConnection _marsConnection; - private SqlConnection MarsConnection => _marsConnection ?? (_marsConnection = GetOpenConnection(true)); + private DbConnection? _marsConnection; + + private DbConnection MarsConnection => _marsConnection ??= Provider.GetOpenConnection(true); [Fact] public async Task TestBasicStringUsageAsync() @@ -22,6 +46,118 @@ public async Task TestBasicStringUsageAsync() Assert.Equal(new[] { "abc", "def" }, arr); } + [Fact] + public async Task TestBasicStringUsageUnbufferedDynamicAsync() + { + var results = new List(); + await foreach (var row in connection.QueryUnbufferedAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }) + .ConfigureAwait(false)) + { + string value = row.Value; + results.Add(value); + } + var arr = results.ToArray(); + Assert.Equal(new[] { "abc", "def" }, arr); + } + + [Fact] + public async Task TestBasicStringUsageUnbufferedAsync() + { + var results = new List(); + await foreach (var value in connection.QueryUnbufferedAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }) + .ConfigureAwait(false)) + { + results.Add(value); + } + var arr = results.ToArray(); + Assert.Equal(new[] { "abc", "def" }, arr); + } + + [Fact] + public async Task TestBasicStringUsageUnbufferedAsync_Cancellation() + { + using var cts = new CancellationTokenSource(); + var results = new List(); + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var value in connection.QueryUnbufferedAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }) + .ConfigureAwait(false).WithCancellation(cts.Token)) + { + results.Add(value); + cts.Cancel(); // cancel after first item + } + }); + var arr = results.ToArray(); + Assert.Equal(new[] { "abc" }, arr); // we don't expect the "def" because of the cancellation + } + + [Fact] + public async Task TestBasicStringUsageViaGridReaderUnbufferedAsync() + { + var results = new List(); + await using (var grid = await connection.QueryMultipleAsync("select 'abc' union select 'def'; select @txt", new { txt = "ghi" }) + .ConfigureAwait(false)) + { + while (!grid.IsConsumed) + { + await foreach (var value in grid.ReadUnbufferedAsync() + .ConfigureAwait(false)) + { + results.Add(value); + } + } + } + var arr = results.ToArray(); + Assert.Equal(new[] { "abc", "def", "ghi" }, arr); + } + + [Fact] + public async Task TestBasicStringUsageViaGridReaderUnbufferedDynamicAsync() + { + var results = new List(); + await using (var grid = await connection.QueryMultipleAsync("select 'abc' as [Foo] union select 'def'; select @txt as [Foo]", new { txt = "ghi" }) + .ConfigureAwait(false)) + { + while (!grid.IsConsumed) + { + await foreach (var value in grid.ReadUnbufferedAsync() + .ConfigureAwait(false)) + { + results.Add((string)value.Foo); + } + } + } + var arr = results.ToArray(); + Assert.Equal(new[] { "abc", "def", "ghi" }, arr); + } + + [Fact] + public async Task TestBasicStringUsageViaGridReaderUnbufferedAsync_Cancellation() + { + using var cts = new CancellationTokenSource(); + var results = new List(); + await using (var grid = await connection.QueryMultipleAsync("select 'abc' union select 'def'; select @txt", new { txt = "ghi" }) + .ConfigureAwait(false)) + { + var ex = await Assert.ThrowsAnyAsync(async () => + { + while (!grid.IsConsumed) + { + await foreach (var value in grid.ReadUnbufferedAsync() + .ConfigureAwait(false) + .WithCancellation(cts.Token)) + { + results.Add(value); + } + cts.Cancel(); + } + }); + Assert.True(ex is OperationCanceledException or DbException { Message: "Operation cancelled by user." }); + } + var arr = results.ToArray(); + Assert.Equal(new[] { "abc", "def" }, arr); // don't expect the ghi because of cancellation + } + [Fact] public async Task TestBasicStringUsageQueryFirstAsync() { @@ -47,7 +183,7 @@ public async Task TestBasicStringUsageQueryFirstOrDefaultAsync() public async Task TestBasicStringUsageQueryFirstOrDefaultAsyncDynamic() { var str = await connection.QueryFirstOrDefaultAsync("select null as [Value] union all select @txt", new { txt = "def" }).ConfigureAwait(false); - Assert.Null(str.Value); + Assert.Null(str!.Value); } [Fact] @@ -74,7 +210,7 @@ public async Task TestBasicStringUsageQuerySingleOrDefaultAsync() [Fact] public async Task TestBasicStringUsageQuerySingleOrDefaultAsyncDynamic() { - var str = await connection.QuerySingleOrDefaultAsync("select null as [Value]").ConfigureAwait(false); + var str = (await connection.QuerySingleOrDefaultAsync("select null as [Value]").ConfigureAwait(false))!; Assert.Null(str.Value); } @@ -89,7 +225,7 @@ public async Task TestBasicStringUsageAsyncNonBuffered() [Fact] public void TestLongOperationWithCancellation() { - CancellationTokenSource cancel = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + CancellationTokenSource cancel = new(TimeSpan.FromSeconds(5)); var task = connection.QueryAsync(new CommandDefinition("waitfor delay '00:00:10';select 1", cancellationToken: cancel.Token)); try { @@ -100,14 +236,15 @@ public void TestLongOperationWithCancellation() } catch (AggregateException agg) { - Assert.True(agg.InnerException is SqlException); + Assert.Equal("SqlException", agg.InnerException?.GetType().Name); } } [Fact] public async Task TestBasicStringUsageClosedAsync() { - var query = await connection.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }).ConfigureAwait(false); + using var conn = GetClosedConnection(); + var query = await conn.QueryAsync("select 'abc' as [Value] union all select @txt", new { txt = "def" }).ConfigureAwait(false); var arr = query.ToArray(); Assert.Equal(new[] { "abc", "def" }, arr); } @@ -132,15 +269,16 @@ public async Task TestClassWithStringUsageAsync() public async Task TestExecuteAsync() { var val = await connection.ExecuteAsync("declare @foo table(id int not null); insert @foo values(@id);", new { id = 1 }).ConfigureAwait(false); - val.Equals(1); + Assert.Equal(1, val); } [Fact] public void TestExecuteClosedConnAsyncInner() { - var query = connection.ExecuteAsync("declare @foo table(id int not null); insert @foo values(@id);", new { id = 1 }); + using var conn = GetClosedConnection(); + var query = conn.ExecuteAsync("declare @foo table(id int not null); insert @foo values(@id);", new { id = 1 }); var val = query.Result; - val.Equals(1); + Assert.Equal(1, val); } [Fact] @@ -157,6 +295,7 @@ public async Task TestMultiMapWithSplitAsync() // assertions Assert.Equal(1, product.Id); Assert.Equal("abc", product.Name); + Assert.NotNull(product.Category); Assert.Equal(2, product.Category.Id); Assert.Equal("def", product.Category.Name); } @@ -176,6 +315,7 @@ public async Task TestMultiMapArbitraryWithSplitAsync() // assertions Assert.Equal(1, product.Id); Assert.Equal("abc", product.Name); + Assert.NotNull(product.Category); Assert.Equal(2, product.Category.Id); Assert.Equal("def", product.Category.Name); } @@ -184,70 +324,72 @@ public async Task TestMultiMapArbitraryWithSplitAsync() public async Task TestMultiMapWithSplitClosedConnAsync() { const string sql = "select 1 as id, 'abc' as name, 2 as id, 'def' as name"; - using (var conn = GetClosedConnection()) + + using var conn = GetClosedConnection(); + + var productQuery = await conn.QueryAsync(sql, (prod, cat) => { - var productQuery = await conn.QueryAsync(sql, (prod, cat) => - { - prod.Category = cat; - return prod; - }).ConfigureAwait(false); - - var product = productQuery.First(); - // assertions - Assert.Equal(1, product.Id); - Assert.Equal("abc", product.Name); - Assert.Equal(2, product.Category.Id); - Assert.Equal("def", product.Category.Name); - } + prod.Category = cat; + return prod; + }).ConfigureAwait(false); + + var product = productQuery.First(); + // assertions + Assert.Equal(1, product.Id); + Assert.Equal("abc", product.Name); + Assert.NotNull(product.Category); + Assert.Equal(2, product.Category.Id); + Assert.Equal("def", product.Category.Name); + } + + [Fact] + public async Task TestMultiAsync() + { + using SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2").ConfigureAwait(false); + Assert.Equal(1, multi.ReadAsync().Result.Single()); + Assert.Equal(2, multi.ReadAsync().Result.Single()); } [Fact] - public async Task TestMultiAsync() + public async Task TestMultiConversionAsync() { - using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2").ConfigureAwait(false)) - { - Assert.Equal(1, multi.ReadAsync().Result.Single()); - Assert.Equal(2, multi.ReadAsync().Result.Single()); - } + using SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select Cast(1 as BigInt) Col1; select Cast(2 as BigInt) Col2").ConfigureAwait(false); + Assert.Equal(1, multi.ReadAsync().Result.Single()); + Assert.Equal(2, multi.ReadAsync().Result.Single()); } [Fact] public async Task TestMultiAsyncViaFirstOrDefault() { - using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5").ConfigureAwait(false)) - { - Assert.Equal(1, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(2, multi.ReadAsync().Result.Single()); - Assert.Equal(3, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(4, multi.ReadAsync().Result.Single()); - Assert.Equal(5, multi.ReadFirstOrDefaultAsync().Result); - } + using SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5").ConfigureAwait(false); + Assert.Equal(1, multi.ReadFirstOrDefaultAsync().Result); + Assert.Equal(2, multi.ReadAsync().Result.Single()); + Assert.Equal(3, multi.ReadFirstOrDefaultAsync().Result); + Assert.Equal(4, multi.ReadAsync().Result.Single()); + Assert.Equal(5, multi.ReadFirstOrDefaultAsync().Result); } [Fact] public async Task TestMultiClosedConnAsync() { - using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2").ConfigureAwait(false)) - { - Assert.Equal(1, multi.ReadAsync().Result.Single()); - Assert.Equal(2, multi.ReadAsync().Result.Single()); - } + using var conn = GetClosedConnection(); + using SqlMapper.GridReader multi = await conn.QueryMultipleAsync("select 1; select 2").ConfigureAwait(false); + Assert.Equal(1, multi.ReadAsync().Result.Single()); + Assert.Equal(2, multi.ReadAsync().Result.Single()); } [Fact] public async Task TestMultiClosedConnAsyncViaFirstOrDefault() { - using (SqlMapper.GridReader multi = await connection.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5;").ConfigureAwait(false)) - { - Assert.Equal(1, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(2, multi.ReadAsync().Result.Single()); - Assert.Equal(3, multi.ReadFirstOrDefaultAsync().Result); - Assert.Equal(4, multi.ReadAsync().Result.Single()); - Assert.Equal(5, multi.ReadFirstOrDefaultAsync().Result); - } + using var conn = GetClosedConnection(); + using SqlMapper.GridReader multi = await conn.QueryMultipleAsync("select 1; select 2; select 3; select 4; select 5").ConfigureAwait(false); + Assert.Equal(1, multi.ReadFirstOrDefaultAsync().Result); + Assert.Equal(2, multi.ReadAsync().Result.Single()); + Assert.Equal(3, multi.ReadFirstOrDefaultAsync().Result); + Assert.Equal(4, multi.ReadAsync().Result.Single()); + Assert.Equal(5, multi.ReadFirstOrDefaultAsync().Result); } -#if !NETCOREAPP1_0 [Fact] public async Task ExecuteReaderOpenAsync() { @@ -264,19 +406,16 @@ public async Task ExecuteReaderOpenAsync() [Fact] public async Task ExecuteReaderClosedAsync() { - using (var conn = GetClosedConnection()) - { - var dt = new DataTable(); - dt.Load(await conn.ExecuteReaderAsync("select 3 as [three], 4 as [four]").ConfigureAwait(false)); - Assert.Equal(2, dt.Columns.Count); - Assert.Equal("three", dt.Columns[0].ColumnName); - Assert.Equal("four", dt.Columns[1].ColumnName); - Assert.Equal(1, dt.Rows.Count); - Assert.Equal(3, (int)dt.Rows[0][0]); - Assert.Equal(4, (int)dt.Rows[0][1]); - } + using var conn = GetClosedConnection(); + var dt = new DataTable(); + dt.Load(await conn.ExecuteReaderAsync("select 3 as [three], 4 as [four]").ConfigureAwait(false)); + Assert.Equal(2, dt.Columns.Count); + Assert.Equal("three", dt.Columns[0].ColumnName); + Assert.Equal("four", dt.Columns[1].ColumnName); + Assert.Equal(1, dt.Rows.Count); + Assert.Equal(3, (int)dt.Rows[0][0]); + Assert.Equal(4, (int)dt.Rows[0][1]); } -#endif [Fact] public async Task LiteralReplacementOpen() @@ -287,10 +426,11 @@ public async Task LiteralReplacementOpen() [Fact] public async Task LiteralReplacementClosed() { - using (var conn = GetClosedConnection()) await LiteralReplacement(conn).ConfigureAwait(false); + using var conn = GetClosedConnection(); + await LiteralReplacement(conn).ConfigureAwait(false); } - private async Task LiteralReplacement(IDbConnection conn) + private static async Task LiteralReplacement(IDbConnection conn) { try { @@ -304,7 +444,7 @@ private async Task LiteralReplacement(IDbConnection conn) var count = (await conn.QueryAsync("select count(1) from literal1 where id={=foo}", new { foo = 123 }).ConfigureAwait(false)).Single(); Assert.Equal(1, count); int sum = (await conn.QueryAsync("select sum(id) + sum(foo) from literal1").ConfigureAwait(false)).Single(); - Assert.Equal(sum, 123 + 456 + 1 + 2 + 3 + 4); + Assert.Equal(123 + 456 + 1 + 2 + 3 + 4, sum); } [Fact] @@ -316,10 +456,11 @@ public async Task LiteralReplacementDynamicOpen() [Fact] public async Task LiteralReplacementDynamicClosed() { - using (var conn = GetClosedConnection()) await LiteralReplacementDynamic(conn).ConfigureAwait(false); + using var conn = GetClosedConnection(); + await LiteralReplacementDynamic(conn).ConfigureAwait(false); } - private async Task LiteralReplacementDynamic(IDbConnection conn) + private static async Task LiteralReplacementDynamic(IDbConnection conn) { var args = new DynamicParameters(); args.Add("id", 123); @@ -382,41 +523,9 @@ public void RunSequentialVersusParallelSync() Console.WriteLine("Pipeline: {0}ms", watch.ElapsedMilliseconds); } - [Collection(NonParallelDefinition.Name)] - public class AsyncQueryCacheTests : TestBase - { - private SqlConnection _marsConnection; - private SqlConnection MarsConnection => _marsConnection ?? (_marsConnection = GetOpenConnection(true)); - - [Fact] - public void AssertNoCacheWorksForQueryMultiple() - { - const int a = 123, b = 456; - var cmdDef = new CommandDefinition("select @a; select @b;", new - { - a, - b - }, commandType: CommandType.Text, flags: CommandFlags.NoCache); - - int c, d; - SqlMapper.PurgeQueryCache(); - int before = SqlMapper.GetCachedSQLCount(); - using (var multi = MarsConnection.QueryMultiple(cmdDef)) - { - c = multi.Read().Single(); - d = multi.Read().Single(); - } - int after = SqlMapper.GetCachedSQLCount(); - Assert.Equal(0, before); - Assert.Equal(0, after); - Assert.Equal(123, c); - Assert.Equal(456, d); - } - } - private class BasicType { - public string Value { get; set; } + public string? Value { get; set; } } [Fact] @@ -424,7 +533,7 @@ public async Task TypeBasedViaTypeAsync() { Type type = Common.GetSomeType(); - dynamic actual = (await MarsConnection.QueryAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).ConfigureAwait(false)).FirstOrDefault(); + dynamic actual = (await MarsConnection.QueryAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).ConfigureAwait(false)).FirstOrDefault()!; Assert.Equal(((object)actual).GetType(), type); int a = actual.A; string b = actual.B; @@ -437,7 +546,7 @@ public async Task TypeBasedViaTypeAsyncFirstOrDefault() { Type type = Common.GetSomeType(); - dynamic actual = await MarsConnection.QueryFirstOrDefaultAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).ConfigureAwait(false); + dynamic actual = (await MarsConnection.QueryFirstOrDefaultAsync(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).ConfigureAwait(false))!; Assert.Equal(((object)actual).GetType(), type); int a = actual.A; string b = actual.B; @@ -475,27 +584,30 @@ public async Task Issue346_QueryAsyncConvert() public async Task TestSupportForDynamicParametersOutputExpressionsAsync() { { - var bob = new Person { Name = "bob", PersonId = 1, Address = new Address { PersonId = 2 } }; + var bob = new Person { Name = "bob", PersonId = 1, Address = new Address { PersonId = 2, Index = new Index() } }; var p = new DynamicParameters(bob); p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); + p.Output(bob, b => b.Address!.Index!.Id); await connection.ExecuteAsync(@" SET @Occupation = 'grillmaster' SET @PersonId = @PersonId + 1 SET @NumberOfLegs = @NumberOfLegs - 1 SET @AddressName = 'bobs burgers' -SET @AddressPersonId = @PersonId", p).ConfigureAwait(false); +SET @AddressPersonId = @PersonId +SET @AddressIndexId = '01088'", p).ConfigureAwait(false); Assert.Equal("grillmaster", bob.Occupation); Assert.Equal(2, bob.PersonId); Assert.Equal(1, bob.NumberOfLegs); Assert.Equal("bobs burgers", bob.Address.Name); Assert.Equal(2, bob.Address.PersonId); + Assert.Equal("01088", bob.Address.Index.Id); } } @@ -508,8 +620,8 @@ public async Task TestSupportForDynamicParametersOutputExpressions_ScalarAsync() p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = (int)(await connection.ExecuteScalarAsync(@" SET @Occupation = 'grillmaster' @@ -517,7 +629,7 @@ public async Task TestSupportForDynamicParametersOutputExpressions_ScalarAsync() SET @NumberOfLegs = @NumberOfLegs - 1 SET @AddressName = 'bobs burgers' SET @AddressPersonId = @PersonId -select 42", p).ConfigureAwait(false)); +select 42", p).ConfigureAwait(false))!; Assert.Equal("grillmaster", bob.Occupation); Assert.Equal(2, bob.PersonId); @@ -536,8 +648,8 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_Default p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = (await connection.QueryAsync(@" SET @Occupation = 'grillmaster' @@ -555,6 +667,34 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_Default Assert.Equal(42, result); } + [Fact] + public async Task TestSupportForDynamicParametersOutputExpressions_QueryFirst() + { + var bob = new Person { Name = "bob", PersonId = 1, Address = new Address { PersonId = 2 } }; + + var p = new DynamicParameters(bob); + p.Output(bob, b => b.PersonId); + p.Output(bob, b => b.Occupation); + p.Output(bob, b => b.NumberOfLegs); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); + + var result = (await connection.QueryFirstAsync(@" +SET @Occupation = 'grillmaster' +SET @PersonId = @PersonId + 1 +SET @NumberOfLegs = @NumberOfLegs - 1 +SET @AddressName = 'bobs burgers' +SET @AddressPersonId = @PersonId +select 42", p).ConfigureAwait(false)); + + Assert.Equal("grillmaster", bob.Occupation); + Assert.Equal(2, bob.PersonId); + Assert.Equal(1, bob.NumberOfLegs); + Assert.Equal("bobs burgers", bob.Address.Name); + Assert.Equal(2, bob.Address.PersonId); + Assert.Equal(42, result); + } + [Fact] public async Task TestSupportForDynamicParametersOutputExpressions_Query_BufferedAsync() { @@ -564,8 +704,8 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_Buffere p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = (await connection.QueryAsync(new CommandDefinition(@" SET @Occupation = 'grillmaster' @@ -592,8 +732,8 @@ public async Task TestSupportForDynamicParametersOutputExpressions_Query_NonBuff p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = (await connection.QueryAsync(new CommandDefinition(@" SET @Occupation = 'grillmaster' @@ -620,8 +760,8 @@ public async Task TestSupportForDynamicParametersOutputExpressions_QueryMultiple p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); int x, y; using (var multi = await connection.QueryMultipleAsync(@" @@ -681,10 +821,12 @@ public async Task TestSchemaChangedViaFirstOrDefaultAsync() try { var d = await connection.QueryFirstOrDefaultAsync("select * from #dog").ConfigureAwait(false); + Assert.NotNull(d); Assert.Equal("Alf", d.Name); Assert.Equal(1, d.Age); connection.Execute("alter table #dog drop column Name"); d = await connection.QueryFirstOrDefaultAsync("select * from #dog").ConfigureAwait(false); + Assert.NotNull(d); Assert.Null(d.Name); Assert.Equal(1, d.Age); } @@ -755,6 +897,15 @@ public async Task TestMultiMapArbitraryMapsAsync() var p = data[0]; Assert.Equal(1, p.Id); Assert.Equal("Review Board 1", p.Name); + Assert.NotNull(p.User1); + Assert.NotNull(p.User2); + Assert.NotNull(p.User3); + Assert.NotNull(p.User4); + Assert.NotNull(p.User5); + Assert.NotNull(p.User6); + Assert.NotNull(p.User7); + Assert.NotNull(p.User8); + Assert.NotNull(p.User9); Assert.Equal(1, p.User1.Id); Assert.Equal(2, p.User2.Id); Assert.Equal(3, p.User3.Id); @@ -809,14 +960,12 @@ select @@Name [Fact] public async Task Issue1281_DataReaderOutOfOrderAsync() { - using (var reader = await connection.ExecuteReaderAsync("Select 0, 1, 2").ConfigureAwait(false)) - { - Assert.True(reader.Read()); - Assert.Equal(2, reader.GetInt32(2)); - Assert.Equal(0, reader.GetInt32(0)); - Assert.Equal(1, reader.GetInt32(1)); - Assert.False(reader.Read()); - } + using var reader = await connection.ExecuteReaderAsync("Select 0, 1, 2").ConfigureAwait(false); + Assert.True(reader.Read()); + Assert.Equal(2, reader.GetInt32(2)); + Assert.Equal(0, reader.GetInt32(0)); + Assert.Equal(1, reader.GetInt32(1)); + Assert.False(reader.Read()); } [Fact] @@ -825,9 +974,88 @@ public async Task Issue563_QueryAsyncShouldThrowException() try { var data = (await connection.QueryAsync("select 1 union all select 2; RAISERROR('after select', 16, 1);").ConfigureAwait(false)).ToList(); - Assert.True(false, "Expected Exception"); + Assert.Fail("Expected Exception"); } - catch (SqlException ex) when (ex.Message == "after select") { /* swallow only this */ } + catch (Exception ex) when (ex.GetType().Name == "SqlException" && ex.Message == "after select") { /* swallow only this */ } + } + } + + [Collection(NonParallelDefinition.Name)] + public abstract class AsyncQueryCacheTests : TestBase where TProvider : SqlServerDatabaseProvider + { + private readonly ITestOutputHelper _log; + public AsyncQueryCacheTests(ITestOutputHelper log) => _log = log; + private DbConnection? _marsConnection; + private DbConnection MarsConnection => _marsConnection ??= Provider.GetOpenConnection(true); + + public override void Dispose() + { + _marsConnection?.Dispose(); + _marsConnection = null; + base.Dispose(); + } + + [Fact] + public void AssertNoCacheWorksForQueryMultiple() + { + const int a = 123, b = 456; + var cmdDef = new CommandDefinition("select @a; select @b;", new + { + a, + b + }, commandType: CommandType.Text, flags: CommandFlags.NoCache); + + int c, d; + SqlMapper.PurgeQueryCache(); + int before = SqlMapper.GetCachedSQLCount(); + using (var multi = MarsConnection.QueryMultiple(cmdDef)) + { + c = multi.Read().Single(); + d = multi.Read().Single(); + } + int after = SqlMapper.GetCachedSQLCount(); + _log?.WriteLine($"before: {before}; after: {after}"); + // too brittle in concurrent tests to assert + // Assert.Equal(0, before); + // Assert.Equal(0, after); + Assert.Equal(123, c); + Assert.Equal(456, d); + } + + [Fact] + public async Task AssertNoCacheWorksForMultiMap() + { + const int a = 123, b = 456; + var cmdDef = new CommandDefinition("select @a as a, @b as b;", new + { + a, + b + }, commandType: CommandType.Text, flags: CommandFlags.NoCache | CommandFlags.Buffered); + + SqlMapper.PurgeQueryCache(); + var before = SqlMapper.GetCachedSQLCount(); + Assert.Equal(0, before); + + await MarsConnection.QueryAsync(cmdDef, splitOn: "b", map: (a, b) => (a, b)); + Assert.Equal(0, SqlMapper.GetCachedSQLCount()); + } + + [Fact] + public async Task AssertNoCacheWorksForQueryAsync() + { + const int a = 123, b = 456; + var cmdDef = new CommandDefinition("select @a as a, @b as b;", new + { + a, + b + }, commandType: CommandType.Text, flags: CommandFlags.NoCache | CommandFlags.Buffered); + + SqlMapper.PurgeQueryCache(); + var before = SqlMapper.GetCachedSQLCount(); + Assert.Equal(0, before); + + await MarsConnection.QueryAsync<(int, int)>(cmdDef); + Assert.Equal(0, SqlMapper.GetCachedSQLCount()); } } } diff --git a/Dapper.Tests/ConstructorTests.cs b/tests/Dapper.Tests/ConstructorTests.cs similarity index 58% rename from Dapper.Tests/ConstructorTests.cs rename to tests/Dapper.Tests/ConstructorTests.cs index baac5f6ab..927c030a4 100644 --- a/Dapper.Tests/ConstructorTests.cs +++ b/tests/Dapper.Tests/ConstructorTests.cs @@ -1,11 +1,17 @@ using System; -using System.Data; using System.Linq; using Xunit; namespace Dapper.Tests { - public class ConstructorTests : TestBase + [Collection("ConstructorTests")] + public sealed class SystemSqlClientConstructorTests : ConstructorTests { } +#if MSSQLCLIENT + [Collection("ConstructorTests")] + public sealed class MicrosoftSqlClientConstructorTests : ConstructorTests { } +#endif + + public abstract class ConstructorTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestAbstractInheritance() @@ -52,9 +58,9 @@ public void TestNoDefaultConstructorWithChar() const char c1 = 'ą'; const char c3 = 'ó'; NoDefaultConstructorWithChar nodef = connection.Query("select @c1 c1, @c2 c2, @c3 c3", new { c1 = c1, c2 = (char?)null, c3 = c3 }).First(); - Assert.Equal(nodef.Char1, c1); + Assert.Equal(c1, nodef.Char1); Assert.Null(nodef.Char2); - Assert.Equal(nodef.Char3, c3); + Assert.Equal(c3, nodef.Char3); } [Fact] @@ -125,6 +131,7 @@ private class MultipleConstructors { public MultipleConstructors() { + B = default!; } public MultipleConstructors(int a, string b) @@ -150,7 +157,7 @@ public ConstructorsWithAccessModifiers(int a, string b) } public int A { get; set; } - public string B { get; set; } + public string? B { get; set; } } private class NoDefaultConstructor @@ -213,5 +220,111 @@ public void TestWithNonPublicConstructor() var output = connection.Query("select 1 as Foo").First(); Assert.Equal(1, output.Foo); } + + [Fact] + public void CtorWithUnderscores() + { + var obj = connection.QueryFirst("select 'abc' as FIRST_NAME, 'def' as LAST_NAME"); + Assert.NotNull(obj); + Assert.Equal("abc", obj.FirstName); + Assert.Equal("def", obj.LastName); + } + + [Fact] + public void CtorWithoutUnderscores() + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + var obj = connection.QueryFirst("select 'abc' as FIRST_NAME, 'def' as LAST_NAME"); + Assert.NotNull(obj); + Assert.Equal("abc", obj.FirstName); + Assert.Equal("def", obj.LastName); + } + + [Fact] + public void Issue1993_PreferPropertyOverField() // https://github.com/DapperLib/Dapper/issues/1993 + { + var oldValue = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + var map = new DefaultTypeMap(typeof(ShowIssue1993)); + var first = map.GetMember("field_first"); + Assert.NotNull(first); + Assert.Null(first.Field); + Assert.Equal(nameof(ShowIssue1993.FieldFirst), first.Property?.Name); + + var last = map.GetMember("field_last"); + Assert.NotNull(last); + Assert.Null(last.Field); + Assert.Equal(nameof(ShowIssue1993.FieldLast), last.Property?.Name); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = oldValue; + } + } + + [Fact] + public void Issue1993_Query() + { + var oldValue = DefaultTypeMap.MatchNamesWithUnderscores; + try + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + + var obj = connection.QueryFirst("select 'abc' as field_first, 'def' as field_last"); + Assert.Equal("abc", obj.FieldFirst); + Assert.Equal("def", obj.FieldLast); + + Assert.Equal("abc", obj.AltFieldFirst); + Assert.Equal("def", obj.AltFieldLast); + } + finally + { + DefaultTypeMap.MatchNamesWithUnderscores = oldValue; + } + } + + public class ShowIssue1993 + { + private string _fieldFirst { get; set; } = null!; // not actually a field + public string FieldFirst + { + get => _fieldFirst; + set => _fieldFirst = AltFieldFirst = value; + } + + public string FieldLast + { + get => _fieldLast; + set => _fieldLast = AltFieldLast = value; + } + private string _fieldLast { get; set; } = null!;// not actually a field + + public string AltFieldFirst { get; set; } = null!; + public string AltFieldLast { get; set; } = null!; + } + + class Type_ParamsWithUnderscores + { + public string FirstName { get; } + public string LastName { get; } + public Type_ParamsWithUnderscores(string first_name, string last_name) + { + FirstName = first_name; + LastName = last_name; + } + } + class Type_ParamsWithoutUnderscores + { + public string FirstName { get; } + public string LastName { get; } + public Type_ParamsWithoutUnderscores(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + } } } diff --git a/tests/Dapper.Tests/Dapper.Tests.csproj b/tests/Dapper.Tests/Dapper.Tests.csproj new file mode 100644 index 000000000..93b498618 --- /dev/null +++ b/tests/Dapper.Tests/Dapper.Tests.csproj @@ -0,0 +1,56 @@ + + + Dapper.Tests + Dapper Core Test Suite + net481;net8.0;net10.0 + $(DefineConstants);MSSQLCLIENT + $(NoWarn);IDE0017;IDE0034;IDE0037;IDE0039;IDE0042;IDE0044;IDE0051;IDE0052;IDE0059;IDE0060;IDE0063;IDE1006;xUnit1004;CA1806;CA1816;CA1822;CA1825;CA2208;CA1861 + enable + true + + + + $(DefineConstants);ENTITY_FRAMEWORK;LINQ2SQL;OLEDB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/tests/Dapper.Tests/DataReaderTests.cs b/tests/Dapper.Tests/DataReaderTests.cs new file mode 100644 index 000000000..f98bfabed --- /dev/null +++ b/tests/Dapper.Tests/DataReaderTests.cs @@ -0,0 +1,322 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + [Collection("DataReaderTests")] + public sealed class SystemSqlClientDataReaderTests : DataReaderTests { } +#if MSSQLCLIENT + [Collection("DataReaderTests")] + public sealed class MicrosoftSqlClientDataReaderTests : DataReaderTests { } +#endif + + public abstract class DataReaderTests : TestBase where TProvider : DatabaseProvider + { + [Fact] + public void GetSameReaderForSameShape_IDataReader() + { + var origReader = connection.ExecuteReader("select 'abc' as Name, 123 as Id"); +#pragma warning disable CS0618 // Type or member is obsolete + var origParser = origReader.GetRowParser(typeof(HazNameId)); + + var typedParser = origReader.GetRowParser(); +#pragma warning restore CS0618 // Type or member is obsolete + + // because wrapped for IDataReader, not same instance each time + Assert.False(ReferenceEquals(origParser, typedParser)); + + var list = origReader.Parse().ToList(); + Assert.Single(list); + Assert.Equal("abc", list[0].Name); + Assert.Equal(123, list[0].Id); + origReader.Dispose(); + + var secondReader = connection.ExecuteReader("select 'abc' as Name, 123 as Id"); +#pragma warning disable CS0618 // Type or member is obsolete + var secondParser = secondReader.GetRowParser(typeof(HazNameId)); + var thirdParser = secondReader.GetRowParser(typeof(HazNameId), 1); +#pragma warning restore CS0618 // Type or member is obsolete + + list = secondReader.Parse().ToList(); + Assert.Single(list); + Assert.Equal("abc", list[0].Name); + Assert.Equal(123, list[0].Id); + secondReader.Dispose(); + + // now: should be different readers, and because wrapped for IDataReader, not same parser + Assert.False(ReferenceEquals(origReader, secondReader)); + Assert.False(ReferenceEquals(origParser, secondParser)); + Assert.False(ReferenceEquals(secondParser, thirdParser)); + } + + [Fact] + public void GetSameReaderForSameShape_DbDataReader() + { + var origReader = Assert.IsAssignableFrom(connection.ExecuteReader("select 'abc' as Name, 123 as Id")); + var origParser = origReader.GetRowParser(typeof(HazNameId)); + + var typedParser = origReader.GetRowParser(); + + Assert.True(ReferenceEquals(origParser, typedParser)); + + var list = origReader.Parse().ToList(); + Assert.Single(list); + Assert.Equal("abc", list[0].Name); + Assert.Equal(123, list[0].Id); + origReader.Dispose(); + + var secondReader = Assert.IsAssignableFrom(connection.ExecuteReader("select 'abc' as Name, 123 as Id")); + var secondParser = secondReader.GetRowParser(typeof(HazNameId)); + var thirdParser = secondReader.GetRowParser(typeof(HazNameId), 1); + + list = secondReader.Parse().ToList(); + Assert.Single(list); + Assert.Equal("abc", list[0].Name); + Assert.Equal(123, list[0].Id); + secondReader.Dispose(); + + // now: should be different readers, but same parser + Assert.False(ReferenceEquals(origReader, secondReader)); + Assert.True(ReferenceEquals(origParser, secondParser)); + Assert.False(ReferenceEquals(secondParser, thirdParser)); + } + + [Fact] + public void TestTreatIntAsABool() + { + // Test we are consistent with direct call to database, see TypeHandlerTests.TestTreatIntAsABool + using(var reader = connection.ExecuteReader("select CAST(1 AS BIT)")) + Assert.True(SqlMapper.Parse(reader).Single()); + using (var reader = connection.ExecuteReader("select 1")) + Assert.True(SqlMapper.Parse(reader).Single()); + } + + [Fact] + public void DiscriminatedUnion_IDataReader() + { + var result = new List(); + using (var reader = connection.ExecuteReader(@" +select 'abc' as Name, 1 as Type, 3.0 as Value +union all +select 'def' as Name, 2 as Type, 4.0 as Value")) + { + if (reader.Read()) + { +#pragma warning disable CS0618 + var toFoo = reader.GetRowParser(typeof(Discriminated_Foo)); + var toBar = reader.GetRowParser(typeof(Discriminated_Bar)); +#pragma warning restore CS0618 + + var col = reader.GetOrdinal("Type"); + do + { + switch (reader.GetInt32(col)) + { + case 1: + result.Add(toFoo(reader)); + break; + case 2: + result.Add(toBar(reader)); + break; + } + } while (reader.Read()); + } + } + + Assert.Equal(2, result.Count); + Assert.Equal(1, result[0].Type); + Assert.Equal(2, result[1].Type); + var foo = (Discriminated_Foo)result[0]; + Assert.Equal("abc", foo.Name); + var bar = (Discriminated_Bar)result[1]; + Assert.Equal((float)4.0, bar.Value); + } + + [Fact] + public void DiscriminatedUnion_DbDataReader() + { + var result = new List(); + using (var reader = Assert.IsAssignableFrom(connection.ExecuteReader(@" +select 'abc' as Name, 1 as Type, 3.0 as Value +union all +select 'def' as Name, 2 as Type, 4.0 as Value"))) + { + if (reader.Read()) + { + var toFoo = reader.GetRowParser(typeof(Discriminated_Foo)); + var toBar = reader.GetRowParser(typeof(Discriminated_Bar)); + + var col = reader.GetOrdinal("Type"); + do + { + switch (reader.GetInt32(col)) + { + case 1: + result.Add(toFoo(reader)); + break; + case 2: + result.Add(toBar(reader)); + break; + } + } while (reader.Read()); + } + } + + Assert.Equal(2, result.Count); + Assert.Equal(1, result[0].Type); + Assert.Equal(2, result[1].Type); + var foo = (Discriminated_Foo)result[0]; + Assert.Equal("abc", foo.Name); + var bar = (Discriminated_Bar)result[1]; + Assert.Equal((float)4.0, bar.Value); + } + + [Fact] + public void DiscriminatedUnionWithMultiMapping_IDataReader() + { + var result = new List(); + using (var reader = connection.ExecuteReader(@" +select 'abc' as Name, 1 as Type, 3.0 as Value, 1 as Id, 'zxc' as Name +union all +select 'def' as Name, 2 as Type, 4.0 as Value, 2 as Id, 'qwe' as Name")) + { + if (reader.Read()) + { + var col = reader.GetOrdinal("Type"); + var splitOn = reader.GetOrdinal("Id"); + +#pragma warning disable CS0618 + var toFoo = reader.GetRowParser(typeof(DiscriminatedWithMultiMapping_Foo), 0, splitOn); + var toBar = reader.GetRowParser(typeof(DiscriminatedWithMultiMapping_Bar), 0, splitOn); + var toHaz = reader.GetRowParser(typeof(HazNameId), splitOn, reader.FieldCount - splitOn); +#pragma warning restore CS0618 + + do + { + DiscriminatedWithMultiMapping_BaseType? obj = null; + switch (reader.GetInt32(col)) + { + case 1: + obj = toFoo(reader); + break; + case 2: + obj = toBar(reader); + break; + } + + Assert.NotNull(obj); + obj!.HazNameIdObject = toHaz(reader); + result.Add(obj); + + } while (reader.Read()); + } + } + + Assert.Equal(2, result.Count); + Assert.Equal(1, result[0].Type); + Assert.Equal(2, result[1].Type); + var foo = (DiscriminatedWithMultiMapping_Foo)result[0]; + Assert.Equal("abc", foo.Name); + Assert.NotNull(foo.HazNameIdObject); + Assert.Equal(1, foo.HazNameIdObject.Id); + Assert.Equal("zxc", foo.HazNameIdObject!.Name); + var bar = (DiscriminatedWithMultiMapping_Bar)result[1]; + Assert.Equal((float)4.0, bar.Value); + Assert.NotNull(bar.HazNameIdObject); + Assert.Equal(2, bar.HazNameIdObject.Id); + Assert.Equal("qwe", bar.HazNameIdObject.Name); + } + + [Fact] + public void DiscriminatedUnionWithMultiMapping_DbDataReader() + { + var result = new List(); + using (var reader = Assert.IsAssignableFrom(connection.ExecuteReader(@" +select 'abc' as Name, 1 as Type, 3.0 as Value, 1 as Id, 'zxc' as Name +union all +select 'def' as Name, 2 as Type, 4.0 as Value, 2 as Id, 'qwe' as Name"))) + { + if (reader.Read()) + { + var col = reader.GetOrdinal("Type"); + var splitOn = reader.GetOrdinal("Id"); + + var toFoo = reader.GetRowParser(typeof(DiscriminatedWithMultiMapping_Foo), 0, splitOn); + var toBar = reader.GetRowParser(typeof(DiscriminatedWithMultiMapping_Bar), 0, splitOn); + var toHaz = reader.GetRowParser(typeof(HazNameId), splitOn, reader.FieldCount - splitOn); + + do + { + DiscriminatedWithMultiMapping_BaseType? obj = null; + switch (reader.GetInt32(col)) + { + case 1: + obj = toFoo(reader); + break; + case 2: + obj = toBar(reader); + break; + } + + Assert.NotNull(obj); + obj.HazNameIdObject = toHaz(reader); + result.Add(obj); + + } while (reader.Read()); + } + } + + Assert.Equal(2, result.Count); + Assert.Equal(1, result[0].Type); + Assert.Equal(2, result[1].Type); + var foo = (DiscriminatedWithMultiMapping_Foo)result[0]; + Assert.Equal("abc", foo.Name); + Assert.NotNull(foo.HazNameIdObject); + Assert.Equal(1, foo.HazNameIdObject.Id); + Assert.Equal("zxc", foo.HazNameIdObject.Name); + var bar = (DiscriminatedWithMultiMapping_Bar)result[1]; + Assert.Equal((float)4.0, bar.Value); + Assert.NotNull(bar.HazNameIdObject); + Assert.Equal(2, bar.HazNameIdObject.Id); + Assert.Equal("qwe", bar.HazNameIdObject.Name); + } + + private abstract class Discriminated_BaseType + { + public abstract int Type { get; } + } + + private class Discriminated_Foo : Discriminated_BaseType + { + public string? Name { get; set; } + public override int Type => 1; + } + + private class Discriminated_Bar : Discriminated_BaseType + { + public float Value { get; set; } + public override int Type => 2; + } + + private abstract class DiscriminatedWithMultiMapping_BaseType : Discriminated_BaseType + { + public abstract HazNameId? HazNameIdObject { get; set; } + } + + private class DiscriminatedWithMultiMapping_Foo : DiscriminatedWithMultiMapping_BaseType + { + public override HazNameId? HazNameIdObject { get; set; } + public string? Name { get; set; } + public override int Type => 1; + } + + private class DiscriminatedWithMultiMapping_Bar : DiscriminatedWithMultiMapping_BaseType + { + public override HazNameId? HazNameIdObject { get; set; } + public float Value { get; set; } + public override int Type => 2; + } + } +} diff --git a/tests/Dapper.Tests/DateTimeOnlyTests.cs b/tests/Dapper.Tests/DateTimeOnlyTests.cs new file mode 100644 index 000000000..cabad699d --- /dev/null +++ b/tests/Dapper.Tests/DateTimeOnlyTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +#if NET6_0_OR_GREATER +namespace Dapper.Tests; + +/* we do **NOT** expect this to work against System.Data +[Collection("DateTimeOnlyTests")] +public sealed class SystemSqlClientDateTimeOnlyTests : DateTimeOnlyTests { } +*/ +#if MSSQLCLIENT && DATEONLY +[Collection("DateTimeOnlyTests")] +public sealed class MicrosoftSqlClientDateTimeOnlyTests : DateTimeOnlyTests { } +#endif +public abstract class DateTimeOnlyTests : TestBase where TProvider : DatabaseProvider +{ + public class HazDateTimeOnly + { + public string Name { get; set; } = ""; + public DateOnly Date { get; set; } + public TimeOnly Time { get; set; } + public DateOnly? NDate { get; set; } + public TimeOnly? NTime { get; set; } + } + + [Fact] + public void TypedInOut() + { + var now = DateTime.Now; + var args = new HazDateTimeOnly + { + Name = nameof(TypedInOut), + Date = DateOnly.FromDateTime(now), + Time = TimeOnly.FromDateTime(now), + NDate = DateOnly.FromDateTime(now), + NTime = TimeOnly.FromDateTime(now), + }; + var row = connection.QuerySingle("select @name as [Name], @date as [Date], @time as [Time], @ndate as [NDate], @ntime as [NTime]", args); + Assert.Equal(args.Name, row.Name); + Assert.Equal(args.Date, row.Date); + Assert.Equal(args.Time, row.Time); + Assert.Equal(args.NDate, row.NDate); + Assert.Equal(args.NTime, row.NTime); + } + + [Fact] + public async Task TypedInOutAsync() + { + var now = DateTime.Now; + var args = new HazDateTimeOnly + { + Name = nameof(TypedInOutAsync), + Date = DateOnly.FromDateTime(now), + Time = TimeOnly.FromDateTime(now), + NDate = DateOnly.FromDateTime(now), + NTime = TimeOnly.FromDateTime(now), + }; + var row = await connection.QuerySingleAsync("select @name as [Name], @date as [Date], @time as [Time], @ndate as [NDate], @ntime as [NTime]", args); + Assert.Equal(args.Name, row.Name); + Assert.Equal(args.Date, row.Date); + Assert.Equal(args.Time, row.Time); + Assert.Equal(args.NDate, row.NDate); + Assert.Equal(args.NTime, row.NTime); + } + + [Fact] + public void UntypedInOut() + { + var now = DateTime.Now; + var args = new DynamicParameters(); + var name = nameof(UntypedInOut); + var date = DateOnly.FromDateTime(now); + var time = TimeOnly.FromDateTime(now); + args.Add("name", name); + args.Add("date", date); + args.Add("time", time); + var row = connection.QuerySingle("select @name as [Name], @date as [Date], @time as [Time]", args); + Assert.Equal(name, (string)row.Name); + // untyped, observation is that these come back as DateTime and TimeSpan + Assert.Equal(date, DateOnly.FromDateTime((DateTime)row.Date)); + Assert.Equal(time, TimeOnly.FromTimeSpan((TimeSpan)row.Time)); + } +} +#endif diff --git a/Dapper.Tests/DecimalTests.cs b/tests/Dapper.Tests/DecimalTests.cs similarity index 74% rename from Dapper.Tests/DecimalTests.cs rename to tests/Dapper.Tests/DecimalTests.cs index 5d2fef7b3..fc0b0fce5 100644 --- a/Dapper.Tests/DecimalTests.cs +++ b/tests/Dapper.Tests/DecimalTests.cs @@ -5,7 +5,13 @@ namespace Dapper.Tests { - public class DecimalTests : TestBase + [Collection("DecimalTests")] + public sealed class SystemSqlClientDecimalTests : DecimalTests { } +#if MSSQLCLIENT + [Collection("DecimalTests")] + public sealed class MicrosoftSqlClientDecimalTests : DecimalTests { } +#endif + public abstract class DecimalTests : TestBase where TProvider : DatabaseProvider { [Fact] public void Issue261_Decimals() @@ -14,7 +20,7 @@ public void Issue261_Decimals() parameters.Add("c", dbType: DbType.Decimal, direction: ParameterDirection.Output, precision: 10, scale: 5); connection.Execute("create proc #Issue261 @c decimal(10,5) OUTPUT as begin set @c=11.884 end"); connection.Execute("#Issue261", parameters, commandType: CommandType.StoredProcedure); - var c = parameters.Get("c"); + var c = parameters.Get("c"); Assert.Equal(11.884M, c); } @@ -28,11 +34,9 @@ private void Issue261_Decimals_ADONET(bool setPrecisionScaleViaAbstractApi) { try { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "create proc #Issue261Direct @c decimal(10,5) OUTPUT as begin set @c=11.884 end"; - cmd.ExecuteNonQuery(); - } + using var cmd = connection.CreateCommand(); + cmd.CommandText = "create proc #Issue261Direct @c decimal(10,5) OUTPUT as begin set @c=11.884 end"; + cmd.ExecuteNonQuery(); } catch { /* we don't care that it already exists */ } @@ -77,10 +81,10 @@ public void TestDoubleDecimalConversions_SO18228523_RightWay() { var row = connection.Query( "select cast(1 as float) as A, cast(2 as float) as B, cast(3 as decimal) as C, cast(4 as decimal) as D").Single(); - row.A.Equals(1.0); - row.B.Equals(2.0); - row.C.Equals(3.0M); - row.D.Equals(4.0M); + Assert.Equal(1.0, row.A); + Assert.Equal(2.0, row.B); + Assert.Equal(3.0M, row.C); + Assert.Equal(4.0M, row.D); } [Fact] @@ -88,10 +92,10 @@ public void TestDoubleDecimalConversions_SO18228523_WrongWay() { var row = connection.Query( "select cast(1 as decimal) as A, cast(2 as decimal) as B, cast(3 as float) as C, cast(4 as float) as D").Single(); - row.A.Equals(1.0); - row.B.Equals(2.0); - row.C.Equals(3.0M); - row.D.Equals(4.0M); + Assert.Equal(1.0, row.A); + Assert.Equal(2.0, row.B); + Assert.Equal(3.0M, row.C); + Assert.Equal(4.0M, row.D); } [Fact] @@ -99,9 +103,9 @@ public void TestDoubleDecimalConversions_SO18228523_Nulls() { var row = connection.Query( "select cast(null as decimal) as A, cast(null as decimal) as B, cast(null as float) as C, cast(null as float) as D").Single(); - row.A.Equals(0.0); + Assert.Equal(0.0, row.A); Assert.Null(row.B); - row.C.Equals(0.0M); + Assert.Equal(0.0M, row.C); Assert.Null(row.D); } diff --git a/Dapper.Tests/EnumTests.cs b/tests/Dapper.Tests/EnumTests.cs similarity index 77% rename from Dapper.Tests/EnumTests.cs rename to tests/Dapper.Tests/EnumTests.cs index e74e4522d..b8696049b 100644 --- a/Dapper.Tests/EnumTests.cs +++ b/tests/Dapper.Tests/EnumTests.cs @@ -4,7 +4,13 @@ namespace Dapper.Tests { - public class EnumTests : TestBase + [Collection("EnumTests")] + public sealed class SystemSqlClientEnumTests : EnumTests { } +#if MSSQLCLIENT + [Collection("EnumTests")] + public sealed class MicrosoftSqlClientEnumTests : EnumTests { } +#endif + public abstract class EnumTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestEnumWeirdness() @@ -86,18 +92,17 @@ private class TestEnumClassNoNull [Fact] public void AdoNetEnumValue() { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "select @foo"; - var p = cmd.CreateParameter(); - p.ParameterName = "@foo"; - p.DbType = DbType.Int32; // it turns out that this is the key piece; setting the DbType - p.Value = AnEnum.B; - cmd.Parameters.Add(p); - object value = cmd.ExecuteScalar(); - AnEnum val = (AnEnum)value; - Assert.Equal(AnEnum.B, val); - } + using var cmd = connection.CreateCommand(); + cmd.CommandText = "select @foo"; + var p = cmd.CreateParameter(); + p.ParameterName = "@foo"; + p.DbType = DbType.Int32; // it turns out that this is the key piece; setting the DbType + p.Value = AnEnum.B; + cmd.Parameters.Add(p); + object? value = cmd.ExecuteScalar(); + Assert.NotNull(value); + AnEnum val = (AnEnum)value; + Assert.Equal(AnEnum.B, val); } [Fact] diff --git a/tests/Dapper.Tests/Helpers/Attributes.cs b/tests/Dapper.Tests/Helpers/Attributes.cs new file mode 100644 index 000000000..e41cf624d --- /dev/null +++ b/tests/Dapper.Tests/Helpers/Attributes.cs @@ -0,0 +1,100 @@ +using System; +using Xunit.Sdk; + +namespace Dapper.Tests +{ + /// + /// Override for that truncates our DisplayName down. + /// + /// Attribute that is applied to a method to indicate that it is a fact that should + /// be run by the test runner. It can also be extended to support a customized definition + /// of a test method. + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("Dapper.Tests.FactDiscoverer", "Dapper.Tests")] + public class FactAttribute : Xunit.FactAttribute + { + } + + /// + /// Override for that truncates our DisplayName down. + /// + /// Marks a test method as being a data theory. Data theories are tests which are + /// fed various bits of data from a data source, mapping to parameters on the test + /// method. If the data source contains multiple rows, then the test method is executed + /// multiple times (once with each data row). Data is provided by attributes which + /// derive from Xunit.Sdk.DataAttribute (notably, Xunit.InlineDataAttribute and Xunit.MemberDataAttribute). + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("Dapper.Tests.TheoryDiscoverer", "Dapper.Tests")] + public class TheoryAttribute : Xunit.TheoryAttribute { } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public sealed class FactLongRunningAttribute : FactAttribute + { + public FactLongRunningAttribute() + { +#if !LONG_RUNNING + Skip = "Long running"; +#endif + } + + public string? Url { get; private set; } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactRequiredCompatibilityLevelAttribute : FactAttribute + { + public FactRequiredCompatibilityLevelAttribute(int level) : base() + { + if (DetectedLevel < level) + { + Skip = $"Compatibility level {level} required; detected {DetectedLevel}"; + } + } + + public const int SqlServer2016 = 130; + public static readonly int DetectedLevel; + static FactRequiredCompatibilityLevelAttribute() + { + using var conn = DatabaseProvider.Instance.GetOpenConnection(); + try + { + DetectedLevel = conn.QuerySingle("SELECT compatibility_level FROM sys.databases where name = DB_NAME()"); + } + catch { /* don't care */ } + } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactUnlessCaseSensitiveDatabaseAttribute : FactAttribute + { + public FactUnlessCaseSensitiveDatabaseAttribute() : base() + { + if (IsCaseSensitive) + { + Skip = "Case sensitive database"; + } + } + + public static readonly bool IsCaseSensitive; + static FactUnlessCaseSensitiveDatabaseAttribute() + { + using var conn = DatabaseProvider.Instance.GetOpenConnection(); + try + { + conn.Execute("declare @i int; set @I = 1;"); + } + catch (Exception ex) when (ex.GetType().Name == "SqlException") + { + int err = ((dynamic)ex).Number; + if (err == 137) + IsCaseSensitive = true; + else + throw; + } + } + } +} diff --git a/Dapper.Tests/Helpers/Common.cs b/tests/Dapper.Tests/Helpers/Common.cs similarity index 94% rename from Dapper.Tests/Helpers/Common.cs rename to tests/Dapper.Tests/Helpers/Common.cs index c3fc74c60..301147127 100644 --- a/Dapper.Tests/Helpers/Common.cs +++ b/tests/Dapper.Tests/Helpers/Common.cs @@ -24,14 +24,14 @@ public static void DapperEnumValue(IDbConnection connection) // test passing as int, reading as AnEnum var k = (int)connection.QuerySingle("select @v, @y, @z", new { v = (int)AnEnum.B, y = (int?)(int)AnEnum.B, z = (int?)null }); - Assert.Equal(k, (int)AnEnum.B); + Assert.Equal((int)AnEnum.B, k); args = new DynamicParameters(); args.Add("v", (int)AnEnum.B); args.Add("y", (int)AnEnum.B); args.Add("z", null); k = (int)connection.QuerySingle("select @v, @y, @z", args); - Assert.Equal(k, (int)AnEnum.B); + Assert.Equal((int)AnEnum.B, k); } public static void TestDateTime(DbConnection connection) diff --git a/tests/Dapper.Tests/Helpers/IsExternalInit.cs b/tests/Dapper.Tests/Helpers/IsExternalInit.cs new file mode 100644 index 000000000..7a67b19de --- /dev/null +++ b/tests/Dapper.Tests/Helpers/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices; + +#if !NET5_0_OR_GREATER +internal static class IsExternalInit +{ +} +#endif diff --git a/Dapper.Tests/Helpers/SqlServerTypesLoader.cs b/tests/Dapper.Tests/Helpers/SqlServerTypesLoader.cs similarity index 100% rename from Dapper.Tests/Helpers/SqlServerTypesLoader.cs rename to tests/Dapper.Tests/Helpers/SqlServerTypesLoader.cs diff --git a/Dapper.Tests/Helpers/TransactedConnection.cs b/tests/Dapper.Tests/Helpers/TransactedConnection.cs similarity index 74% rename from Dapper.Tests/Helpers/TransactedConnection.cs rename to tests/Dapper.Tests/Helpers/TransactedConnection.cs index 7dd785748..6452aa9be 100644 --- a/Dapper.Tests/Helpers/TransactedConnection.cs +++ b/tests/Dapper.Tests/Helpers/TransactedConnection.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.Data; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dapper.Tests { @@ -20,8 +16,10 @@ public TransactedConnection(IDbConnection conn, IDbTransaction tran) public string ConnectionString { - get { return _conn.ConnectionString; } + get { return _conn?.ConnectionString ?? ""; } +#pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). set { _conn.ConnectionString = value; } +#pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes). } public int ConnectionTimeout => _conn.ConnectionTimeout; diff --git a/tests/Dapper.Tests/Helpers/XunitSkippable.cs b/tests/Dapper.Tests/Helpers/XunitSkippable.cs new file mode 100644 index 000000000..81814e9e9 --- /dev/null +++ b/tests/Dapper.Tests/Helpers/XunitSkippable.cs @@ -0,0 +1,159 @@ +using System; + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Dapper.Tests +{ + public static class Skip + { + public static void Inconclusive(string reason = "inconclusive") + => throw new SkipTestException(reason); + + public static void If(object obj, string? reason = null) + where T : class + { + if (obj is T) Skip.Inconclusive(reason ?? $"not valid for {typeof(T).FullName}"); + } + } + +#pragma warning disable RCS1194 // Implement exception constructors. + public class SkipTestException : Exception + { + public SkipTestException(string reason) : base(reason) + { + } + } +#pragma warning restore RCS1194 // Implement exception constructors. + + public class FactDiscoverer : Xunit.Sdk.FactDiscoverer + { + public FactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } + + protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) + => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); + } + + public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer + { + public TheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } + + protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) + => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow) }; + + protected override IEnumerable CreateTestCasesForSkip(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, string skipReason) + => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; + + protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + => new[] { new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; + + protected override IEnumerable CreateTestCasesForSkippedDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow, string skipReason) + => new[] { new NamedSkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow) }; + } + + public class SkippableTestCase : XunitTestCase + { + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => + base.GetDisplayName(factAttribute, displayName).StripName(); + + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableTestCase() { } + + public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) + { + } + + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var skipMessageBus = new SkippableMessageBus(messageBus); + var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ConfigureAwait(false); + return result.Update(skipMessageBus); + } + } + + public class SkippableTheoryTestCase : XunitTheoryTestCase + { + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => + base.GetDisplayName(factAttribute, displayName).StripName(); + + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public SkippableTheoryTestCase() { } + + public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } + + public override async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + var skipMessageBus = new SkippableMessageBus(messageBus); + var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ConfigureAwait(false); + return result.Update(skipMessageBus); + } + } + + public class NamedSkippedDataRowTestCase : XunitSkippedDataRowTestCase + { + protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => + base.GetDisplayName(factAttribute, displayName).StripName(); + + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public NamedSkippedDataRowTestCase() { } + + public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) { } + } + + public class SkippableMessageBus : IMessageBus + { + private readonly IMessageBus InnerBus; + public SkippableMessageBus(IMessageBus innerBus) => InnerBus = innerBus; + + public int DynamicallySkippedTestCount { get; private set; } + + public void Dispose() { } + + public bool QueueMessage(IMessageSinkMessage message) + { + if (message is ITestFailed testFailed) + { + var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); + if (exceptionType == typeof(SkipTestException).FullName) + { + DynamicallySkippedTestCount++; + return InnerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); + } + } + return InnerBus.QueueMessage(message); + } + } + + internal static class XUnitExtensions + { + internal static string StripName(this string name) => + name.Replace("Dapper.Tests.", ""); + + public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus) + { + if (bus.DynamicallySkippedTestCount > 0) + { + summary.Failed -= bus.DynamicallySkippedTestCount; + summary.Skipped += bus.DynamicallySkippedTestCount; + } + return summary; + } + } +} diff --git a/Dapper.Tests/LiteralTests.cs b/tests/Dapper.Tests/LiteralTests.cs similarity index 81% rename from Dapper.Tests/LiteralTests.cs rename to tests/Dapper.Tests/LiteralTests.cs index aba2877bb..2ad080dc5 100644 --- a/Dapper.Tests/LiteralTests.cs +++ b/tests/Dapper.Tests/LiteralTests.cs @@ -3,7 +3,13 @@ namespace Dapper.Tests { - public class LiteralTests : TestBase + [Collection("LiteralTests")] + public sealed class SystemSqlClientLiteralTests : LiteralTests { } +#if MSSQLCLIENT + [Collection("LiteralTests")] + public sealed class MicrosoftSqlClientLiteralTests : LiteralTests { } +#endif + public abstract class LiteralTests : TestBase where TProvider : DatabaseProvider { [Fact] public void LiteralReplacementEnumAndString() @@ -13,9 +19,9 @@ public void LiteralReplacementEnumAndString() AnEnum x = (AnEnum)(int)row.x; decimal y = row.y; AnotherEnum z = (AnotherEnum)(byte)row.z; - x.Equals(AnEnum.B); - y.Equals(123.45M); - z.Equals(AnotherEnum.A); + Assert.Equal(AnEnum.B, x); + Assert.Equal(123.45M, y); + Assert.Equal(AnotherEnum.A, z); } [Fact] @@ -29,9 +35,9 @@ public void LiteralReplacementDynamicEnumAndString() AnEnum x = (AnEnum)(int)row.x; decimal y = row.y; AnotherEnum z = (AnotherEnum)(byte)row.z; - x.Equals(AnEnum.B); - y.Equals(123.45M); - z.Equals(AnotherEnum.A); + Assert.Equal(AnEnum.B, x); + Assert.Equal(123.45M, y); + Assert.Equal(AnotherEnum.A, z); } [Fact] @@ -80,7 +86,7 @@ public void LiteralReplacement() var count = connection.Query("select count(1) from #literal1 where id={=foo}", new { foo = 123 }).Single(); Assert.Equal(1, count); int sum = connection.Query("select sum(id) + sum(foo) from #literal1").Single(); - Assert.Equal(sum, 123 + 456 + 1 + 2 + 3 + 4); + Assert.Equal(123 + 456 + 1 + 2 + 3 + 4, sum); } [Fact] diff --git a/Dapper.Tests/MiscTests.cs b/tests/Dapper.Tests/MiscTests.cs similarity index 67% rename from Dapper.Tests/MiscTests.cs rename to tests/Dapper.Tests/MiscTests.cs index 3e46ee05b..077e93647 100644 --- a/Dapper.Tests/MiscTests.cs +++ b/tests/Dapper.Tests/MiscTests.cs @@ -1,46 +1,33 @@ -using Microsoft.CSharp.RuntimeBinder; -using System; +using System; using System.Collections.Generic; +using System.ComponentModel; using System.Data; using System.Data.Common; -using System.Data.SqlClient; using System.Diagnostics; using System.Linq; -using Xunit; - -#if NETCOREAPP1_0 -using System.Collections; -using System.Dynamic; -using System.Data.SqlTypes; -#else // net452 -using System.IO; -using System.Threading; using System.Threading.Tasks; -#endif +using Microsoft.CSharp.RuntimeBinder; +using Xunit; -#if NETCOREAPP1_0 -namespace System +#if NET472 +namespace System.Runtime.CompilerServices { - public enum GenericUriParserOptions + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit // yeah, don't do this! { - Default - } - - public class GenericUriParser - { - private readonly GenericUriParserOptions options; - - public GenericUriParser(GenericUriParserOptions options) - { - this.options = options; - } } } #endif namespace Dapper.Tests { - public class MiscTests : TestBase + [Collection("MiscTests")] + public sealed class SystemSqlClientMiscTests : MiscTests { } +#if MSSQLCLIENT + [Collection("MiscTests")] + public sealed class MicrosoftSqlClientMiscTests : MiscTests { } +#endif + public abstract class MiscTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestNullableGuidSupport() @@ -78,9 +65,20 @@ public enum TrapEnum : int private struct CarWithAllProps { public string Name { get; set; } - public int Age { get; set; } + public int Age { get; init; } + public Car.TrapEnum Trap { get; init; } + } - public Car.TrapEnum Trap { get; set; } + private record PositionalCarRecord(int Age, Car.TrapEnum Trap, string? Name) + { + public PositionalCarRecord() : this(default, default, default) { } + } + + private record NominalCarRecord + { + public int Age { get; init; } + public Car.TrapEnum Trap { get; init; } + public string? Name { get; init; } } [Fact] @@ -93,6 +91,26 @@ public void TestStructs() Assert.Equal(2, (int)car.Trap); } + [Fact] + public void TestPositionalRecord() + { + var car = connection.Query("select 'Ford' Name, 21 Age, 2 Trap").First(); + + Assert.Equal(21, car.Age); + Assert.Equal("Ford", car.Name); + Assert.Equal(2, (int)car.Trap); + } + + [Fact] + public void TestNominalRecord() + { + var car = connection.Query("select 'Ford' Name, 21 Age, 2 Trap").First(); + + Assert.Equal(21, car.Age); + Assert.Equal("Ford", car.Name); + Assert.Equal(2, (int)car.Trap); + } + [Fact] public void TestStructAsParam() { @@ -144,10 +162,12 @@ public void TestSchemaChangedViaFirstOrDefault() try { var d = connection.QueryFirstOrDefault("select * from #dog"); + Assert.NotNull(d); Assert.Equal("Alf", d.Name); Assert.Equal(1, d.Age); connection.Execute("alter table #dog drop column Name"); d = connection.QueryFirstOrDefault("select * from #dog"); + Assert.NotNull(d); Assert.Null(d.Name); Assert.Equal(1, d.Age); } @@ -189,6 +209,91 @@ public void Test_Single_First_Default() Assert.Equal("Sequence contains more than one element", ex.Message); } + /// + /// This test is ensuring our "single row" methods also behave like a type being deserialized + /// and give a useful error message when the types don't match. + /// + [Fact] + public async Task TestConversionExceptionMessages() + { + const string sql = "Select Null;"; + + // Nullable is expected to work if we get a null in all cases + // List paths + var list = connection.Query(sql); + Assert.Null(Assert.Single(list)); + list = await connection.QueryAsync(sql); + Assert.Null(Assert.Single(list)); + + // Single row paths + Assert.Null(connection.QueryFirst(sql)); + Assert.Null(connection.QueryFirstOrDefault(sql)); + Assert.Null(connection.QuerySingle(sql)); + Assert.Null(connection.QuerySingleOrDefault(sql)); + + Assert.Null(await connection.QueryFirstAsync(sql)); + Assert.Null(await connection.QueryFirstOrDefaultAsync(sql)); + Assert.Null(await connection.QuerySingleAsync(sql)); + Assert.Null(await connection.QuerySingleOrDefaultAsync(sql)); + + static async Task TestExceptionsAsync(DbConnection connection, string sql, string exception) + { + var ex = Assert.Throws(() => connection.Query(sql)); + Assert.Equal(exception, ex.Message); + ex = Assert.Throws(() => connection.QueryFirst(sql)); + Assert.Equal(exception, ex.Message); + ex = Assert.Throws(() => connection.QueryFirstOrDefault(sql)); + Assert.Equal(exception, ex.Message); + ex = Assert.Throws(() => connection.QuerySingle(sql)); + Assert.Equal(exception, ex.Message); + ex = Assert.Throws(() => connection.QuerySingleOrDefault(sql)); + Assert.Equal(exception, ex.Message); + + ex = await Assert.ThrowsAsync(() => connection.QueryAsync(sql)); + Assert.Equal(exception, ex.Message); + ex = await Assert.ThrowsAsync(() => connection.QueryFirstAsync(sql)); + Assert.Equal(exception, ex.Message); + ex = await Assert.ThrowsAsync(() => connection.QueryFirstOrDefaultAsync(sql)); + Assert.Equal(exception, ex.Message); + ex = await Assert.ThrowsAsync(() => connection.QuerySingleAsync(sql)); + Assert.Equal(exception, ex.Message); + ex = await Assert.ThrowsAsync(() => connection.QuerySingleOrDefaultAsync(sql)); + Assert.Equal(exception, ex.Message); + } + + // Null value throws + await TestExceptionsAsync( + connection, + "Select null as Foo", + "Error parsing column 0 (Foo=n/a - Null object cannot be converted to a value type.)"); + // Incompatible value throws (testing unnamed column bits here too) + await TestExceptionsAsync( + connection, + "Select 'bar'", + "Error parsing column 0 ((Unnamed Column)=bar - String)"); + // Null with a full type (testing position too) + await TestExceptionsAsync( + connection, + "Select 1 Id, 'bar' Foo", + "Error parsing column 1 (Foo=bar - String)"); + + // And a ValueTuple! (testing position too) + // Still needs love, because we handle ValueTuple differently today + // It'll yield a raw: typeof(System.FormatException): Input string was not in a correct format. + // Note: not checking the "Select 1 Id, null Foo" case here, because we won't attempt to set the column + // ...and there will no error in that case. + //await TestExceptionsAsync<(int Id, int Foo)>( + // connection, + // "Select 1 Id, 'bar' Foo", + // "Error parsing column 1 (Foo=bar - String)"); + } + + private class NullTestType + { + public int Id { get; } + public int Foo { get; } + } + [Fact] public void TestStrings() { @@ -313,7 +418,7 @@ public void TestExecuteMultipleCommand() private class Student { - public string Name { get; set; } + public string? Name { get; set; } public int Age { get; set; } } @@ -323,7 +428,7 @@ public void TestExecuteMultipleCommandStrongType() connection.Execute("create table #t(Name nvarchar(max), Age int)"); try { - int tally = connection.Execute("insert #t (Name,Age) values(@Name, @Age)", new List + int tally = connection.Execute("insert #t (Name,Age) values(@Name, @Age)", new List(2) { new Student{Age = 1, Name = "sam"}, new Student{Age = 2, Name = "bob"} @@ -455,7 +560,7 @@ public void TestBigIntMember() declare @bar table(Value bigint) insert @bar values (@foo) select * from @bar", new { foo }).Single(); - Assert.Equal(result.Value, foo); + Assert.Equal(foo, result.Value); } private class WithBigInt @@ -497,14 +602,14 @@ private string f private class InheritanceTest1 { - public string Base1 { get; set; } - public string Base2 { get; private set; } + public string? Base1 { get; set; } + public string? Base2 { get; private set; } } private class InheritanceTest2 : InheritanceTest1 { - public string Derived1 { get; set; } - public string Derived2 { get; private set; } + public string? Derived1 { get; set; } + public string? Derived2 { get; private set; } } [Fact] @@ -518,7 +623,6 @@ public void TestInheritance() Assert.Equal("Four", list.First().Base2); } -#if !NETCOREAPP1_0 [Fact] public void ExecuteReader() { @@ -531,7 +635,6 @@ public void ExecuteReader() Assert.Equal(3, (int)dt.Rows[0][0]); Assert.Equal(4, (int)dt.Rows[0][1]); } -#endif [Fact] public void TestDbString() @@ -554,6 +657,47 @@ public void TestDbString() Assert.Equal(10, (int)obj.f); } + [Fact] + public void DbStringNullHandling() + { + // without lengths + var obj = new { x = new DbString("abc"), y = (DbString?)new DbString(null) }; + var row = connection.QuerySingle<(string? x,string? y)>("select @x as x, @y as y", obj); + Assert.Equal("abc", row.x); + Assert.Null(row.y); + + // with lengths + obj = new { x = new DbString("abc", 200), y = (DbString?)new DbString(null, 200) }; + row = connection.QuerySingle<(string? x, string? y)>("select @x as x, @y as y", obj); + Assert.Equal("abc", row.x); + Assert.Null(row.y); + + // null raw value - give clear message, at least + obj = obj with { y = null }; + var ex = Assert.Throws(() => connection.QuerySingle<(string? x, string? y)>("select @x as x, @y as y", obj)); + Assert.Equal("Member 'y' is an ICustomQueryParameter and cannot be null", ex.Message); + } + + [Fact] + public void TestDbStringToString() + { + Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: True, IsFixedLength: True)", + new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = true }.ToString()); + Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: False, IsFixedLength: True)", + new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = false }.ToString()); + Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: True, IsFixedLength: False)", + new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = true }.ToString()); + Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: False, IsFixedLength: False)", + new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = false }.ToString()); + Assert.Equal("Dapper.DbString (Value: null, Length: -1, IsAnsi: False, IsFixedLength: False)", + new DbString { Value = null }.ToString()); + + Assert.Equal("Dapper.DbString (Value: 'abcde', Length: -1, IsAnsi: True, IsFixedLength: False)", + new DbString { Value = "abcde", IsAnsi = true }.ToString()); + Assert.Equal("Dapper.DbString (Value: 'abcde', Length: -1, IsAnsi: False, IsFixedLength: False)", + new DbString { Value = "abcde", IsAnsi = false }.ToString()); + } + [Fact] public void TestDefaultDbStringDbType() { @@ -576,6 +720,7 @@ public void TestDefaultDbStringDbType() public void TestFastExpandoSupportsIDictionary() { var row = connection.Query("select 1 A, 'two' B").First() as IDictionary; + Assert.NotNull(row); Assert.Equal(1, row["A"]); Assert.Equal("two", row["B"]); } @@ -585,7 +730,7 @@ public void TestDapperSetsPrivates() { Assert.Equal(1, connection.Query("select 'one' ShadowInDB").First().Shadow); - Assert.Equal(1, connection.QueryFirstOrDefault("select 'one' ShadowInDB").Shadow); + Assert.Equal(1, connection.QueryFirstOrDefault("select 'one' ShadowInDB")?.Shadow); } private class PrivateDan @@ -600,7 +745,7 @@ private string ShadowInDB [Fact] public void TestUnexpectedDataMessage() { - string msg = null; + string? msg = null; try { connection.Query("select count(1) where 1 = @Foo", new WithBizarreData { Foo = new GenericUriParser(GenericUriParserOptions.Default), Bar = 23 }).First(); @@ -622,7 +767,7 @@ public void TestUnexpectedButFilteredDataMessage() private class WithBizarreData { - public GenericUriParser Foo { get; set; } + public GenericUriParser? Foo { get; set; } public int Bar { get; set; } } @@ -638,11 +783,11 @@ public void TestCharInputAndOutput() const char test = '〠'; char c = connection.Query("select @c", new { c = test }).Single(); - Assert.Equal(c, test); + Assert.Equal(test, c); var obj = connection.Query("select @Value as Value", new WithCharValue { Value = c }).Single(); - Assert.Equal(obj.Value, test); + Assert.Equal(test, obj.Value); } [Fact] @@ -675,7 +820,7 @@ public void TestNullableCharInputAndOutputNull() public void WorkDespiteHavingWrongStructColumnTypes() { var hazInt = connection.Query("select cast(1 as bigint) Value").Single(); - hazInt.Value.Equals(1); + Assert.Equal(1, hazInt.Value); } private struct CanHazInt @@ -686,20 +831,20 @@ private struct CanHazInt [Fact] public void TestInt16Usage() { - Assert.Equal(connection.Query("select cast(42 as smallint)").Single(), (short)42); - Assert.Equal(connection.Query("select cast(42 as smallint)").Single(), (short?)42); - Assert.Equal(connection.Query("select cast(null as smallint)").Single(), (short?)null); + Assert.Equal((short)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((short?)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((short?)null, connection.Query("select cast(null as smallint)").Single()); - Assert.Equal(connection.Query("select cast(42 as smallint)").Single(), (ShortEnum)42); - Assert.Equal(connection.Query("select cast(42 as smallint)").Single(), (ShortEnum?)42); - Assert.Equal(connection.Query("select cast(null as smallint)").Single(), (ShortEnum?)null); + Assert.Equal((ShortEnum)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((ShortEnum?)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((ShortEnum?)null, connection.Query("select cast(null as smallint)").Single()); var row = connection.Query( "select cast(1 as smallint) as NonNullableInt16, cast(2 as smallint) as NullableInt16, cast(3 as smallint) as NonNullableInt16Enum, cast(4 as smallint) as NullableInt16Enum") .Single(); - Assert.Equal(row.NonNullableInt16, (short)1); - Assert.Equal(row.NullableInt16, (short)2); + Assert.Equal((short)1, row.NonNullableInt16); + Assert.Equal((short)2, row.NullableInt16); Assert.Equal(ShortEnum.Three, row.NonNullableInt16Enum); Assert.Equal(ShortEnum.Four, row.NullableInt16Enum); @@ -707,29 +852,29 @@ public void TestInt16Usage() connection.Query( "select cast(5 as smallint) as NonNullableInt16, cast(null as smallint) as NullableInt16, cast(6 as smallint) as NonNullableInt16Enum, cast(null as smallint) as NullableInt16Enum") .Single(); - Assert.Equal(row.NonNullableInt16, (short)5); - Assert.Equal(row.NullableInt16, (short?)null); + Assert.Equal((short)5, row.NonNullableInt16); + Assert.Equal((short?)null, row.NullableInt16); Assert.Equal(ShortEnum.Six, row.NonNullableInt16Enum); - Assert.Equal(row.NullableInt16Enum, (ShortEnum?)null); + Assert.Equal((ShortEnum?)null, row.NullableInt16Enum); } [Fact] public void TestInt32Usage() { - Assert.Equal(connection.Query("select cast(42 as int)").Single(), (int)42); - Assert.Equal(connection.Query("select cast(42 as int)").Single(), (int?)42); - Assert.Equal(connection.Query("select cast(null as int)").Single(), (int?)null); + Assert.Equal((int)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((int?)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((int?)null, connection.Query("select cast(null as int)").Single()); - Assert.Equal(connection.Query("select cast(42 as int)").Single(), (IntEnum)42); - Assert.Equal(connection.Query("select cast(42 as int)").Single(), (IntEnum?)42); - Assert.Equal(connection.Query("select cast(null as int)").Single(), (IntEnum?)null); + Assert.Equal((IntEnum)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((IntEnum?)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((IntEnum?)null, connection.Query("select cast(null as int)").Single()); var row = connection.Query( "select cast(1 as int) as NonNullableInt32, cast(2 as int) as NullableInt32, cast(3 as int) as NonNullableInt32Enum, cast(4 as int) as NullableInt32Enum") .Single(); - Assert.Equal(row.NonNullableInt32, (int)1); - Assert.Equal(row.NullableInt32, (int)2); + Assert.Equal((int)1, row.NonNullableInt32); + Assert.Equal((int)2, row.NullableInt32); Assert.Equal(IntEnum.Three, row.NonNullableInt32Enum); Assert.Equal(IntEnum.Four, row.NullableInt32Enum); @@ -737,10 +882,10 @@ public void TestInt32Usage() connection.Query( "select cast(5 as int) as NonNullableInt32, cast(null as int) as NullableInt32, cast(6 as int) as NonNullableInt32Enum, cast(null as int) as NullableInt32Enum") .Single(); - Assert.Equal(row.NonNullableInt32, (int)5); - Assert.Equal(row.NullableInt32, (int?)null); + Assert.Equal((int)5, row.NonNullableInt32); + Assert.Equal((int?)null, row.NullableInt32); Assert.Equal(IntEnum.Six, row.NonNullableInt32Enum); - Assert.Equal(row.NullableInt32Enum, (IntEnum?)null); + Assert.Equal((IntEnum?)null, row.NullableInt32Enum); } public class WithInt16Values @@ -790,42 +935,34 @@ public Issue40_User() [Fact] public void ExecuteFromClosed() { - using (var conn = GetClosedConnection()) - { - conn.Execute("-- nop"); - Assert.Equal(ConnectionState.Closed, conn.State); - } + using var conn = GetClosedConnection(); + conn.Execute("-- nop"); + Assert.Equal(ConnectionState.Closed, conn.State); } [Fact] public void ExecuteInvalidFromClosed() { - using (var conn = GetClosedConnection()) - { - var ex = Assert.ThrowsAny(() => conn.Execute("nop")); - Assert.Equal(ConnectionState.Closed, conn.State); - } + using var conn = GetClosedConnection(); + var ex = Assert.ThrowsAny(() => conn.Execute("nop")); + Assert.Equal(ConnectionState.Closed, conn.State); } [Fact] public void QueryFromClosed() { - using (var conn = GetClosedConnection()) - { - var i = conn.Query("select 1").Single(); - Assert.Equal(ConnectionState.Closed, conn.State); - Assert.Equal(1, i); - } + using var conn = GetClosedConnection(); + var i = conn.Query("select 1").Single(); + Assert.Equal(ConnectionState.Closed, conn.State); + Assert.Equal(1, i); } [Fact] public void QueryInvalidFromClosed() { - using (var conn = GetClosedConnection()) - { - Assert.ThrowsAny(() => conn.Query("select gibberish").Single()); - Assert.Equal(ConnectionState.Closed, conn.State); - } + using var conn = GetClosedConnection(); + Assert.ThrowsAny(() => conn.Query("select gibberish").Single()); + Assert.Equal(ConnectionState.Closed, conn.State); } [Fact] @@ -864,7 +1001,7 @@ public void TestIssue131() "SELECT 1 Id, 'Mr' Title, 'John' Surname, 4 AddressCount", (person, addressCount) => person, splitOn: "AddressCount" - ).FirstOrDefault(); + ).First(); var asDict = (IDictionary)results; @@ -941,7 +1078,7 @@ public void TypeBasedViaDynamic() { Type type = Common.GetSomeType(); - dynamic template = Activator.CreateInstance(type); + dynamic template = Activator.CreateInstance(type)!; dynamic actual = CheetViaDynamic(template, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }); Assert.Equal(((object)actual).GetType(), type); int a = actual.A; @@ -955,7 +1092,7 @@ public void TypeBasedViaType() { Type type = Common.GetSomeType(); - dynamic actual = connection.Query(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).FirstOrDefault(); + dynamic actual = connection.Query(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).First(); Assert.Equal(((object)actual).GetType(), type); int a = actual.A; string b = actual.B; @@ -965,7 +1102,7 @@ public void TypeBasedViaType() private T CheetViaDynamic(T template, string query, object args) { - return connection.Query(query, args).SingleOrDefault(); + return connection.Query(query, args).Single(); } [Fact] @@ -1026,9 +1163,10 @@ public void Issue178_SqlServer() try { connection.Execute("create table Issue178(id int not null)"); } catch { /* don't care */ } // raw ADO.net - var sqlCmd = new SqlCommand(sql, connection); - using (IDataReader reader1 = sqlCmd.ExecuteReader()) + using (var sqlCmd = connection.CreateCommand()) { + sqlCmd.CommandText = sql; + using IDataReader reader1 = sqlCmd.ExecuteReader(); Assert.True(reader1.Read()); Assert.Equal(0, reader1.GetInt32(0)); Assert.False(reader1.Read()); @@ -1151,5 +1289,110 @@ private class HazGetOnly public int Id { get; } public string Name { get; } = "abc"; } + + [Fact] + public void TestConstructorParametersWithUnderscoredColumns() + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + var obj = connection.QuerySingle("select 42 as [id_property], 'def' as [name_property];"); + Assert.Equal(42, obj.IdProperty); + Assert.Equal("def", obj.NameProperty); + } + + private class HazGetOnlyAndCtor + { + public int IdProperty { get; } + public string NameProperty { get; } + + public HazGetOnlyAndCtor(int idProperty, string nameProperty) + { + IdProperty = idProperty; + NameProperty = nameProperty; + } + } + + [Fact] + public void Issue1164_OverflowExceptionForByte() + { + const string sql = "select cast(200 as smallint) as [value]"; // 200 more than sbyte.MaxValue but less than byte.MaxValue + Issue1164Object obj = connection.QuerySingle>(sql); + Assert.StrictEqual(200, obj.Value); + } + + [Fact] + public void Issue1164_OverflowExceptionForUInt16() + { + const string sql = "select cast(40000 as bigint) as [value]"; // 40000 more than short.MaxValue but less than ushort.MaxValue + Issue1164Object obj = connection.QuerySingle>(sql); + Assert.StrictEqual(40000, obj.Value); + } + + [Fact] + public void Issue1164_OverflowExceptionForUInt32() + { + const string sql = "select cast(4000000000 as bigint) as [value]"; // 4000000000 more than int.MaxValue but less than uint.MaxValue + Issue1164Object obj = connection.QuerySingle>(sql); + Assert.StrictEqual(4000000000, obj.Value); + } + + [Fact] + public void Issue1164_OverflowExceptionForUInt64() + { + const string sql = "select cast(10000000000000000000.0 as float) as [value]"; // 10000000000000000000 more than long.MaxValue but less than ulong.MaxValue + Issue1164Object obj = connection.QuerySingle>(sql); + Assert.StrictEqual(10000000000000000000, obj.Value); + } + + private class Issue1164Object + { + public T Value = default!; + } + + internal record struct One(int OID); + internal record struct Two(int OID, string Name); + + [Fact] + public async Task QuerySplitStruct() // https://github.com/DapperLib/Dapper/issues/2005 + { + var results = await connection.QueryAsync(@"SELECT 1 AS OID, 2 AS OID, 'Name' AS Name", (x,y) => (x,y), splitOn: "OID"); + + Assert.Single(results); + } + + [Fact] + public void SetDynamicProperty_WithReferenceType_Succeeds() + { + var obj = connection.QueryFirst("select 1 as ExistingProperty"); + + obj.ExistingProperty = "foo"; + Assert.Equal("foo", (string)obj.ExistingProperty); + + obj.NewProperty = new Uri("http://example.net/"); + Assert.Equal(new Uri("http://example.net/"), (Uri)obj.NewProperty); + } + + [Fact] + public void SetDynamicProperty_WithBoxedValueType_Succeeds() + { + var obj = connection.QueryFirst("select 'foo' as ExistingProperty"); + + obj.ExistingProperty = (object)1; + Assert.Equal(1, (int)obj.ExistingProperty); + + obj.NewProperty = (object)true; + Assert.True(obj.NewProperty); + } + + [Fact] + public void SetDynamicProperty_WithValueType_Succeeds() + { + var obj = connection.QueryFirst("select 'foo' as ExistingProperty"); + + obj.ExistingProperty = 1; + Assert.Equal(1, (int)obj.ExistingProperty); + + obj.NewProperty = true; + Assert.True(obj.NewProperty); + } } } diff --git a/Dapper.Tests/MultiMapTests.cs b/tests/Dapper.Tests/MultiMapTests.cs similarity index 86% rename from Dapper.Tests/MultiMapTests.cs rename to tests/Dapper.Tests/MultiMapTests.cs index ec612d03d..510860e84 100644 --- a/Dapper.Tests/MultiMapTests.cs +++ b/tests/Dapper.Tests/MultiMapTests.cs @@ -6,7 +6,13 @@ namespace Dapper.Tests { - public class MultiMapTests : TestBase + [Collection("MultiMapTests")] + public sealed class SystemSqlClientMultiMapTests : MultiMapTests { } +#if MSSQLCLIENT + [Collection("MultiMapTests")] + public sealed class MicrosoftSqlClientMultiMapTests : MultiMapTests { } +#endif + public abstract class MultiMapTests : TestBase where TProvider : DatabaseProvider { [Fact] public void ParentChildIdentityAssociations() @@ -15,7 +21,7 @@ public void ParentChildIdentityAssociations() var parents = connection.Query("select 1 as [Id], 1 as [Id] union all select 1,2 union all select 2,3 union all select 1,4 union all select 3,5", (parent, child) => { - if (!lookup.TryGetValue(parent.Id, out Parent found)) + if (!lookup.TryGetValue(parent.Id, out Parent? found)) { lookup.Add(parent.Id, found = parent); } @@ -66,6 +72,7 @@ public void TestMultiMap() Assert.Equal("Sams Post1", p.Content); Assert.Equal(1, p.Id); + Assert.NotNull(p.Owner); Assert.Equal("Sam", p.Owner.Name); Assert.Equal(99, p.Owner.Id); @@ -126,13 +133,11 @@ private class Multi2 [Fact] public void QueryMultimapFromClosed() { - using (var conn = GetClosedConnection()) - { - Assert.Equal(ConnectionState.Closed, conn.State); - var i = conn.Query("select 2 as [Id], 3 as [Id]", (x, y) => x.Id + y.Id).Single(); - Assert.Equal(ConnectionState.Closed, conn.State); - Assert.Equal(5, i); - } + using var conn = GetClosedConnection(); + Assert.Equal(ConnectionState.Closed, conn.State); + var i = conn.Query("select 2 as [Id], 3 as [Id]", (x, y) => x.Id + y.Id).Single(); + Assert.Equal(ConnectionState.Closed, conn.State); + Assert.Equal(5, i); } [Fact] @@ -168,7 +173,10 @@ public void TestMultiMapThreeTypesWithGridReader() var post2 = grid.Read((post, user, comment) => { post.Owner = user; post.Comment = comment; return post; }).SingleOrDefault(); + Assert.NotNull(post2); + Assert.NotNull(post2.Comment); Assert.Equal(1, post2.Comment.Id); + Assert.NotNull(post2.Owner); Assert.Equal(99, post2.Owner.Id); } finally @@ -188,6 +196,13 @@ public void TestMultiMapperIsNotConfusedWithUnorderedCols() Assert.Equal("a", result.Item2.Name); } + [Fact] + public void TestMultiMapperSplitOnError() + { + var ex = Assert.Throws(() => connection.Query>("select 1 as Id, 2 as BarId", Tuple.Create, splitOn: "DoesntExist").First()); + Assert.StartsWith("Multi-map error: splitOn column 'DoesntExist' was not found - please ensure your splitOn parameter is set and in the correct order", ex.Message); + } + [Fact] public void TestMultiMapDynamic() { @@ -235,6 +250,7 @@ public void TestMultiMapWithSplit() // https://stackoverflow.com/q/6056778/23354 // assertions Assert.Equal(1, product.Id); Assert.Equal("abc", product.Name); + Assert.NotNull(product.Category); Assert.Equal(2, product.Category.Id); Assert.Equal("def", product.Category.Name); } @@ -338,9 +354,9 @@ public PostWithConstructor(int id, int ownerid, string content) } public int Ident { get; set; } - public UserWithConstructor Owner { get; set; } + public UserWithConstructor? Owner { get; set; } public string FullContent { get; set; } - public Comment Comment { get; set; } + public Comment? Comment { get; set; } } [Fact] @@ -367,6 +383,7 @@ public void TestMultiMapWithConstructor() Assert.Equal("Sams Post1", p.FullContent); Assert.Equal(1, p.Ident); + Assert.NotNull(p.Owner); Assert.Equal("Sam", p.Owner.FullName); Assert.Equal(99, p.Owner.Ident); @@ -439,6 +456,15 @@ public void TestMultiMapArbitraryMaps() var p = data[0]; Assert.Equal(1, p.Id); Assert.Equal("Review Board 1", p.Name); + Assert.NotNull(p.User1); + Assert.NotNull(p.User2); + Assert.NotNull(p.User3); + Assert.NotNull(p.User4); + Assert.NotNull(p.User5); + Assert.NotNull(p.User6); + Assert.NotNull(p.User7); + Assert.NotNull(p.User8); + Assert.NotNull(p.User9); Assert.Equal(1, p.User1.Id); Assert.Equal(2, p.User2.Id); Assert.Equal(3, p.User3.Id); @@ -499,6 +525,7 @@ Order by p.Id Assert.Equal("Sams Post1", p.Content); Assert.Equal(1, p.Id); + Assert.NotNull(p.Owner); Assert.Equal(p.Owner.Name, "Sam" + i); Assert.Equal(99, p.Owner.Id); @@ -552,7 +579,7 @@ public void TestMultiMappingWithSplitOnSpaceBetweenCommas() private class Extra { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } } [Fact] @@ -570,6 +597,7 @@ public void TestMultiMappingWithNonReturnedProperty() Assert.Equal(1, postWithBlog.PostId); Assert.Equal("Title", postWithBlog.Title); + Assert.NotNull(postWithBlog.Blog); Assert.Equal(2, postWithBlog.Blog.BlogId); Assert.Equal("Blog", postWithBlog.Blog.Title); } @@ -577,15 +605,15 @@ public void TestMultiMappingWithNonReturnedProperty() private class Post_DupeProp { public int PostId { get; set; } - public string Title { get; set; } + public string? Title { get; set; } public int BlogId { get; set; } - public Blog_DupeProp Blog { get; set; } + public Blog_DupeProp? Blog { get; set; } } private class Blog_DupeProp { public int BlogId { get; set; } - public string Title { get; set; } + public string? Title { get; set; } } // see https://stackoverflow.com/questions/16955357/issue-about-dapper @@ -599,37 +627,38 @@ public void TestSplitWithMissingMembers() (T, P) => { T.Author = P; return T; }, null, null, true, "ID,Name").Single(); - result.ID.Equals(123); - result.Title.Equals("abc"); - result.CreateDate.Equals(new DateTime(2013, 2, 1)); + Assert.Equal(123, result.ID); + Assert.Equal("abc", result.Title); + Assert.Equal(new DateTime(2013, 2, 1), result.CreateDate); Assert.Null(result.Name); Assert.Null(result.Content); - result.Author.Phone.Equals("def"); - result.Author.Name.Equals("ghi"); - result.Author.ID.Equals(0); + Assert.NotNull(result.Author); + Assert.Equal("def", result.Author.Phone); + Assert.Equal("ghi", result.Author.Name); + Assert.Equal(0, result.Author.ID); Assert.Null(result.Author.Address); } public class Profile { public int ID { get; set; } - public string Name { get; set; } - public string Phone { get; set; } - public string Address { get; set; } + public string? Name { get; set; } + public string? Phone { get; set; } + public string? Address { get; set; } //public ExtraInfo Extra { get; set; } } public class Topic { public int ID { get; set; } - public string Title { get; set; } + public string? Title { get; set; } public DateTime CreateDate { get; set; } - public string Content { get; set; } + public string? Content { get; set; } public int UID { get; set; } public int TestColum { get; set; } - public string Name { get; set; } - public Profile Author { get; set; } + public string? Name { get; set; } + public Profile? Author { get; set; } //public Attachment Attach { get; set; } } diff --git a/Dapper.Tests/NullTests.cs b/tests/Dapper.Tests/NullTests.cs similarity index 84% rename from Dapper.Tests/NullTests.cs rename to tests/Dapper.Tests/NullTests.cs index 5bce7b586..6e24e4e16 100644 --- a/Dapper.Tests/NullTests.cs +++ b/tests/Dapper.Tests/NullTests.cs @@ -3,7 +3,13 @@ namespace Dapper.Tests { [Collection(NonParallelDefinition.Name)] - public class NullTests : TestBase + public sealed class SystemSqlClientNullTests : NullTests { } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] + public sealed class MicrosoftSqlClientNullTests : NullTests { } +#endif + + public abstract class NullTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestNullableDefault() diff --git a/Dapper.Tests/ParameterTests.cs b/tests/Dapper.Tests/ParameterTests.cs similarity index 68% rename from Dapper.Tests/ParameterTests.cs rename to tests/Dapper.Tests/ParameterTests.cs index 2fbbc37aa..5eb455c65 100644 --- a/Dapper.Tests/ParameterTests.cs +++ b/tests/Dapper.Tests/ParameterTests.cs @@ -3,25 +3,33 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; -using System.Data.SqlClient; +using System.Data.Common; using System.Data.SqlTypes; +using System.Diagnostics; using System.Dynamic; -using System.Linq; -using Xunit; using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; -using System.Diagnostics; +using Xunit; #if ENTITY_FRAMEWORK using System.Data.Entity.Spatial; using Microsoft.SqlServer.Types; #endif +[assembly: CollectionBehavior(DisableTestParallelization = true)] + namespace Dapper.Tests { - public class ParameterTests : TestBase + [Collection(NonParallelDefinition.Name)] // because it creates SQL types that compete between the two providers + public sealed class SystemSqlClientParameterTests : ParameterTests { } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] // because it creates SQL types that compete between the two providers + public sealed class MicrosoftSqlClientParameterTests : ParameterTests { } +#endif + public abstract class ParameterTests : TestBase where TProvider : DatabaseProvider { - public class DbParams : SqlMapper.IDynamicParameters, IEnumerable + public class DbDynamicParams : SqlMapper.IDynamicParameters, IEnumerable { private readonly List parameters = new List(); public IEnumerator GetEnumerator() { return parameters.GetEnumerator(); } @@ -37,8 +45,56 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id command.Parameters.Add(parameter); } } + + public class DbCustomParam : SqlMapper.ICustomQueryParameter + { + private readonly IDbDataParameter _sqlParameter; + + public DbCustomParam(IDbDataParameter sqlParameter) + { + _sqlParameter = sqlParameter; + } + + public void AddParameter(IDbCommand command, string name) + { + command.Parameters.Add(_sqlParameter); + } + } + + public readonly struct DbCustomParamStruct : SqlMapper.ICustomQueryParameter + { + private readonly IDbDataParameter _sqlParameter; + + public DbCustomParamStruct(IDbDataParameter sqlParameter) + { + _sqlParameter = sqlParameter; + } + + public void AddParameter(IDbCommand command, string name) + { + command.Parameters.Add(_sqlParameter); + } + } - private static List CreateSqlDataRecordList(IEnumerable numbers) + private static IEnumerable CreateSqlDataRecordList(IDbCommand command, IEnumerable numbers) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (command is System.Data.SqlClient.SqlCommand) return CreateSqlDataRecordList_SD(numbers); +#pragma warning restore CS0618 // Type or member is obsolete + if (command is Microsoft.Data.SqlClient.SqlCommand) return CreateSqlDataRecordList_MD(numbers); + throw new ArgumentException(nameof(command)); + } + private static IEnumerable CreateSqlDataRecordList(IDbConnection connection, IEnumerable numbers) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (connection is System.Data.SqlClient.SqlConnection) return CreateSqlDataRecordList_SD(numbers); +#pragma warning restore CS0618 // Type or member is obsolete + if (connection is Microsoft.Data.SqlClient.SqlConnection) return CreateSqlDataRecordList_MD(numbers); + throw new ArgumentException(nameof(connection)); + } + +#pragma warning disable CS0618 // Type or member is obsolete + private static List CreateSqlDataRecordList_SD(IEnumerable numbers) { var number_list = new List(); @@ -55,6 +111,26 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id return number_list; } +#pragma warning restore CS0618 // Type or member is obsolete + + private static List CreateSqlDataRecordList_MD(IEnumerable numbers) + { + var number_list = new List(); + + // Create an SqlMetaData object that describes our table type. + Microsoft.Data.SqlClient.Server.SqlMetaData[] tvp_definition = { new Microsoft.Data.SqlClient.Server.SqlMetaData("n", SqlDbType.Int) }; + + foreach (int n in numbers) + { + // Create a new record, using the metadata array above. + var rec = new Microsoft.Data.SqlClient.Server.SqlDataRecord(tvp_definition); + rec.SetInt32(0, n); // Set the value. + number_list.Add(rec); // Add it to the list. + } + + return number_list; + } + private class IntDynamicParam : SqlMapper.IDynamicParameters { @@ -66,16 +142,11 @@ public IntDynamicParam(IEnumerable numbers) public void AddParameters(IDbCommand command, SqlMapper.Identity identity) { - var sqlCommand = (SqlCommand)command; - sqlCommand.CommandType = CommandType.StoredProcedure; + command.CommandType = CommandType.StoredProcedure; - var number_list = CreateSqlDataRecordList(numbers); + var number_list = CreateSqlDataRecordList(command, numbers); - // Add the table parameter. - var p = sqlCommand.Parameters.Add("ints", SqlDbType.Structured); - p.Direction = ParameterDirection.Input; - p.TypeName = "int_list_type"; - p.Value = number_list; + AddStructured(command, number_list); } } @@ -89,17 +160,37 @@ public IntCustomParam(IEnumerable numbers) public void AddParameter(IDbCommand command, string name) { - var sqlCommand = (SqlCommand)command; - sqlCommand.CommandType = CommandType.StoredProcedure; + command.CommandType = CommandType.StoredProcedure; - var number_list = CreateSqlDataRecordList(numbers); + var number_list = CreateSqlDataRecordList(command, numbers); // Add the table parameter. - var p = sqlCommand.Parameters.Add(name, SqlDbType.Structured); + AddStructured(command, number_list); + } + } + + private static IDbDataParameter AddStructured(IDbCommand command, object value) + { +#pragma warning disable CS0618 // Type or member is obsolete + if (command is System.Data.SqlClient.SqlCommand sdcmd) + { + var p = sdcmd.Parameters.Add("integers", SqlDbType.Structured); + p.Direction = ParameterDirection.Input; + p.TypeName = "int_list_type"; + p.Value = value; + return p; + } +#pragma warning restore CS0618 // Type or member is obsolete + else if (command is Microsoft.Data.SqlClient.SqlCommand mdcmd) + { + var p = mdcmd.Parameters.Add("integers", SqlDbType.Structured); p.Direction = ParameterDirection.Input; p.TypeName = "int_list_type"; - p.Value = number_list; + p.Value = value; + return p; } + else + throw new ArgumentException(nameof(command)); } /* TODO: @@ -148,8 +239,8 @@ public void PassInIntArray() public void PassInEmptyIntArray() { Assert.Equal( - new int[0], - connection.Query("select * from (select 1 as Id union all select 2 union all select 3) as X where Id in @Ids", new { Ids = new int[0] }) + Array.Empty(), + connection.Query("select * from (select 1 as Id union all select 2 union all select 3) as X where Id in @Ids", new { Ids = Array.Empty() }) ); } @@ -174,20 +265,20 @@ public void GuidIn_SO_24177902() // check that rows 2&3 yield guids b&c var guids = connection.Query("select g from #foo where i in (2,3)").ToArray(); - guids.Length.Equals(2); - guids.Contains(a).Equals(false); - guids.Contains(b).Equals(true); - guids.Contains(c).Equals(true); - guids.Contains(d).Equals(false); + Assert.Equal(2, guids.Length); + Assert.DoesNotContain(a, guids); + Assert.Contains(b, guids); + Assert.Contains(c, guids); + Assert.DoesNotContain(d, guids); // in query on the guids var rows = connection.Query("select * from #foo where g in @guids order by i", new { guids }) .Select(row => new { i = (int)row.i, g = (Guid)row.g }).ToArray(); - rows.Length.Equals(2); - rows[0].i.Equals(2); - rows[0].g.Equals(b); - rows[1].i.Equals(3); - rows[1].g.Equals(c); + Assert.Equal(2, rows.Length); + Assert.Equal(2, rows[0].i); + Assert.Equal(b, rows[0].g); + Assert.Equal(3, rows[1].i); + Assert.Equal(c, rows[1].g); } [FactUnlessCaseSensitiveDatabase] @@ -214,6 +305,7 @@ public void TestMassiveStrings() Assert.Equal(connection.Query("select @a", new { a = str }).First(), str); } + [Fact] public void TestTVPWithAnonymousObject() { @@ -241,6 +333,37 @@ public void TestTVPWithAnonymousObject() } } + [Fact] + public void TestTVPWithAnonymousEmptyObject() + { + try + { + connection.Execute("CREATE TYPE int_list_type AS TABLE (n int NOT NULL PRIMARY KEY)"); + connection.Execute("CREATE PROC get_ints @integers int_list_type READONLY AS select * from @integers"); + + var nums = connection.Query("get_ints", new { integers = new IntCustomParam(new int[] { }) }, commandType: CommandType.StoredProcedure).ToList(); + Assert.Equal(1, nums[0]); + Assert.Equal(2, nums[1]); + Assert.Equal(3, nums[2]); + Assert.Equal(3, nums.Count); + } + catch (ArgumentException ex) + { + Assert.True(string.Compare(ex.Message, "There are no records in the SqlDataRecord enumeration. To send a table-valued parameter with no rows, use a null reference for the value instead.") == 0); + } + finally + { + try + { + connection.Execute("DROP PROC get_ints"); + } + finally + { + connection.Execute("DROP TYPE int_list_type"); + } + } + } + // SQL Server specific test to demonstrate TVP [Fact] public void TestTVP() @@ -248,7 +371,7 @@ public void TestTVP() try { connection.Execute("CREATE TYPE int_list_type AS TABLE (n int NOT NULL PRIMARY KEY)"); - connection.Execute("CREATE PROC get_ints @ints int_list_type READONLY AS select * from @ints"); + connection.Execute("CREATE PROC get_ints @integers int_list_type READONLY AS select * from @integers"); var nums = connection.Query("get_ints", new IntDynamicParam(new int[] { 1, 2, 3 })).ToList(); Assert.Equal(1, nums[0]); @@ -260,11 +383,11 @@ public void TestTVP() { try { - connection.Execute("DROP PROC get_ints"); + try { connection.Execute("DROP PROC get_ints"); } catch { } } finally { - connection.Execute("DROP TYPE int_list_type"); + try { connection.Execute("DROP TYPE int_list_type"); } catch { } } } } @@ -281,16 +404,12 @@ public DynamicParameterWithIntTVP(IEnumerable numbers) { base.AddParameters(command, identity); - var sqlCommand = (SqlCommand)command; - sqlCommand.CommandType = CommandType.StoredProcedure; + command.CommandType = CommandType.StoredProcedure; - var number_list = CreateSqlDataRecordList(numbers); + var number_list = CreateSqlDataRecordList(command, numbers); // Add the table parameter. - var p = sqlCommand.Parameters.Add("ints", SqlDbType.Structured); - p.Direction = ParameterDirection.Input; - p.TypeName = "int_list_type"; - p.Value = number_list; + AddStructured(command, number_list); } } @@ -300,7 +419,7 @@ public void TestTVPWithAdditionalParams() try { connection.Execute("CREATE TYPE int_list_type AS TABLE (n int NOT NULL PRIMARY KEY)"); - connection.Execute("CREATE PROC get_values @ints int_list_type READONLY, @stringParam varchar(20), @dateParam datetime AS select i.*, @stringParam as stringParam, @dateParam as dateParam from @ints i"); + connection.Execute("CREATE PROC get_values @integers int_list_type READONLY, @stringParam varchar(20), @dateParam datetime AS select i.*, @stringParam as stringParam, @dateParam as dateParam from @integers i"); var dynamicParameters = new DynamicParameterWithIntTVP(new int[] { 1, 2, 3 }); dynamicParameters.AddDynamicParams(new { stringParam = "stringParam", dateParam = new DateTime(2012, 1, 1) }); @@ -336,7 +455,7 @@ public void TestSqlDataRecordListParametersWithAsTableValuedParameter() connection.Execute("CREATE TYPE int_list_type AS TABLE (n int NOT NULL PRIMARY KEY)"); connection.Execute("CREATE PROC get_ints @integers int_list_type READONLY AS select * from @integers"); - var records = CreateSqlDataRecordList(new int[] { 1, 2, 3 }); + var records = CreateSqlDataRecordList(connection, new int[] { 1, 2, 3 }); var nums = connection.Query("get_ints", new { integers = records.AsTableValuedParameter() }, commandType: CommandType.StoredProcedure).ToList(); Assert.Equal(new int[] { 1, 2, 3 }, nums); @@ -367,6 +486,33 @@ public void TestSqlDataRecordListParametersWithAsTableValuedParameter() } } + [Fact] + public void TestEmptySqlDataRecordListParametersWithAsTableValuedParameter() + { + try + { + connection.Execute("CREATE TYPE int_list_type AS TABLE (n int NOT NULL PRIMARY KEY)"); + connection.Execute("CREATE PROC get_ints @integers int_list_type READONLY AS select * from @integers"); + + + var emptyRecord = CreateSqlDataRecordList(connection, Enumerable.Empty()); + + var nums = connection.Query("get_ints", new { integers = emptyRecord.AsTableValuedParameter() }, commandType: CommandType.StoredProcedure).ToList(); + Assert.True(nums.Count == 0); + } + finally + { + try + { + connection.Execute("DROP PROC get_ints"); + } + finally + { + connection.Execute("DROP TYPE int_list_type"); + } + } + } + [Fact] public void TestSqlDataRecordListParametersWithTypeHandlers() { @@ -376,14 +522,30 @@ public void TestSqlDataRecordListParametersWithTypeHandlers() connection.Execute("CREATE PROC get_ints @integers int_list_type READONLY AS select * from @integers"); // Variable type has to be IEnumerable for TypeHandler to kick in. - IEnumerable records = CreateSqlDataRecordList(new int[] { 1, 2, 3 }); + object args; +#pragma warning disable CS0618 // Type or member is obsolete + if (connection is System.Data.SqlClient.SqlConnection) + { + IEnumerable records = CreateSqlDataRecordList_SD(new int[] { 1, 2, 3 }); + args = new { integers = records }; + } +#pragma warning restore CS0618 // Type or member is obsolete + else if (connection is Microsoft.Data.SqlClient.SqlConnection) + { + IEnumerable records = CreateSqlDataRecordList_MD(new int[] { 1, 2, 3 }); + args = new { integers = records }; + } + else + { + throw new ArgumentException(nameof(connection)); + } - var nums = connection.Query("get_ints", new { integers = records }, commandType: CommandType.StoredProcedure).ToList(); + var nums = connection.Query("get_ints", args, commandType: CommandType.StoredProcedure).ToList(); Assert.Equal(new int[] { 1, 2, 3 }, nums); try { - connection.Query("select * from @integers", new { integers = records }).First(); + connection.Query("select * from @integers", args).First(); throw new InvalidOperationException(); } catch (Exception ex) @@ -404,7 +566,6 @@ public void TestSqlDataRecordListParametersWithTypeHandlers() } } -#if !NETCOREAPP1_0 [Fact] public void DataTableParameters() { @@ -447,7 +608,7 @@ public void SO29533765_DataTableParametersViaDynamicParameters() var table = new DataTable { TableName = "MyTVPType", Columns = { { "id", typeof(int) } }, Rows = { { 1 }, { 2 }, { 3 } } }; table.SetTypeName(table.TableName); // per SO29533765 - IDictionary args = new Dictionary + IDictionary args = new Dictionary(1) { ["ids"] = table }; @@ -514,13 +675,13 @@ public void DataTableParametersWithExtendedProperty() public void SupportInit() { var obj = connection.Query("select 'abc' as Value").Single(); - obj.Value.Equals("abc"); - obj.Flags.Equals(31); + Assert.Equal("abc", obj.Value); + Assert.Equal(31, obj.Flags); } public class WithInit : ISupportInitialize { - public string Value { get; set; } + public string? Value { get; set; } public int Flags { get; set; } void ISupportInitialize.BeginInit() => Flags++; @@ -554,14 +715,19 @@ public SO29596645_RuleTableValuedParameters(string parameterName) public void AddParameters(IDbCommand command, SqlMapper.Identity identity) { Debug.WriteLine("> AddParameters"); - var lazy = (SqlCommand)command; - lazy.Parameters.AddWithValue("Id", 7); + var p = command.CreateParameter(); + p.ParameterName = "Id"; + p.Value = 7; + command.Parameters.Add(p); var table = new DataTable { Columns = { { "Id", typeof(int) } }, Rows = { { 4 }, { 9 } } }; - lazy.Parameters.AddWithValue("Rules", table); + p = command.CreateParameter(); + p.ParameterName = "Rules"; + p.Value = table; + command.Parameters.Add(p); Debug.WriteLine("< AddParameters"); } } @@ -575,26 +741,27 @@ public SO29596645_OrganisationDTO() Rules = new SO29596645_RuleTableValuedParameters("@Rules"); } } -#endif #if ENTITY_FRAMEWORK private class HazGeo { public int Id { get; set; } - public DbGeography Geo { get; set; } - public DbGeometry Geometry { get; set; } + public DbGeography? Geo { get; set; } + public DbGeometry? Geometry { get; set; } } private class HazSqlGeo { public int Id { get; set; } - public SqlGeography Geo { get; set; } - public SqlGeometry Geometry { get; set; } + public SqlGeography? Geo { get; set; } + public SqlGeometry? Geometry { get; set; } } [Fact] public void DBGeography_SO24405645_SO24402424() { + SkipIfMsDataClient(); + EntityFramework.Handlers.Register(); connection.Execute("create table #Geo (id int, geo geography, geometry geometry)"); @@ -616,6 +783,8 @@ public void DBGeography_SO24405645_SO24402424() [Fact] public void SqlGeography_SO25538154() { + SkipIfMsDataClient(); + SqlMapper.ResetTypeHandlers(); connection.Execute("create table #SqlGeo (id int, geo geography, geometry geometry)"); @@ -654,9 +823,11 @@ public void NullableSqlGeometry() [Fact] public void SqlHierarchyId_SO18888911() { + SkipIfMsDataClient(); + SqlMapper.ResetTypeHandlers(); var row = connection.Query("select 3 as [Id], hierarchyid::Parse('/1/2/3/') as [Path]").Single(); - row.Id.Equals(3); + Assert.Equal(3, row.Id); Assert.NotEqual(default(SqlHierarchyId), row.Path); var val = connection.Query("select @Path", row).Single(); @@ -672,11 +843,72 @@ public class HazSqlHierarchy #endif [Fact] - public void TestCustomParameters() + public void TestDynamicParameters() + { + var args = new DbDynamicParams { + Provider.CreateRawParameter("foo", 123), + Provider.CreateRawParameter("bar", "abc") + }; + var result = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + int foo = result.Foo; + string bar = result.Bar; + Assert.Equal(123, foo); + Assert.Equal("abc", bar); + } + + [Fact] + public void TestDynamicParametersReuse() + { + var args = new DbDynamicParams { + Provider.CreateRawParameter("foo", 123), + Provider.CreateRawParameter("bar", "abc") + }; + var result1 = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + var result2 = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + Assert.Equal(123, result1.Foo); + Assert.Equal("abc", result1.Bar); + Assert.Equal(123, result2.Foo); + Assert.Equal("abc", result2.Bar); + } + + + [Fact] + public void TestCustomParameter() + { + var args = new { + foo = new DbCustomParam(Provider.CreateRawParameter("foo", 123)), + bar = "abc" + }; + var result = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + int foo = result.Foo; + string bar = result.Bar; + Assert.Equal(123, foo); + Assert.Equal("abc", bar); + } + + [Fact] + public void TestCustomParameterReuse() + { + var args = new { + foo = new DbCustomParam(Provider.CreateRawParameter("foo", 123)), + bar = "abc" + }; + var result1 = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + var result2 = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); + Assert.Equal(123, result1.Foo); + Assert.Equal("abc", result1.Bar); + Assert.Equal(123, result2.Foo); + Assert.Equal("abc", result2.Bar); + } + + [Fact] + public void TestCustomParameterValueType() { - var args = new DbParams { - new SqlParameter("foo", 123), - new SqlParameter("bar", "abc") + // Value type (struct) ICustomQueryParameter previously caused a segfault + // because the IL emitted Callvirt on an unboxed struct (see #2189) + var args = new { + foo = new DbCustomParamStruct(Provider.CreateRawParameter("foo", 123)), + bar = "abc" }; var result = connection.Query("select Foo=@foo, Bar=@bar", args).Single(); int foo = result.Foo; @@ -714,7 +946,7 @@ public void TestAppendingAnonClasses() [Fact] public void TestAppendingADictionary() { - var dictionary = new Dictionary + var dictionary = new Dictionary(2) { ["A"] = 1, ["B"] = "two" @@ -764,7 +996,7 @@ public void TestAppendingAListAsDictionary() { var p = new DynamicParameters(); var list = new int[] { 1, 2, 3 }; - var args = new Dictionary { ["ids"] = list }; + var args = new Dictionary(1) { ["ids"] = list }; p.AddDynamicParams(args); var result = connection.Query("select * from (select 1 A union all select 2 union all select 3) X where A in @ids", p).ToList(); @@ -868,8 +1100,8 @@ public void TestSupportForDynamicParametersOutputExpressions() p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); connection.Execute(@" SET @Occupation = 'grillmaster' @@ -896,8 +1128,8 @@ public void TestSupportForDynamicParametersOutputExpressions_Scalar() p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = (int)connection.ExecuteScalar(@" SET @Occupation = 'grillmaster' @@ -905,7 +1137,7 @@ public void TestSupportForDynamicParametersOutputExpressions_Scalar() SET @NumberOfLegs = @NumberOfLegs - 1 SET @AddressName = 'bobs burgers' SET @AddressPersonId = @PersonId -select 42", p); +select 42", p)!; Assert.Equal("grillmaster", bob.Occupation); Assert.Equal(2, bob.PersonId); @@ -927,8 +1159,8 @@ public void TestSupportForDynamicParametersOutputExpressions_Query_Buffered() p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = connection.Query(@" SET @Occupation = 'grillmaster' @@ -958,8 +1190,8 @@ public void TestSupportForDynamicParametersOutputExpressions_Query_NonBuffered() p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); var result = connection.Query(@" SET @Occupation = 'grillmaster' @@ -989,8 +1221,8 @@ public void TestSupportForDynamicParametersOutputExpressions_QueryMultiple() p.Output(bob, b => b.PersonId); p.Output(bob, b => b.Occupation); p.Output(bob, b => b.NumberOfLegs); - p.Output(bob, b => b.Address.Name); - p.Output(bob, b => b.Address.PersonId); + p.Output(bob, b => b.Address!.Name); + p.Output(bob, b => b.Address!.PersonId); int x, y; using (var multi = connection.QueryMultiple(@" @@ -1044,7 +1276,7 @@ public void SO25069578_DynamicParams_Procs() public class HazX { - public string X { get; set; } + public string? X { get; set; } } [Fact] @@ -1060,7 +1292,7 @@ public void SO25297173_DynamicIn() insert @table values(6); insert @table values(7); SELECT value FROM @table WHERE value IN @myIds"; - var queryParams = new Dictionary + var queryParams = new Dictionary(1) { ["myIds"] = new[] { 5, 6 } }; @@ -1097,7 +1329,7 @@ public void Test_AddDynamicParametersRepeatedIfParamTypeIsDbStiringShouldWork() [Fact] public void AllowIDictionaryParameters() { - var parameters = new Dictionary + var parameters = new Dictionary(1) { ["param1"] = 0 }; @@ -1120,7 +1352,7 @@ select @A public class ParameterWithIndexer { public int A { get; set; } - public virtual string this[string columnName] + public virtual string? this[string columnName] { get { return null; } set { } @@ -1143,13 +1375,13 @@ public class MultipleParametersWithIndexer : MultipleParametersWithIndexerDeclar public class MultipleParametersWithIndexerDeclaringType { - public object this[object field] + public object? this[object field] { get { return null; } set { } } - public object this[object field, int index] + public object? this[object field, int index] { get { return null; } set { } @@ -1170,15 +1402,16 @@ public void Issue182_BindDynamicObjectParametersAndColumns() var fromDb = connection.Query("select * from #Dyno where Id=@Id", orig).Single(); Assert.Equal((Guid)fromDb.Id, guid); Assert.Equal("T Rex", fromDb.Name); + Assert.NotNull(fromDb.Foo); Assert.Equal(123L, (long)fromDb.Foo); } public class Dyno { - public dynamic Id { get; set; } - public string Name { get; set; } + public dynamic? Id { get; set; } + public string? Name { get; set; } - public object Foo { get; set; } + public object? Foo { get; set; } } [Fact] @@ -1189,8 +1422,8 @@ public void Issue151_ExpandoObjectArgsQuery() args.Name = "abc"; var row = connection.Query("select @Id as [Id], @Name as [Name]", (object)args).Single(); - ((int)row.Id).Equals(123); - ((string)row.Name).Equals("abc"); + Assert.Equal(123, (int)row.Id); + Assert.Equal("abc", (string)row.Name); } [Fact] @@ -1202,8 +1435,8 @@ public void Issue151_ExpandoObjectArgsExec() connection.Execute("create table #issue151 (Id int not null, Name nvarchar(20) not null)"); Assert.Equal(1, connection.Execute("insert #issue151 values(@Id, @Name)", (object)args)); var row = connection.Query("select Id, Name from #issue151").Single(); - ((int)row.Id).Equals(123); - ((string)row.Name).Equals("abc"); + Assert.Equal(123, (int)row.Id); + Assert.Equal("abc", (string)row.Name); } [Fact] @@ -1315,12 +1548,42 @@ public void Issue601_InternationalParameterNamesWork() Assert.Equal(42, result); } - [Fact] + [FactLongRunning] public void TestListExpansionPadding_Enabled() => TestListExpansionPadding(true); - [Fact] + [FactLongRunning] public void TestListExpansionPadding_Disabled() => TestListExpansionPadding(false); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void OleDbParamFilterFails(bool legacyParameterToken) + { + SqlMapper.PurgeQueryCache(); + var oldValue = SqlMapper.Settings.SupportLegacyParameterTokens; + try + { + SqlMapper.Settings.SupportLegacyParameterTokens = legacyParameterToken; + + if (legacyParameterToken) // OLE DB parameter support enabled; can false-positive + { + Assert.Throws(() => GetValue(connection)); + } + else // OLE DB parameter support disabled; more reliable + { + Assert.Equal("this ? could be awkward", GetValue(connection)); + } + } + finally + { + SqlMapper.Settings.SupportLegacyParameterTokens = oldValue; + } + + static string GetValue(DbConnection connection) + => connection.QuerySingle("select 'this ? could be awkward'", + new TypeWithDodgyProperties()); + } + private void TestListExpansionPadding(bool enabled) { bool oldVal = SqlMapper.Settings.PadListExpansions; @@ -1405,5 +1668,123 @@ private static int GetExpectedListExpansionCount(int count, bool enabled) if (delta != 0) blocks++; return blocks * padFactor; } + + [Fact] + public void Issue1907_SqlDecimalPreciseValues() + { + bool close = false; + try + { + if (connection.State != ConnectionState.Open) + { + connection.Open(); + close = true; + } + connection.Execute(@" +create table #Issue1907 ( + Id int not null primary key identity(1,1), + Value numeric(30,15) not null);"); + + const string PreciseValue = "999999999999999.999999999999999"; + SqlDecimal sentValue = SqlDecimal.Parse(PreciseValue), recvValue; + connection.Execute("insert #Issue1907 (Value) values (@value)", new { value = sentValue }); + + // access via vendor-specific API; if this fails, nothing else can work + using (var wrappedReader = connection.ExecuteReader("select Id, Value from #Issue1907")) + { + var reader = Assert.IsAssignableFrom(wrappedReader).Reader; + Assert.True(reader.Read()); + if (reader is Microsoft.Data.SqlClient.SqlDataReader msReader) + { + recvValue = msReader.GetSqlDecimal(1); + } +#pragma warning disable CS0618 // Type or member is obsolete + else if (reader is System.Data.SqlClient.SqlDataReader sdReader) + { + recvValue = sdReader.GetSqlDecimal(1); + } +#pragma warning restore CS0618 // Type or member is obsolete + else + { + throw new InvalidOperationException($"unexpected reader type: {reader.GetType().FullName}"); + } + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + Assert.False(reader.Read()); + Assert.False(reader.NextResult()); + } + + // access via generic API + using (var wrappedReader = connection.ExecuteReader("select Id, Value from #Issue1907")) + { + var reader = Assert.IsAssignableFrom(Assert.IsAssignableFrom(wrappedReader).Reader); + Assert.True(reader.Read()); + recvValue = reader.GetFieldValue(1); + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + Assert.False(reader.Read()); + Assert.False(reader.NextResult()); + } + + // prove that we **cannot** fix ExecuteScalar, because ADO.NET itself doesn't work for that + Assert.Throws(() => + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = "select Value from #Issue1907"; + cmd.CommandType = CommandType.Text; + _ = cmd.ExecuteScalar(); + }); + + // prove that simple read: works + recvValue = connection.QuerySingle("select Value from #Issue1907"); + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + recvValue = connection.QuerySingle("select Value from #Issue1907")!.Value; + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + // prove that object read: works + recvValue = connection.QuerySingle("select Id, Value from #Issue1907").Value; + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + recvValue = connection.QuerySingle("select Id, Value from #Issue1907").Value!.Value; + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + // prove that value-tuple read: works + recvValue = connection.QuerySingle<(int Id, SqlDecimal Value)>("select Id, Value from #Issue1907").Value; + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + + recvValue = connection.QuerySingle<(int Id, SqlDecimal? Value)>("select Id, Value from #Issue1907").Value!.Value; + Assert.Equal(sentValue, recvValue); + Assert.Equal(PreciseValue, recvValue.ToString()); + } + finally + { + if (close) connection.Close(); + } + + } + class HazSqlDecimal + { + public int Id { get; set; } + public SqlDecimal Value { get; set; } + } + + class HazNullableSqlDecimal + { + public int Id { get; set; } + public SqlDecimal? Value { get; set; } + } + + class TypeWithDodgyProperties + { + public string Name => throw new NotSupportedException(); + } } } diff --git a/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs b/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs new file mode 100644 index 000000000..958f0d546 --- /dev/null +++ b/tests/Dapper.Tests/PreferTypeHandlersForEnumsTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Data; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + [Collection(NonParallelDefinition.Name)] + public sealed class SystemSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests { } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] + public sealed class MicrosoftSqlClientPreferTypeHandlersForEnumsTests : PreferTypeHandlersForEnumsTests { } +#endif + + public abstract class PreferTypeHandlersForEnumsTests : TestBase where TProvider : DatabaseProvider + { + private enum Color + { + Red = 1, + Green = 2, + Blue = 3 + } + + private class ColorResult + { + public Color Value { get; set; } + } + + private class NullableColorResult + { + public Color? Value { get; set; } + } + + /// + /// A TypeHandler that stores enum values as their name strings + /// and parses them back from strings. + /// + private class StringEnumHandler : SqlMapper.TypeHandler where TEnum : struct, Enum + { + public static readonly StringEnumHandler Instance = new(); + public int ParseCallCount; + public int SetValueCallCount; + + public override TEnum Parse(object? value) + { + ParseCallCount++; + return (TEnum)Enum.Parse(typeof(TEnum), (string)value!); + } + + public override void SetValue(IDbDataParameter parameter, TEnum value) + { + SetValueCallCount++; + parameter.DbType = DbType.AnsiString; + parameter.Value = value.ToString(); + } + } + + [Fact] + public void EnumTypeHandler_WriteAndRead_UsesHandlerWhenEnabled() + { + var handler = new StringEnumHandler(); + var oldSetting = SqlMapper.Settings.PreferTypeHandlersForEnums; + try + { + SqlMapper.ResetTypeHandlers(); + SqlMapper.AddTypeHandler(typeof(Color), handler); + SqlMapper.Settings.PreferTypeHandlersForEnums = true; + SqlMapper.PurgeQueryCache(); + + // Round-trip: write as string, read back via handler + var result = connection.Query( + "SELECT @Value AS Value", new { Value = Color.Green }).Single(); + + Assert.Equal(Color.Green, result.Value); + Assert.True(handler.SetValueCallCount > 0, "SetValue should have been called"); + Assert.True(handler.ParseCallCount > 0, "Parse should have been called"); + } + finally + { + SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting; + SqlMapper.ResetTypeHandlers(); + SqlMapper.PurgeQueryCache(); + } + } + + [Fact] + public void EnumTypeHandler_NullableWithNull_ReturnsNull() + { + var handler = new StringEnumHandler(); + var oldSetting = SqlMapper.Settings.PreferTypeHandlersForEnums; + try + { + SqlMapper.ResetTypeHandlers(); + SqlMapper.AddTypeHandler(typeof(Color), handler); + SqlMapper.Settings.PreferTypeHandlersForEnums = true; + SqlMapper.PurgeQueryCache(); + + Color? input = null; + var result = connection.Query( + "SELECT @Value AS Value", new { Value = input }).Single(); + + Assert.Null(result.Value); + } + finally + { + SqlMapper.Settings.PreferTypeHandlersForEnums = oldSetting; + SqlMapper.ResetTypeHandlers(); + SqlMapper.PurgeQueryCache(); + } + } + } +} diff --git a/Dapper.Tests/ProcedureTests.cs b/tests/Dapper.Tests/ProcedureTests.cs similarity index 68% rename from Dapper.Tests/ProcedureTests.cs rename to tests/Dapper.Tests/ProcedureTests.cs index 120460205..4c15bd7bb 100644 --- a/Dapper.Tests/ProcedureTests.cs +++ b/tests/Dapper.Tests/ProcedureTests.cs @@ -1,11 +1,18 @@ using System; using System.Data; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Dapper.Tests { - public class ProcedureTests : TestBase + [Collection("ProcedureTests")] + public sealed class SystemSqlClientProcedureTests : ProcedureTests { } +#if MSSQLCLIENT + [Collection("ProcedureTests")] + public sealed class MicrosoftSqlClientProcedureTests : ProcedureTests { } +#endif + public abstract class ProcedureTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestProcWithOutParameter() @@ -85,6 +92,26 @@ @ErrorDescription varchar(255) OUTPUT Assert.Equal("Completed successfully", p.Get("ErrorDescription")); } + [Theory] + [InlineData(CommandType.StoredProcedure)] + [InlineData(null)] // auto + public void InferProcedure(CommandType? commandType) + { + connection.Execute("CREATE PROCEDURE #InferProcedure @id int AS BEGIN SELECT -@id END"); + var result = connection.QuerySingle("#InferProcedure", new { id = 42 }, commandType: commandType); + Assert.Equal(-42, result); + } + + [Theory] + [InlineData(CommandType.Text)] + [InlineData(null)] // auto + public void InferNotProcedure(CommandType? commandType) + { + connection.Execute("CREATE PROCEDURE #InferNotProcedure @id int AS BEGIN SELECT -@id END"); + var result = connection.QuerySingle("EXEC #InferNotProcedure @id", new { id = 42 }, commandType: commandType); + Assert.Equal(-42, result); + } + [Fact] public void SO24605346_ProcsAndStrings() { @@ -99,16 +126,15 @@ @TaxInvoiceNumber nvarchar(20) TaxInvoiceNumber = InvoiceNumber }, commandType: CommandType.StoredProcedure).FirstOrDefault(); + Assert.NotNull(result); Assert.Equal("INV0000000028PPN", result.TaxInvoiceNumber); } private class PracticeRebateOrders { - public string fTaxInvoiceNumber; -#if !NETCOREAPP1_0 + public string? fTaxInvoiceNumber; [System.Xml.Serialization.XmlElement(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] -#endif - public string TaxInvoiceNumber + public string? TaxInvoiceNumber { get { return fTaxInvoiceNumber; } set { fTaxInvoiceNumber = value; } @@ -134,14 +160,14 @@ CREATE PROCEDURE #TestEmptyResults private class Issue327_Person { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } } private class Issue327_Magic { - public string Creature { get; set; } - public string SpiritAnimal { get; set; } - public string Location { get; set; } + public string? Creature { get; set; } + public string? SpiritAnimal { get; set; } + public string? Location { get; set; } } [Fact] @@ -192,7 +218,7 @@ public void TestDateTime2PrecisionPreservedInDynamicParameters() { const string tempSPName = "#" + nameof(TestDateTime2PrecisionPreservedInDynamicParameters); - DateTime datetimeDefault = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + DateTime datetimeDefault = new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); DateTime datetime2 = datetimeDefault.AddTicks(1); // Add 100 ns Assert.True(datetimeDefault < datetime2); @@ -219,14 +245,14 @@ select DATEADD(ns, -100, @b) Assert.Equal(datetime2, p.Get("b")); } - [Theory()] + [Theory] [InlineData(null)] [InlineData(DbType.DateTime)] public void TestDateTime2LosePrecisionInDynamicParameters(DbType? dbType) { const string tempSPName = "#" + nameof(TestDateTime2LosePrecisionInDynamicParameters); - DateTime datetimeDefault = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + DateTime datetimeDefault = new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); DateTime datetime2 = datetimeDefault.AddTicks(1); // Add 100 ns Assert.True(datetimeDefault < datetime2); @@ -254,5 +280,67 @@ select @b // @b gets set to datetime2 value but is truncated back to DbType.DateTime by DynamicParameter's Output declaration Assert.Equal(datetimeDefault, p.Get("b")); } + + + [Fact] + public async Task Issue591_NoResultsAsync() + { + const string tempSPName = "#" + nameof(Issue591_NoResultsAsync); + + var result = await connection.QueryAsync( + $@"create proc {tempSPName} + as + begin + -- basically a failed if statement, so the select is not happening and the stored proc return nothing + if 1=0 + begin + select 1 as Num + end + end + + exec {tempSPName}"); + + Assert.Empty(result); + } + + [Theory] + [InlineData(" ")] + [InlineData("\u00A0")] // nbsp + [InlineData("\u202F")] // narrow nbsp + [InlineData("\u2000")] // n quad + [InlineData("\t")] + [InlineData("\r")] + [InlineData("\n")] + public async Task Issue1986_AutoProc_Whitespace(string space) + { + var sql = "select!42".Replace("!", space); + var result = await connection.QuerySingleAsync(sql); + Assert.Equal(42, result); + } + + [Theory] + [InlineData("foo", CommandType.StoredProcedure)] + [InlineData("foo;", CommandType.Text)] + [InlineData("foo bar", CommandType.Text)] + [InlineData("foo bar;", CommandType.Text)] + [InlineData("vacuum", CommandType.Text)] + [InlineData("vacuum;", CommandType.Text)] + [InlineData("FOO", CommandType.StoredProcedure)] + [InlineData("FOO;", CommandType.Text)] + [InlineData("FOO BAR", CommandType.Text)] + [InlineData("FOO BAR;", CommandType.Text)] + [InlineData("VACUUM", CommandType.Text)] + [InlineData("VACUUM;", CommandType.Text)] + [InlineData("cOmmiT", CommandType.Text)] + [InlineData("rOllbAck", CommandType.Text)] + [InlineData("reVErt", CommandType.Text)] + + // comments imply text + [InlineData("foo--bar", CommandType.Text)] + [InlineData("foo/*bar*/", CommandType.Text)] + public void InferCommandType(string sql, CommandType commandType) + { + Assert.Equal(commandType, CommandDefinition.InferCommandType(sql)); + } } } diff --git a/tests/Dapper.Tests/ProviderTests.cs b/tests/Dapper.Tests/ProviderTests.cs new file mode 100644 index 000000000..c0f2d58f2 --- /dev/null +++ b/tests/Dapper.Tests/ProviderTests.cs @@ -0,0 +1,117 @@ +using System; +using System.Data.Common; +using Dapper.ProviderTools; +using Xunit; + +namespace Dapper.Tests +{ + public class ProviderTests + { + [Fact] + public void BulkCopy_SystemDataSqlClient() + { +#pragma warning disable CS0618 // Type or member is obsolete + using var conn = new System.Data.SqlClient.SqlConnection(); + Test(conn); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void BulkCopy_MicrosoftDataSqlClient() + { + using var conn = new Microsoft.Data.SqlClient.SqlConnection(); + Test(conn); + } + + [Fact] + public void ClientId_SystemDataSqlClient() + => TestClientId(); + + [Fact] + public void ClearPool_SystemDataSqlClient() + => ClearPool(); + + [Fact] + public void ClearAllPools_SystemDataSqlClient() + => ClearAllPools(); + +#if MSSQLCLIENT + [Fact] + public void ClientId_MicrosoftDataSqlClient() + => TestClientId(); + + [Fact] + public void ClearPool_MicrosoftDataSqlClient() + => ClearPool(); + + [Fact] + public void ClearAllPools_MicrosoftDataSqlClient() + => ClearAllPools(); +#endif + + private static void TestClientId() + where T : SqlServerDatabaseProvider, new() + { + var provider = new T(); + using var conn = provider.GetOpenConnection(); + Assert.True(conn.TryGetClientConnectionId(out var id)); + Assert.NotEqual(Guid.Empty, id); + } + + private static void ClearPool() + where T : SqlServerDatabaseProvider, new() + { + var provider = new T(); + using var conn = provider.GetOpenConnection(); + Assert.True(conn.TryClearPool()); + } + + private static void ClearAllPools() + where T : SqlServerDatabaseProvider, new() + { + var provider = new T(); + using var conn = provider.GetOpenConnection(); + Assert.True(conn.TryClearAllPools()); + } + + private static void Test(DbConnection connection) + { + using var bcp = BulkCopy.TryCreate(connection); + Assert.NotNull(bcp); + Assert.IsType(bcp.Wrapped); + bcp.EnableStreaming = true; + } + + [Theory] + [InlineData(51000, 51000, true)] + [InlineData(51000, 43, false)] + public void DbNumber_SystemData(int create, int test, bool result) + => Test(create, test, result); + +#if MSSQLCLIENT + [Theory] + [InlineData(51000, 51000, true)] + [InlineData(51000, 43, false)] + public void DbNumber_MicrosoftData(int create, int test, bool result) + => Test(create, test, result); +#endif + + private static void Test(int create, int test, bool result) + where T : SqlServerDatabaseProvider, new() + { + var provider = new T(); + + using var conn = provider.GetOpenConnection(); + + try + { + conn.Execute("throw @create, 'boom', 1;", new { create }); + Assert.False(true); + } + catch(DbException err) + { + Assert.Equal(result, err.IsNumber(test)); + } + } + } +} diff --git a/tests/Dapper.Tests/Providers/DuckDBTests.cs b/tests/Dapper.Tests/Providers/DuckDBTests.cs new file mode 100644 index 000000000..d06d25d6b --- /dev/null +++ b/tests/Dapper.Tests/Providers/DuckDBTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Data.Common; +using DuckDB.NET.Data; +using Xunit; + +namespace Dapper.Tests +{ + public class DuckDBProvider : DatabaseProvider + { + public override DbProviderFactory Factory => DuckDBClientFactory.Instance; + public override string GetConnectionString() => "Data Source=:memory:"; + } + + public abstract class DuckDBTypeTestBase : TestBase + { + protected DuckDBConnection GetDuckDBConnection(bool open = true) + => (DuckDBConnection)(open ? Provider.GetOpenConnection() : Provider.GetClosedConnection()); + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactDuckDBAttribute : FactAttribute + { + public override string? Skip + { + get { return unavailable ?? base.Skip; } + set { base.Skip = value; } + } + + private static readonly string? unavailable; + + static FactDuckDBAttribute() + { + try + { + using var _ = DatabaseProvider.Instance.GetOpenConnection(); + } + catch (Exception ex) + { + unavailable = $"DuckDB is unavailable: {ex.Message}"; + } + } + } + } + + public class DuckDBTests : DuckDBTypeTestBase + { + [FactDuckDB] + public void DuckDBNamedParameter() + { + using var connection = GetDuckDBConnection(); + + var result = connection.QueryFirst("Select $foo", new {foo = 42}); + Assert.Equal(42, result); + } + + [FactDuckDB] + public void DuckDBPositionalParameter() + { + using var connection = GetDuckDBConnection(); + + var dp = new DynamicParameters(); + dp.Add("?", 42); + + var result = connection.QueryFirst("Select ?", dp); + Assert.Equal(42, result); + } + } +} diff --git a/tests/Dapper.Tests/Providers/EntityFrameworkTests.cs b/tests/Dapper.Tests/Providers/EntityFrameworkTests.cs new file mode 100644 index 000000000..2bd4a77bc --- /dev/null +++ b/tests/Dapper.Tests/Providers/EntityFrameworkTests.cs @@ -0,0 +1,73 @@ +#if ENTITY_FRAMEWORK +using System; +using System.Data.Entity.Spatial; +using System.Linq; +using Xunit; + +namespace Dapper.Tests.Providers +{ + public sealed class SystemSqlClientEntityFrameworkTests : EntityFrameworkTests { } +#if MSSQLCLIENT + public sealed class MicrosoftSqlClientEntityFrameworkTests : EntityFrameworkTests { } +#endif + + [Collection("TypeHandlerTests")] + public abstract class EntityFrameworkTests : TestBase where TProvider : DatabaseProvider + { + public EntityFrameworkTests() + { + EntityFramework.Handlers.Register(); + } + + [Fact] + public void Issue570_DbGeo_HasValues() + { + SkipIfMsDataClient(); + + EntityFramework.Handlers.Register(); + const string redmond = "POINT (-122.1215 47.6740)"; + DbGeography point = DbGeography.PointFromText(redmond, DbGeography.DefaultCoordinateSystemId); + DbGeography orig = point.Buffer(20); + + var fromDb = connection.QuerySingle("declare @geos table(geo geography); insert @geos(geo) values(@val); select * from @geos", + new { val = orig }); + + Assert.NotNull(fromDb.Area); + Assert.Equal(orig.Area, fromDb.Area); + } + + [Fact] + public void Issue22_ExecuteScalar_EntityFramework() + { + SkipIfMsDataClient(); + + var geo = DbGeography.LineFromText("LINESTRING(-122.360 47.656, -122.343 47.656 )", 4326); + var geo2 = connection.ExecuteScalar("select @geo", new { geo }); + Assert.NotNull(geo2); + } + + [Fact] + public void TestGeometryParsingRetainsSrid() + { + const int srid = 27700; + var s = $@"DECLARE @EdinburghPoint GEOMETRY = geometry::STPointFromText('POINT(258647 665289)', {srid}); +SELECT @EdinburghPoint"; + var edinPoint = connection.Query(s).Single(); + Assert.NotNull(edinPoint); + Assert.Equal(srid, edinPoint.CoordinateSystemId); + } + + [Fact] + public void TestGeographyParsingRetainsSrid() + { + const int srid = 4324; + var s = $@"DECLARE @EdinburghPoint GEOGRAPHY = geography::STPointFromText('POINT(-3.19 55.95)', {srid}); +SELECT @EdinburghPoint"; + var edinPoint = connection.Query(s).Single(); + Assert.NotNull(edinPoint); + Assert.Equal(srid, edinPoint.CoordinateSystemId); + } + + } +} +#endif diff --git a/tests/Dapper.Tests/Providers/FirebirdTests.cs b/tests/Dapper.Tests/Providers/FirebirdTests.cs new file mode 100644 index 000000000..d234a8676 --- /dev/null +++ b/tests/Dapper.Tests/Providers/FirebirdTests.cs @@ -0,0 +1,57 @@ +using FirebirdSql.Data.FirebirdClient; +using System.Data; +using System.Data.Common; +using System.Linq; +using Xunit; + +namespace Dapper.Tests.Providers +{ + /// + /// If Docker Desktop is installed, run the following command to start a container suitable for the tests. + /// + /// docker run -d -p 3050:3050 --name Dapper.Tests.Firebird -e FIREBIRD_DATABASE=database -e ISC_PASSWORD=masterkey jacobalberty/firebird + /// + /// + public class FirebirdProvider : DatabaseProvider + { + public override DbProviderFactory Factory => FirebirdClientFactory.Instance; + public override string GetConnectionString() => "initial catalog=localhost:database;user id=SYSDBA;password=masterkey"; + } + public class FirebirdTests : TestBase + { + private FbConnection GetOpenFirebirdConnection() => (FbConnection)Provider.GetOpenConnection(); + + [Fact(Skip = "Bug in Firebird; a PR to fix it has been submitted")] + public void Issue178_Firebird() + { + using var connection = GetOpenFirebirdConnection(); + + const string sql = "select count(*) from Issue178"; + try { connection.Execute("drop table Issue178"); } + catch { /* don't care */ } + connection.Execute("create table Issue178(id int not null)"); + connection.Execute("insert into Issue178(id) values(42)"); + // raw ADO.net + using (var sqlCmd = new FbCommand(sql, connection)) + using (var reader1 = sqlCmd.ExecuteReader()) + { + Assert.True(reader1.Read()); + Assert.Equal(1, reader1.GetInt32(0)); + Assert.False(reader1.Read()); + Assert.False(reader1.NextResult()); + } + + // dapper + using (var reader2 = connection.ExecuteReader(sql)) + { + Assert.True(reader2.Read()); + Assert.Equal(1, reader2.GetInt32(0)); + Assert.False(reader2.Read()); + Assert.False(reader2.NextResult()); + } + + var count = connection.Query(sql).Single(); + Assert.Equal(1, count); + } + } +} diff --git a/Dapper.Tests/Providers/Linq2SqlTests.cs b/tests/Dapper.Tests/Providers/Linq2SqlTests.cs similarity index 76% rename from Dapper.Tests/Providers/Linq2SqlTests.cs rename to tests/Dapper.Tests/Providers/Linq2SqlTests.cs index 4e0691b7c..dfaa2228a 100644 --- a/Dapper.Tests/Providers/Linq2SqlTests.cs +++ b/tests/Dapper.Tests/Providers/Linq2SqlTests.cs @@ -8,7 +8,11 @@ namespace Dapper.Tests { - public class Linq2SqlTests : TestBase + public sealed class SystemSqlClientLinq2SqlTests : Linq2SqlTests { } +#if MSSQLCLIENT + public sealed class MicrosoftSqlClientLinq2SqlTests : Linq2SqlTests { } +#endif + public abstract class Linq2SqlTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestLinqBinaryToClass() @@ -19,7 +23,7 @@ public void TestLinqBinaryToClass() var output = connection.Query("select @input as [Value]", new { input }).First().Value; - Assert.Equal(orig, output.ToArray()); + Assert.Equal(orig, output?.ToArray()); } [Fact] @@ -36,7 +40,7 @@ public void TestLinqBinaryRaw() private class WithBinary { - public System.Data.Linq.Binary Value { get; set; } + public System.Data.Linq.Binary? Value { get; set; } } private class NoDefaultConstructorWithBinary diff --git a/tests/Dapper.Tests/Providers/MySQLTests.cs b/tests/Dapper.Tests/Providers/MySQLTests.cs new file mode 100644 index 000000000..0a6abef69 --- /dev/null +++ b/tests/Dapper.Tests/Providers/MySQLTests.cs @@ -0,0 +1,235 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// If Docker Desktop is installed, run the following command to start a container suitable for the tests. + /// + /// docker run -d -p 3306:3306 --name Dapper.Tests.MySQL -e MYSQL_DATABASE=tests -e MYSQL_USER=test -e MYSQL_PASSWORD=pass -e MYSQL_ROOT_PASSWORD=pass mysql + /// + /// + public sealed class MySqlProvider : DatabaseProvider + { + public override DbProviderFactory Factory => MySqlConnector.MySqlConnectorFactory.Instance; + public override string GetConnectionString() => + GetConnectionString("MySqlConnectionString", "Server=localhost;Database=tests;Uid=test;Pwd=pass;"); + + public DbConnection GetMySqlConnection(bool open = true, + bool convertZeroDatetime = false, bool allowZeroDatetime = false) + { + string cs = GetConnectionString(); + var csb = Factory.CreateConnectionStringBuilder()!; + csb.ConnectionString = cs; + ((dynamic)csb).AllowZeroDateTime = allowZeroDatetime; + ((dynamic)csb).ConvertZeroDateTime = convertZeroDatetime; + var conn = Factory.CreateConnection()!; + conn.ConnectionString = csb.ConnectionString; + if (open) conn.Open(); + return conn; + } + } + public class MySQLTests : TestBase + { + [FactMySql] + public void DapperEnumValue_Mysql() + { + using var conn = Provider.GetMySqlConnection(); + Common.DapperEnumValue(conn); + } + + [FactMySql] + public void Issue552_SignedUnsignedBooleans() + { + using (var conn = Provider.GetMySqlConnection(true, false, false)) + { + conn.Execute(@" +CREATE TEMPORARY TABLE IF NOT EXISTS `bar` ( + `id` INT NOT NULL, + `bool_val` BOOL NULL, + PRIMARY KEY (`id`)); + + truncate table bar; + insert bar (id, bool_val) values (1, null); + insert bar (id, bool_val) values (2, 0); + insert bar (id, bool_val) values (3, 1); + insert bar (id, bool_val) values (4, null); + insert bar (id, bool_val) values (5, 1); + insert bar (id, bool_val) values (6, 0); + insert bar (id, bool_val) values (7, null); + insert bar (id, bool_val) values (8, 1);"); + + var rows = conn.Query("select * from bar;").ToDictionary(x => x.Id); + + Assert.Null(rows[1].Bool_Val); + Assert.False(rows[2].Bool_Val); + Assert.True(rows[3].Bool_Val); + Assert.Null(rows[4].Bool_Val); + Assert.True(rows[5].Bool_Val); + Assert.False(rows[6].Bool_Val); + Assert.Null(rows[7].Bool_Val); + Assert.True(rows[8].Bool_Val); + } + } + + private class MySqlHasBool + { + public int Id { get; set; } + public bool? Bool_Val { get; set; } + } + + [FactMySql] + public void Issue295_NullableDateTime_MySql_Default() + { + using var conn = Provider.GetMySqlConnection(true, false, false); + Common.TestDateTime(conn); + } + + [FactMySql] + public void Issue295_NullableDateTime_MySql_ConvertZeroDatetime() + { + using var conn = Provider.GetMySqlConnection(true, true, false); + Common.TestDateTime(conn); + } + + [FactMySql(Skip = "See https://github.com/DapperLib/Dapper/issues/295, AllowZeroDateTime=True is not supported")] + public void Issue295_NullableDateTime_MySql_AllowZeroDatetime() + { + using (var conn = Provider.GetMySqlConnection(true, false, true)) + { + Common.TestDateTime(conn); + } + } + + [FactMySql(Skip = "See https://github.com/DapperLib/Dapper/issues/295, AllowZeroDateTime=True is not supported")] + public void Issue295_NullableDateTime_MySql_ConvertAllowZeroDatetime() + { + using var conn = Provider.GetMySqlConnection(true, true, true); + Common.TestDateTime(conn); + } + + [FactMySql] + public void Issue426_SO34439033_DateTimeGainsTicks() + { + using var conn = Provider.GetMySqlConnection(true, true, true); + + try { conn.Execute("drop table Issue426_Test"); } catch { /* don't care */ } + try { conn.Execute("create table Issue426_Test (Id int not null, Time time not null)"); } catch { /* don't care */ } + const long ticks = 553440000000; + const int Id = 426; + + var localObj = new Issue426_Test + { + Id = Id, + Time = TimeSpan.FromTicks(ticks) // from code example + }; + conn.Execute("replace into Issue426_Test values (@Id,@Time)", localObj); + + var dbObj = conn.Query("select * from Issue426_Test where Id = @id", new { id = Id }).Single(); + Assert.Equal(Id, dbObj.Id); + Assert.Equal(ticks, dbObj.Time?.Ticks); + } + + [FactMySql] + public void SO36303462_Tinyint_Bools() + { + using var conn = Provider.GetMySqlConnection(true, true, true); + + try { conn.Execute("drop table SO36303462_Test"); } catch { /* don't care */ } + conn.Execute("create table SO36303462_Test (Id int not null, IsBold tinyint not null);"); + conn.Execute("insert SO36303462_Test (Id, IsBold) values (1,1);"); + conn.Execute("insert SO36303462_Test (Id, IsBold) values (2,0);"); + conn.Execute("insert SO36303462_Test (Id, IsBold) values (3,1);"); + + var rows = conn.Query("select * from SO36303462_Test").ToDictionary(x => x.Id); + Assert.Equal(3, rows.Count); + Assert.True(rows[1].IsBold); + Assert.False(rows[2].IsBold); + Assert.True(rows[3].IsBold); + } + + [FactMySql] + public void Issue1277_ReaderSync() + { + using var conn = Provider.GetMySqlConnection(); + + try { conn.Execute("drop table Issue1277_Test"); } catch { /* don't care */ } + conn.Execute("create table Issue1277_Test (Id int not null, IsBold tinyint not null);"); + conn.Execute("insert Issue1277_Test (Id, IsBold) values (1,1);"); + conn.Execute("insert Issue1277_Test (Id, IsBold) values (2,0);"); + conn.Execute("insert Issue1277_Test (Id, IsBold) values (3,1);"); + + using (var reader = conn.ExecuteReader( + "select * from Issue1277_Test where Id < @id", + new { id = 42 })) + { + var table = new DataTable(); + table.Load(reader); + Assert.Equal(2, table.Columns.Count); + Assert.Equal(3, table.Rows.Count); + } + } + + [FactMySql] + public async Task Issue1277_ReaderAsync() + { + using var conn = Provider.GetMySqlConnection(); + + try { await conn.ExecuteAsync("drop table Issue1277_Test"); } catch { /* don't care */ } + await conn.ExecuteAsync("create table Issue1277_Test (Id int not null, IsBold tinyint not null);"); + await conn.ExecuteAsync("insert Issue1277_Test (Id, IsBold) values (1,1);"); + await conn.ExecuteAsync("insert Issue1277_Test (Id, IsBold) values (2,0);"); + await conn.ExecuteAsync("insert Issue1277_Test (Id, IsBold) values (3,1);"); + + using (var reader = await conn.ExecuteReaderAsync( + "select * from Issue1277_Test where Id < @id", + new { id = 42 })) + { + var table = new DataTable(); + table.Load(reader); + Assert.Equal(2, table.Columns.Count); + Assert.Equal(3, table.Rows.Count); + } + } + + private class SO36303462 + { + public int Id { get; set; } + public bool IsBold { get; set; } + } + + public class Issue426_Test + { + public long Id { get; set; } + public TimeSpan? Time { get; set; } + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactMySqlAttribute : FactAttribute + { + public override string? Skip + { + get { return unavailable ?? base.Skip; } + set { base.Skip = value; } + } + + private static readonly string? unavailable; + + static FactMySqlAttribute() + { + try + { + using (DatabaseProvider.Instance.GetMySqlConnection(true)) { /* just trying to see if it works */ } + } + catch (Exception ex) + { + unavailable = $"MySql is unavailable: {ex.Message}"; + } + } + } + } +} diff --git a/tests/Dapper.Tests/Providers/OLDEBTests.cs b/tests/Dapper.Tests/Providers/OLDEBTests.cs new file mode 100644 index 000000000..1fa8da3b9 --- /dev/null +++ b/tests/Dapper.Tests/Providers/OLDEBTests.cs @@ -0,0 +1,264 @@ +#if OLEDB +using System; +using System.Data.Common; +using System.Data.OleDb; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + public class OLEDBProvider : DatabaseProvider + { + public override DbProviderFactory Factory => OleDbFactory.Instance; + public override string GetConnectionString() => + GetConnectionString("OLEDBConnectionString", "Provider=SQLOLEDB;Data Source=.;Initial Catalog=tempdb;Integrated Security=SSPI"); + } + + public class OLDEBTests : TestBase + { + public OleDbConnection GetOleDbConnection() => (OleDbConnection) Provider.GetOpenConnection(); + + // see https://stackoverflow.com/q/18847510/23354 + [Fact] + public void TestOleDbParameters() + { + using var conn = GetOleDbConnection(); + + var row = conn.Query("select Id = ?, Age = ?", + new { foo = 12, bar = 23 } // these names DO NOT MATTER!!! + ).Single(); + int age = row.Age; + int id = row.Id; + Assert.Equal(23, age); + Assert.Equal(12, id); + } + + [Fact] + public void PseudoPositionalParameters_Simple() + { + using var connection = GetOleDbConnection(); + int value = connection.Query("select ?x? + ?y_2? + ?z?", new { x = 1, y_2 = 3, z = 5, z2 = 24 }).Single(); + Assert.Equal(9, value); + } + + [Fact] + public void Issue601_InternationalParameterNamesWork_OleDb() + { + // pseudo-positional + using var connection = GetOleDbConnection(); + int value = connection.QuerySingle("select ?æøå٦?", new { æøå٦ = 42 }); + } + + [Fact] + public void PseudoPositionalParameters_Dynamic() + { + using var connection = GetOleDbConnection(); + var args = new DynamicParameters(); + args.Add("x", 1); + args.Add("y_2", 3); + args.Add("z", 5); + args.Add("z2", 24); + int value = connection.Query("select ?x? + ?y_2? + ?z?", args).Single(); + Assert.Equal(9, value); + } + + [Fact] + public void PseudoPositionalParameters_ReusedParameter() + { + using var connection = GetOleDbConnection(); + var ex = Assert.Throws(() => connection.Query("select ?x? + ?y_2? + ?x?", new { x = 1, y_2 = 3 }).Single()); + Assert.Equal("When passing parameters by position, each parameter can only be referenced once", ex.Message); + } + + [Fact] + public void Issue569_SO38527197_PseudoPositionalParameters_In_And_Other_Condition() + { + const string sql = @"select s1.value as id, s2.value as score + from string_split('1,2,3,4,5',',') s1, string_split('1,2,3,4,5',',') s2 + where s1.value in ?ids? and s2.value = ?score?"; + using var connection = GetOleDbConnection(); + + const int score = 2; + int[] ids = { 1, 2, 5, 7 }; + var list = connection.Query(sql, new { ids, score }).AsList(); + list.Sort(); + Assert.Equal("1,2,5", string.Join(",", list)); + } + + [Fact] + public void Issue569_SO38527197_PseudoPositionalParameters_In() + { + using var connection = GetOleDbConnection(); + int[] ids = { 1, 2, 5, 7 }; + var list = connection.Query("select * from string_split('1,2,3,4,5',',') where value in ?ids?", new { ids }).AsList(); + list.Sort(); + Assert.Equal("1,2,5", string.Join(",", list)); + } + + [Fact] + public void PseudoPositional_CanUseVariable() + { + using var connection = GetOleDbConnection(); + const int id = 42; + var row = connection.QuerySingle("declare @id int = ?id?; select @id as [A], @id as [B];", new { id }); + int a = (int)row.A; + int b = (int)row.B; + Assert.Equal(42, a); + Assert.Equal(42, b); + } + + [Fact] + public void PseudoPositional_CannotUseParameterMultipleTimes() + { + using var connection = GetOleDbConnection(); + var ex = Assert.Throws(() => + { + const int id = 42; + connection.QuerySingle("select ?id? as [A], ?id? as [B];", new { id }); + }); + Assert.Equal("When passing parameters by position, each parameter can only be referenced once", ex.Message); + } + + [Fact] + public void PseudoPositionalParameters_ExecSingle() + { + using var connection = GetOleDbConnection(); + var data = new { x = 6 }; + connection.Execute("create table #named_single(val int not null)"); + int count = connection.Execute("insert #named_single (val) values (?x?)", data); + int? sum = (int?)connection.ExecuteScalar("select sum(val) from #named_single"); + Assert.NotNull(sum); + Assert.Equal(1, count); + Assert.Equal(6, sum); + } + + [Fact] + public void PseudoPositionalParameters_ExecMulti() + { + using var connection = GetOleDbConnection(); + var data = new[] + { + new { x = 1, y = 1 }, + new { x = 3, y = 1 }, + new { x = 6, y = 1 }, + }; + connection.Execute("create table #named_multi(val int not null)"); + int count = connection.Execute("insert #named_multi (val) values (?x?)", data); + int? sum = (int?)connection.ExecuteScalar("select sum(val) from #named_multi"); + Assert.Equal(3, count); + Assert.NotNull(sum); + Assert.Equal(10, sum); + } + + [Fact] + public void Issue457_NullParameterValues() + { + const string sql = @" +DECLARE @since DATETIME, @customerCode nvarchar(10) +SET @since = ? -- ODBC parameter +SET @customerCode = ? -- ODBC parameter + +SELECT @since as [Since], @customerCode as [Code]"; + + using var connection = GetOleDbConnection(); + + DateTime? since = null; // DateTime.Now.Date; + const string? code = null; // "abc"; + var row = connection.QuerySingle(sql, new + { + since, + customerCode = code + }); + var a = (DateTime?)row.Since; + var b = (string)row.Code; + + Assert.Equal(since, a); + Assert.Equal(code, b); + } + + [Fact] + public void Issue457_NullParameterValues_Named() + { + const string sql = @" +DECLARE @since DATETIME, @customerCode nvarchar(10) +SET @since = ?since? -- ODBC parameter +SET @customerCode = ?customerCode? -- ODBC parameter + +SELECT @since as [Since], @customerCode as [Code]"; + + using var connection = GetOleDbConnection(); + + DateTime? since = null; // DateTime.Now.Date; + const string? code = null; // "abc"; + var row = connection.QuerySingle(sql, new + { + since, + customerCode = code + }); + var a = (DateTime?)row.Since; + var b = (string)row.Code; + + Assert.Equal(since, a); + Assert.Equal(code, b); + } + + [Fact] + public async void Issue457_NullParameterValues_MultiAsync() + { + const string sql = @" +DECLARE @since DATETIME, @customerCode nvarchar(10) +SET @since = ? -- ODBC parameter +SET @customerCode = ? -- ODBC parameter + +SELECT @since as [Since], @customerCode as [Code]"; + + using var connection = GetOleDbConnection(); + + DateTime? since = null; // DateTime.Now.Date; + const string? code = null; // "abc"; + using (var multi = await connection.QueryMultipleAsync(sql, new + { + since, + customerCode = code + }).ConfigureAwait(false)) + { + var row = await multi.ReadSingleAsync().ConfigureAwait(false); + var a = (DateTime?)row.Since; + var b = (string?)row.Code; + + Assert.Equal(since, a); + Assert.Equal(code, b); + } + } + + [Fact] + public async void Issue457_NullParameterValues_MultiAsync_Named() + { + const string sql = @" +DECLARE @since DATETIME, @customerCode nvarchar(10) +SET @since = ?since? -- ODBC parameter +SET @customerCode = ?customerCode? -- ODBC parameter + +SELECT @since as [Since], @customerCode as [Code]"; + + using var connection = GetOleDbConnection(); + + DateTime? since = null; // DateTime.Now.Date; + const string? code = null; // "abc"; + using (var multi = await connection.QueryMultipleAsync(sql, new + { + since, + customerCode = code + }).ConfigureAwait(false)) + { + var row = await multi.ReadSingleAsync().ConfigureAwait(false); + var a = (DateTime?)row.Since; + var b = (string)row.Code; + + Assert.Equal(since, a); + Assert.Equal(code, b); + } + } + } +} +#endif diff --git a/tests/Dapper.Tests/Providers/PostgresqlTests.cs b/tests/Dapper.Tests/Providers/PostgresqlTests.cs new file mode 100644 index 000000000..261490a53 --- /dev/null +++ b/tests/Dapper.Tests/Providers/PostgresqlTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + /// + /// If Docker Desktop is installed, run the following command to start a container suitable for the tests. + /// + /// docker run -d -p 5432:5432 --name Dapper.Tests.PostgreSQL -e POSTGRES_DB=dappertest -e POSTGRES_USER=dappertest -e POSTGRES_PASSWORD=dapperpass postgres + /// + /// + public class PostgresProvider : DatabaseProvider + { + public override DbProviderFactory Factory => Npgsql.NpgsqlFactory.Instance; + public override string GetConnectionString() => + GetConnectionString("PostgesConnectionString", "Server=localhost;Port=5432;User Id=dappertest;Password=dapperpass;Database=dappertest"); + } + public class PostgresqlTests : TestBase + { + private Npgsql.NpgsqlConnection GetOpenNpgsqlConnection() => (Npgsql.NpgsqlConnection)Provider.GetOpenConnection(); + + private class Cat + { + public int Id { get; set; } + public string? Breed { get; set; } + public string? Name { get; set; } + } + + private readonly Cat[] Cats = + { + new Cat() { Breed = "Abyssinian", Name="KACTUS"}, + new Cat() { Breed = "Aegean cat", Name="KADAFFI"}, + new Cat() { Breed = "American Bobtail", Name="KANJI"}, + new Cat() { Breed = "Balinese", Name="MACARONI"}, + new Cat() { Breed = "Bombay", Name="MACAULAY"}, + new Cat() { Breed = "Burmese", Name="MACBETH"}, + new Cat() { Breed = "Chartreux", Name="MACGYVER"}, + new Cat() { Breed = "German Rex", Name="MACKENZIE"}, + new Cat() { Breed = "Javanese", Name="MADISON"}, + new Cat() { Breed = "Persian", Name="MAGNA"} + }; + + [FactPostgresql] + public void TestPostgresqlArrayParameters() + { + using var conn = GetOpenNpgsqlConnection(); + + var transaction = conn.BeginTransaction(); + conn.Execute("create table tcat ( id serial not null, breed character varying(20) not null, name character varying (20) not null);"); + conn.Execute("insert into tcat(breed, name) values(:Breed, :Name) ", Cats); + + var r = conn.Query("select * from tcat where id=any(:catids)", new { catids = new[] { 1, 3, 5 } }); + Assert.Equal(3, r.Count()); + Assert.Equal(1, r.Count(c => c.Id == 1)); + Assert.Equal(1, r.Count(c => c.Id == 3)); + Assert.Equal(1, r.Count(c => c.Id == 5)); + transaction.Rollback(); + } + + [FactPostgresql] + public void TestPostgresqlListParameters() + { + using var conn = GetOpenNpgsqlConnection(); + + var transaction = conn.BeginTransaction(); + conn.Execute("create table tcat ( id serial not null, breed character varying(20) not null, name character varying (20) not null);"); + conn.Execute("insert into tcat(breed, name) values(:Breed, :Name) ", new List(Cats)); + + var r = conn.Query("select * from tcat where id=any(:catids)", new { catids = new List { 1, 3, 5 } }); + Assert.Equal(3, r.Count()); + Assert.Equal(1, r.Count(c => c.Id == 1)); + Assert.Equal(1, r.Count(c => c.Id == 3)); + Assert.Equal(1, r.Count(c => c.Id == 5)); + transaction.Rollback(); + } + + private class CharTable + { + public int Id { get; set; } + public char CharColumn { get; set; } + } + + [FactPostgresql] + public void TestPostgresqlChar() + { + using var conn = GetOpenNpgsqlConnection(); + + var transaction = conn.BeginTransaction(); + conn.Execute("create table chartable (id serial not null, charcolumn \"char\" not null);"); + conn.Execute("insert into chartable(charcolumn) values('a');"); + + var r = conn.Query("select * from chartable"); + Assert.Single(r); + Assert.Equal('a', r.Single().CharColumn); + transaction.Rollback(); + } + + [FactPostgresql] + public void TestPostgresqlSelectArray() + { + using var conn = GetOpenNpgsqlConnection(); + + var r = conn.Query("select array[1,2,3]").ToList(); + Assert.Single(r); + Assert.Equal(new[] { 1, 2, 3 }, r.Single()); + } + + [FactPostgresql] + public void TestPostgresqlDateTimeUsage() + { + using var conn = GetOpenNpgsqlConnection(); + + DateTime now = DateTime.UtcNow; + DateTime? nilA = now, nilB = null; + _ = conn.ExecuteScalar("SELECT @now, @nilA, @nilB::timestamp", new { now, nilA, nilB }); + } + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactPostgresqlAttribute : FactAttribute + { + public override string? Skip + { + get { return unavailable ?? base.Skip; } + set { base.Skip = value; } + } + + private static readonly string? unavailable; + + static FactPostgresqlAttribute() + { + try + { + using (DatabaseProvider.Instance.GetOpenConnection()) { /* just trying to see if it works */ } + } + catch (Exception ex) + { + unavailable = $"Postgresql is unavailable: {ex.Message}"; + } + } + } + } +} diff --git a/tests/Dapper.Tests/Providers/SnowflakeTests.cs b/tests/Dapper.Tests/Providers/SnowflakeTests.cs new file mode 100644 index 000000000..610592461 --- /dev/null +++ b/tests/Dapper.Tests/Providers/SnowflakeTests.cs @@ -0,0 +1,86 @@ +#if !NETFRAMEWORK // platform not supported exception +using System; +using System.Collections.Generic; +using System.IO; +using Snowflake.Data.Client; +using Xunit; +using Xunit.Abstractions; + +namespace Dapper.Tests +{ + public class SnowflakeTests + { + static readonly string? s_ConnectionString; + static SnowflakeTests() + { + SqlMapper.Settings.UseIncrementalPseudoPositionalParameterNames = true; + + try + { // this *probably* won't exist (TODO: can we get a test account?) + s_ConnectionString = File.ReadAllText(@"c:\Code\SnowflakeConnectionString.txt").Trim(); + } catch { } + } + + public SnowflakeTests(ITestOutputHelper output) + => Output = output; + + private ITestOutputHelper Output { get; } + + + private static SnowflakeDbConnection GetConnection() + { + if (string.IsNullOrWhiteSpace(s_ConnectionString)) + Skip.Inconclusive("no snowflake connection-string"); + + return new SnowflakeDbConnection + { + ConnectionString = s_ConnectionString + }; + } + + [Fact] + public void Connect() + { + using var connection = GetConnection(); + connection.Open(); + } + + + [Fact] + public void BasicQuery() + { + + using var connection = GetConnection(); + var nations = connection.Query(@"SELECT * FROM NATION").AsList(); + Assert.NotEmpty(nations); + Output.WriteLine($"nations: {nations.Count}"); + foreach (var nation in nations) + { + Output.WriteLine($"{nation.N_NATIONKEY}: {nation.N_NAME} (region: {nation.N_REGIONKEY}), {nation.N_COMMENT}"); + } + } + + [Fact] + public void ParameterizedQuery() + { + using var connection = GetConnection(); + const int region = 1; + var nations = connection.Query(@"SELECT * FROM NATION WHERE N_REGIONKEY=?region?", new { region }).AsList(); + Assert.NotEmpty(nations); + Output.WriteLine($"nations: {nations.Count}"); + foreach (var nation in nations) + { + Output.WriteLine($"{nation.N_NATIONKEY}: {nation.N_NAME} (region: {nation.N_REGIONKEY}), {nation.N_COMMENT}"); + } + } + + public class Nation + { + public int N_NATIONKEY { get; set; } + public string? N_NAME{ get; set; } + public int N_REGIONKEY { get; set; } + public string? N_COMMENT { get; set; } + } + } +} +#endif diff --git a/tests/Dapper.Tests/Providers/SqliteTests.cs b/tests/Dapper.Tests/Providers/SqliteTests.cs new file mode 100644 index 000000000..b0c5e0f1b --- /dev/null +++ b/tests/Dapper.Tests/Providers/SqliteTests.cs @@ -0,0 +1,162 @@ +using Microsoft.Data.Sqlite; +using System; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Dapper.Tests +{ + public class SqliteProvider : DatabaseProvider + { + public override DbProviderFactory Factory => SqliteFactory.Instance; + public override string GetConnectionString() => "Data Source=:memory:"; + } + + public abstract class SqliteTypeTestBase : TestBase + { + protected SqliteConnection GetSQLiteConnection(bool open = true) + => (SqliteConnection)(open ? Provider.GetOpenConnection() : Provider.GetClosedConnection()); + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class FactSqliteAttribute : FactAttribute + { + public override string? Skip + { + get { return unavailable ?? base.Skip; } + set { base.Skip = value; } + } + + private static readonly string? unavailable; + + static FactSqliteAttribute() + { + try + { + using var _ = DatabaseProvider.Instance.GetOpenConnection(); + } + catch (Exception ex) + { + unavailable = $"Sqlite is unavailable: {ex.Message}"; + } + } + } + } + + [Collection(NonParallelDefinition.Name)] + public class SqliteTypeHandlerTests : SqliteTypeTestBase + { + [FactSqlite] + public void Issue466_SqliteHatesOptimizations() + { + using var connection = GetSQLiteConnection(); + + SqlMapper.ResetTypeHandlers(); + var row = connection.Query("select 42 as Id").First(); + Assert.Equal(42, row.Id); + row = connection.Query("select 42 as Id").First(); + Assert.Equal(42, row.Id); + + SqlMapper.ResetTypeHandlers(); + row = connection.QueryFirst("select 42 as Id"); + Assert.Equal(42, row.Id); + row = connection.QueryFirst("select 42 as Id"); + Assert.Equal(42, row.Id); + } + + [FactSqlite] + public async Task Issue466_SqliteHatesOptimizations_Async() + { + using var connection = GetSQLiteConnection(); + + SqlMapper.ResetTypeHandlers(); + var row = (await connection.QueryAsync("select 42 as Id").ConfigureAwait(false)).First(); + Assert.Equal(42, row.Id); + row = (await connection.QueryAsync("select 42 as Id").ConfigureAwait(false)).First(); + Assert.Equal(42, row.Id); + + SqlMapper.ResetTypeHandlers(); + row = await connection.QueryFirstAsync("select 42 as Id").ConfigureAwait(false); + Assert.Equal(42, row.Id); + row = await connection.QueryFirstAsync("select 42 as Id").ConfigureAwait(false); + Assert.Equal(42, row.Id); + } + } + + public class SqliteTests : SqliteTypeTestBase + { + [FactSqlite] + public void DapperEnumValue_Sqlite() + { + using var connection = GetSQLiteConnection(); + Common.DapperEnumValue(connection); + } + + + + [FactSqlite] + public void Isse467_SqliteLikesParametersWithPrefix() + { + Isse467_SqliteParameterNaming(true); + } + + [FactSqlite] + public void Isse467_SqliteLikesParametersWithoutPrefix() + { // see issue 375 / 467; note: fixed from RC2 onwards + Isse467_SqliteParameterNaming(false); + } + + private void Isse467_SqliteParameterNaming(bool prefix) + { + using var connection = GetSQLiteConnection(); + + var cmd = connection.CreateCommand(); + cmd.CommandText = "select @foo"; + const SqliteType type = SqliteType.Integer; + cmd.Parameters.Add(prefix ? "@foo" : "foo", type).Value = 42; + var i = Convert.ToInt32(cmd.ExecuteScalar()); + Assert.Equal(42, i); + } + + [FactSqlite] + public void DateTimeIsParsedWithInvariantCulture() + { + connection.Execute("CREATE TABLE [PersonWithDob] ([Id] integer primary key autoincrement, [DoB] DATETIME not null )"); + + var localMorning = DateTime.Parse("2019-07-31 01:00:00"); + + var culture = Thread.CurrentThread.CurrentCulture; + + try + { + connection.Execute("INSERT INTO [PersonWithDob] ([DoB]) VALUES (@DoB)", new PersonWithDob { DoB = localMorning }); + + // Before we read the column, use Farsi this is a way to ensure the + // InvariantCulture is used as otherwise it would fail because Farsi + // is not able to parse a DateTime that is formatted with Invariant + + var farsi = System.Globalization.CultureInfo.GetCultureInfo("fa-IR"); + Thread.CurrentThread.CurrentCulture = farsi; + Thread.CurrentThread.CurrentUICulture = farsi; + + var person = connection.QueryFirst("SELECT * FROM [PersonWithDob]"); + + Assert.Equal(localMorning, person.DoB); + } + finally + { + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + + connection.Execute("DROP TABLE [PersonWithDob]"); + } + } + + private class PersonWithDob + { + public int Id { get; set; } + public DateTime DoB { get; set; } + } + } +} diff --git a/Dapper.Tests/QueryMultipleTests.cs b/tests/Dapper.Tests/QueryMultipleTests.cs similarity index 50% rename from Dapper.Tests/QueryMultipleTests.cs rename to tests/Dapper.Tests/QueryMultipleTests.cs index a087c1a78..8e3015649 100644 --- a/Dapper.Tests/QueryMultipleTests.cs +++ b/tests/Dapper.Tests/QueryMultipleTests.cs @@ -6,58 +6,69 @@ namespace Dapper.Tests { - public class QueryMultipleTests : TestBase + [Collection("QueryMultipleTests")] + public sealed class SystemSqlClientQueryMultipleTests : QueryMultipleTests { } +#if MSSQLCLIENT + [Collection("QueryMultipleTests")] + public sealed class MicrosoftSqlClientQueryMultipleTests : QueryMultipleTests { } +#endif + public abstract class QueryMultipleTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestQueryMultipleBuffered() { - using (var grid = connection.QueryMultiple("select 1; select 2; select @x; select 4", new { x = 3 })) - { - var a = grid.Read(); - var b = grid.Read(); - var c = grid.Read(); - var d = grid.Read(); - - a.Single().Equals(1); - b.Single().Equals(2); - c.Single().Equals(3); - d.Single().Equals(4); - } + using var grid = connection.QueryMultiple("select 1; select 2; select @x; select 4", new { x = 3 }); + + var a = grid.Read(); + var b = grid.Read(); + var c = grid.Read(); + var d = grid.Read(); + + Assert.Equal(1, a.Single()); + Assert.Equal(2, b.Single()); + Assert.Equal(3, c.Single()); + Assert.Equal(4, d.Single()); + } + + [Fact] + public void TestMultiConversion() + { + using SqlMapper.GridReader multi = connection.QueryMultiple("select Cast(1 as BigInt) Col1; select Cast(2 as BigInt) Col2"); + Assert.Equal(1, multi.Read().Single()); + Assert.Equal(2, multi.Read().Single()); } [Fact] public void TestQueryMultipleNonBufferedIncorrectOrder() { - using (var grid = connection.QueryMultiple("select 1; select 2; select @x; select 4", new { x = 3 })) + using var grid = connection.QueryMultiple("select 1; select 2; select @x; select 4", new { x = 3 }); + + var a = grid.Read(false); + try + { + var b = grid.Read(false); + throw new InvalidOperationException(); // should have thrown + } + catch (InvalidOperationException) { - var a = grid.Read(false); - try - { - var b = grid.Read(false); - throw new InvalidOperationException(); // should have thrown - } - catch (InvalidOperationException) - { - // that's expected - } + // that's expected } } [Fact] public void TestQueryMultipleNonBufferedCorrectOrder() { - using (var grid = connection.QueryMultiple("select 1; select 2; select @x; select 4", new { x = 3 })) - { - var a = grid.Read(false).Single(); - var b = grid.Read(false).Single(); - var c = grid.Read(false).Single(); - var d = grid.Read(false).Single(); - - a.Equals(1); - b.Equals(2); - c.Equals(3); - d.Equals(4); - } + using var grid = connection.QueryMultiple("select 1; select 2; select @x; select 4", new { x = 3 }); + + var a = grid.Read(false).Single(); + var b = grid.Read(false).Single(); + var c = grid.Read(false).Single(); + var d = grid.Read(false).Single(); + + Assert.Equal(1, a); + Assert.Equal(2, b); + Assert.Equal(3, c); + Assert.Equal(4, d); } [Fact] @@ -145,68 +156,62 @@ public void Issue524_QueryMultiple_Cast() Assert.Equal(42, connection.QuerySingle("select cast(42 as bigint)")); // using multi-reader API - using (var reader = connection.QueryMultiple("select cast(42 as bigint); select cast(42 as bigint)")) - { - Assert.Equal(42, reader.Read().Single()); - Assert.Equal(42, reader.ReadSingle()); - } + using var reader = connection.QueryMultiple("select cast(42 as bigint); select cast(42 as bigint)"); + + Assert.Equal(42, reader.Read().Single()); + Assert.Equal(42, reader.ReadSingle()); } [Fact] public void QueryMultipleFromClosed() { - using (var conn = GetClosedConnection()) + using var conn = GetClosedConnection(); + using (var multi = conn.QueryMultiple("select 1; select 'abc';")) { - using (var multi = conn.QueryMultiple("select 1; select 'abc';")) - { - Assert.Equal(1, multi.Read().Single()); - Assert.Equal("abc", multi.Read().Single()); - } - Assert.Equal(ConnectionState.Closed, conn.State); + Assert.Equal(1, multi.Read().Single()); + Assert.Equal("abc", multi.Read().Single()); } + Assert.Equal(ConnectionState.Closed, conn.State); } [Fact] public void QueryMultiple2FromClosed() { - using (var conn = GetClosedConnection()) + using var conn = GetClosedConnection(); + + Assert.Equal(ConnectionState.Closed, conn.State); + + using (var multi = conn.QueryMultiple("select 1 select 2 select 3")) { - Assert.Equal(ConnectionState.Closed, conn.State); - using (var multi = conn.QueryMultiple("select 1 select 2 select 3")) - { - Assert.Equal(1, multi.Read().Single()); - Assert.Equal(2, multi.Read().Single()); - // not reading 3 is intentional here - } - Assert.Equal(ConnectionState.Closed, conn.State); + Assert.Equal(1, multi.Read().Single()); + Assert.Equal(2, multi.Read().Single()); + // not reading 3 is intentional here } + Assert.Equal(ConnectionState.Closed, conn.State); } [Fact] public void SO35554284_QueryMultipleUntilConsumed() { - using (var reader = connection.QueryMultiple("select 1 as Id; select 2 as Id; select 3 as Id;")) + using var reader = connection.QueryMultiple("select 1 as Id; select 2 as Id; select 3 as Id;"); + + var items = new List(); + while (!reader.IsConsumed) { - var items = new List(); - while (!reader.IsConsumed) - { - items.AddRange(reader.Read()); - } - Assert.Equal(3, items.Count); - Assert.Equal(1, items[0].Id); - Assert.Equal(2, items[1].Id); - Assert.Equal(3, items[2].Id); + items.AddRange(reader.Read()); } + Assert.Equal(3, items.Count); + Assert.Equal(1, items[0].Id); + Assert.Equal(2, items[1].Id); + Assert.Equal(3, items[2].Id); } [Fact] public void QueryMultipleInvalidFromClosed() { - using (var conn = GetClosedConnection()) - { - Assert.ThrowsAny(() => conn.QueryMultiple("select gibberish")); - Assert.Equal(ConnectionState.Closed, conn.State); - } + using var conn = GetClosedConnection(); + Assert.ThrowsAny(() => conn.QueryMultiple("select gibberish")); + Assert.Equal(ConnectionState.Closed, conn.State); } [Fact] @@ -217,29 +222,28 @@ public void QueryMultipleInvalidFromClosed() private void TestMultiSelectWithSomeEmptyGrids(bool buffered) { - using (var reader = connection.QueryMultiple("select 1; select 2 where 1 = 0; select 3 where 1 = 0; select 4;")) - { - var one = reader.Read(buffered: buffered).ToArray(); - var two = reader.Read(buffered: buffered).ToArray(); - var three = reader.Read(buffered: buffered).ToArray(); - var four = reader.Read(buffered: buffered).ToArray(); - try - { // only returned four grids; expect a fifth read to fail - reader.Read(buffered: buffered); - throw new InvalidOperationException("this should not have worked!"); - } - catch (ObjectDisposedException ex) - { // expected; success - Assert.Equal("The reader has been disposed; this can happen after all data has been consumed\r\nObject name: 'Dapper.SqlMapper+GridReader'.", ex.Message); - } - - Assert.Single(one); - Assert.Equal(1, one[0]); - Assert.Empty(two); - Assert.Empty(three); - Assert.Single(four); - Assert.Equal(4, four[0]); + using var reader = connection.QueryMultiple("select 1; select 2 where 1 = 0; select 3 where 1 = 0; select 4;"); + + var one = reader.Read(buffered: buffered).ToArray(); + var two = reader.Read(buffered: buffered).ToArray(); + var three = reader.Read(buffered: buffered).ToArray(); + var four = reader.Read(buffered: buffered).ToArray(); + try + { // only returned four grids; expect a fifth read to fail + reader.Read(buffered: buffered); + throw new InvalidOperationException("this should not have worked!"); + } + catch (ObjectDisposedException ex) + { // expected; success + Assert.Equal("The reader has been disposed; this can happen after all data has been consumed\r\nObject name: 'Dapper.SqlMapper+GridReader'.", ex.Message, ignoreLineEndingDifferences: true); } + + Assert.Single(one); + Assert.Equal(1, one[0]); + Assert.Empty(two); + Assert.Empty(three); + Assert.Single(four); + Assert.Equal(4, four[0]); } [Fact] diff --git a/tests/Dapper.Tests/SharedTypes/Address.cs b/tests/Dapper.Tests/SharedTypes/Address.cs new file mode 100644 index 000000000..f679f3c95 --- /dev/null +++ b/tests/Dapper.Tests/SharedTypes/Address.cs @@ -0,0 +1,10 @@ +namespace Dapper.Tests +{ + public class Address + { + public int AddressId { get; set; } + public string? Name { get; set; } + public int PersonId { get; set; } + public Index? Index { get; set; } + } +} diff --git a/Dapper.Tests/SharedTypes/Bar1.cs b/tests/Dapper.Tests/SharedTypes/Bar1.cs similarity index 64% rename from Dapper.Tests/SharedTypes/Bar1.cs rename to tests/Dapper.Tests/SharedTypes/Bar1.cs index 306d6b96c..6628b2ca7 100644 --- a/Dapper.Tests/SharedTypes/Bar1.cs +++ b/tests/Dapper.Tests/SharedTypes/Bar1.cs @@ -3,6 +3,6 @@ public class Bar1 { public int BarId; - public string Name { get; set; } + public string? Name { get; set; } } } diff --git a/Dapper.Tests/SharedTypes/Category.cs b/tests/Dapper.Tests/SharedTypes/Category.cs similarity index 50% rename from Dapper.Tests/SharedTypes/Category.cs rename to tests/Dapper.Tests/SharedTypes/Category.cs index e51ef2008..33350f3d2 100644 --- a/Dapper.Tests/SharedTypes/Category.cs +++ b/tests/Dapper.Tests/SharedTypes/Category.cs @@ -3,7 +3,7 @@ public class Category { public int Id { get; set; } - public string Name { get; set; } - public string Description { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } } } diff --git a/Dapper.Tests/SharedTypes/Comment.cs b/tests/Dapper.Tests/SharedTypes/Comment.cs similarity index 64% rename from Dapper.Tests/SharedTypes/Comment.cs rename to tests/Dapper.Tests/SharedTypes/Comment.cs index 568b0a01d..bb2833000 100644 --- a/Dapper.Tests/SharedTypes/Comment.cs +++ b/tests/Dapper.Tests/SharedTypes/Comment.cs @@ -3,6 +3,6 @@ public class Comment { public int Id { get; set; } - public string CommentData { get; set; } + public string? CommentData { get; set; } } } diff --git a/Dapper.Tests/SharedTypes/Dog.cs b/tests/Dapper.Tests/SharedTypes/Dog.cs similarity index 80% rename from Dapper.Tests/SharedTypes/Dog.cs rename to tests/Dapper.Tests/SharedTypes/Dog.cs index 9a5212922..a5668ecfb 100644 --- a/Dapper.Tests/SharedTypes/Dog.cs +++ b/tests/Dapper.Tests/SharedTypes/Dog.cs @@ -6,7 +6,7 @@ public class Dog { public int? Age { get; set; } public Guid Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public float? Weight { get; set; } public int IgnoredProperty => 1; diff --git a/Dapper.Tests/SharedTypes/Enums.cs b/tests/Dapper.Tests/SharedTypes/Enums.cs similarity index 100% rename from Dapper.Tests/SharedTypes/Enums.cs rename to tests/Dapper.Tests/SharedTypes/Enums.cs diff --git a/Dapper.Tests/SharedTypes/Foo1.cs b/tests/Dapper.Tests/SharedTypes/Foo1.cs similarity index 100% rename from Dapper.Tests/SharedTypes/Foo1.cs rename to tests/Dapper.Tests/SharedTypes/Foo1.cs diff --git a/Dapper.Tests/SharedTypes/HazNameId.cs b/tests/Dapper.Tests/SharedTypes/HazNameId.cs similarity index 66% rename from Dapper.Tests/SharedTypes/HazNameId.cs rename to tests/Dapper.Tests/SharedTypes/HazNameId.cs index 7bc44a5a2..7fd2a1080 100644 --- a/Dapper.Tests/SharedTypes/HazNameId.cs +++ b/tests/Dapper.Tests/SharedTypes/HazNameId.cs @@ -2,7 +2,7 @@ { public class HazNameId { - public string Name { get; set; } + public string? Name { get; set; } public int Id { get; set; } } -} \ No newline at end of file +} diff --git a/tests/Dapper.Tests/SharedTypes/Index.cs b/tests/Dapper.Tests/SharedTypes/Index.cs new file mode 100644 index 000000000..7769cec19 --- /dev/null +++ b/tests/Dapper.Tests/SharedTypes/Index.cs @@ -0,0 +1,7 @@ +namespace Dapper.Tests +{ + public class Index + { + public string? Id { get; set; } + } +} diff --git a/tests/Dapper.Tests/SharedTypes/Person.cs b/tests/Dapper.Tests/SharedTypes/Person.cs new file mode 100644 index 000000000..34e3dcdc6 --- /dev/null +++ b/tests/Dapper.Tests/SharedTypes/Person.cs @@ -0,0 +1,11 @@ +namespace Dapper.Tests +{ + public class Person + { + public int PersonId { get; set; } + public string? Name { get; set; } + public string? Occupation { get; private set; } + public int NumberOfLegs = 2; + public Address? Address { get; set; } + } +} diff --git a/tests/Dapper.Tests/SharedTypes/Post.cs b/tests/Dapper.Tests/SharedTypes/Post.cs new file mode 100644 index 000000000..118133e4f --- /dev/null +++ b/tests/Dapper.Tests/SharedTypes/Post.cs @@ -0,0 +1,10 @@ +namespace Dapper.Tests +{ + public class Post + { + public int Id { get; set; } + public User? Owner { get; set; } + public string? Content { get; set; } + public Comment? Comment { get; set; } + } +} diff --git a/Dapper.Tests/SharedTypes/Product.cs b/tests/Dapper.Tests/SharedTypes/Product.cs similarity index 50% rename from Dapper.Tests/SharedTypes/Product.cs rename to tests/Dapper.Tests/SharedTypes/Product.cs index 0e07da3a4..be0848398 100644 --- a/Dapper.Tests/SharedTypes/Product.cs +++ b/tests/Dapper.Tests/SharedTypes/Product.cs @@ -3,7 +3,7 @@ public class Product { public int Id { get; set; } - public string Name { get; set; } - public Category Category { get; set; } + public string? Name { get; set; } + public Category? Category { get; set; } } } diff --git a/tests/Dapper.Tests/SharedTypes/ReviewBoard.cs b/tests/Dapper.Tests/SharedTypes/ReviewBoard.cs new file mode 100644 index 000000000..8a44006b3 --- /dev/null +++ b/tests/Dapper.Tests/SharedTypes/ReviewBoard.cs @@ -0,0 +1,17 @@ +namespace Dapper.Tests +{ + public class ReviewBoard + { + public int Id { get; set; } + public string? Name { get; set; } + public User? User1 { get; set; } + public User? User2 { get; set; } + public User? User3 { get; set; } + public User? User4 { get; set; } + public User? User5 { get; set; } + public User? User6 { get; set; } + public User? User7 { get; set; } + public User? User8 { get; set; } + public User? User9 { get; set; } + } +} diff --git a/Dapper.Tests/SharedTypes/ShortEnum.cs b/tests/Dapper.Tests/SharedTypes/ShortEnum.cs similarity index 100% rename from Dapper.Tests/SharedTypes/ShortEnum.cs rename to tests/Dapper.Tests/SharedTypes/ShortEnum.cs diff --git a/Dapper.Tests/SharedTypes/SomeType.cs b/tests/Dapper.Tests/SharedTypes/SomeType.cs similarity index 68% rename from Dapper.Tests/SharedTypes/SomeType.cs rename to tests/Dapper.Tests/SharedTypes/SomeType.cs index b86f8371b..5be1bbf46 100644 --- a/Dapper.Tests/SharedTypes/SomeType.cs +++ b/tests/Dapper.Tests/SharedTypes/SomeType.cs @@ -3,6 +3,6 @@ public class SomeType { public int A { get; set; } - public string B { get; set; } + public string? B { get; set; } } } diff --git a/Dapper.Tests/SharedTypes/User.cs b/tests/Dapper.Tests/SharedTypes/User.cs similarity index 66% rename from Dapper.Tests/SharedTypes/User.cs rename to tests/Dapper.Tests/SharedTypes/User.cs index 84d8d8524..6b5728f48 100644 --- a/Dapper.Tests/SharedTypes/User.cs +++ b/tests/Dapper.Tests/SharedTypes/User.cs @@ -3,6 +3,6 @@ public class User { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } } } diff --git a/tests/Dapper.Tests/SingleRowTests.cs b/tests/Dapper.Tests/SingleRowTests.cs new file mode 100644 index 000000000..ab554514f --- /dev/null +++ b/tests/Dapper.Tests/SingleRowTests.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FastMember; +using Xunit; +using Xunit.Abstractions; +using static Dapper.SqlMapper; + +namespace Dapper.Tests; + +[Collection("SingleRowTests")] +public sealed class SystemSqlClientSingleRowTests(ITestOutputHelper log) : SingleRowTests(log) +{ + protected override async Task InjectDataAsync(DbConnection conn, DbDataReader source) + { +#pragma warning disable CS0618 // Type or member is obsolete + using var bcp = new System.Data.SqlClient.SqlBulkCopy((System.Data.SqlClient.SqlConnection)conn); +#pragma warning restore CS0618 // Type or member is obsolete + bcp.DestinationTableName = "#mydata"; + bcp.EnableStreaming = true; + await bcp.WriteToServerAsync(source); + } +} +#if MSSQLCLIENT +[Collection("SingleRowTests")] +public sealed class MicrosoftSqlClientSingleRowTests(ITestOutputHelper log) : SingleRowTests(log) +{ + protected override async Task InjectDataAsync(DbConnection conn, DbDataReader source) + { + using var bcp = new Microsoft.Data.SqlClient.SqlBulkCopy((Microsoft.Data.SqlClient.SqlConnection)conn); + bcp.DestinationTableName = "#mydata"; + bcp.EnableStreaming = true; + await bcp.WriteToServerAsync(source); + } +} +#endif +public abstract class SingleRowTests(ITestOutputHelper log) : TestBase where TProvider : DatabaseProvider +{ + protected abstract Task InjectDataAsync(DbConnection connection, DbDataReader source); + + [Fact] + public async Task QueryFirst_PerformanceAndCorrectness() + { + using var conn = GetOpenConnection(); + conn.Execute("create table #mydata(id int not null, name nvarchar(250) not null)"); + + var rand = new Random(); + var data = from id in Enumerable.Range(1, 500_000) + select new MyRow { Id = rand.Next(), Name = CreateName(rand) }; + + Stopwatch watch; + using (var reader = ObjectReader.Create(data)) + { + await InjectDataAsync(conn, reader); + watch = Stopwatch.StartNew(); + var count = await conn.QuerySingleAsync("""select count(1) from #mydata"""); + watch.Stop(); + log.WriteLine($"bulk-insert complete; {count} rows in {watch.ElapsedMilliseconds}ms"); + } + + // just errors + var ex = Assert.ThrowsAny(() => conn.Execute("raiserror('bad things', 16, 1)")); + log.WriteLine(ex.Message); + ex = await Assert.ThrowsAnyAsync(async () => await conn.ExecuteAsync("raiserror('bad things', 16, 1)")); + log.WriteLine(ex.Message); + + // just data + watch = Stopwatch.StartNew(); + var row = conn.QueryFirst("select top 1 * from #mydata"); + watch.Stop(); + log.WriteLine($"sync top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = await conn.QueryFirstAsync("select top 1 * from #mydata"); + watch.Stop(); + log.WriteLine($"async top 1 read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = conn.QueryFirst("select * from #mydata"); + watch.Stop(); + log.WriteLine($"sync read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = await conn.QueryFirstAsync("select * from #mydata"); + watch.Stop(); + log.WriteLine($"async read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + // data with trailing errors + + watch = Stopwatch.StartNew(); + ex = Assert.ThrowsAny(() => conn.QueryFirst("select * from #mydata; raiserror('bad things', 16, 1)")); + watch.Stop(); + log.WriteLine($"sync read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}"); + + watch = Stopwatch.StartNew(); + ex = await Assert.ThrowsAnyAsync(async () => await conn.QueryFirstAsync("select * from #mydata; raiserror('bad things', 16, 1)")); + watch.Stop(); + log.WriteLine($"async read with error complete in {watch.ElapsedMilliseconds}ms; {ex.Message}"); + + // unbuffered read with trailing errors - do not expect to see this unless we consume all! + + watch = Stopwatch.StartNew(); + row = conn.Query("select * from #mydata", buffered: false).First(); + watch.Stop(); + log.WriteLine($"sync unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + watch = Stopwatch.StartNew(); + row = await conn.QueryUnbufferedAsync("select * from #mydata").FirstAsync(); + watch.Stop(); + log.WriteLine($"async unbuffered LINQ read first complete; row {row.Id} in {watch.ElapsedMilliseconds}ms"); + + static unsafe string CreateName(Random rand) + { + const string Alphabet = "abcdefghijklmnopqrstuvwxyz 0123456789,;-"; + var len = rand.Next(5, 251); + char* ptr = stackalloc char[len]; + for (int i = 0; i < len; i++) + { + ptr[i] = Alphabet[rand.Next(Alphabet.Length)]; + } + return new string(ptr, 0, len); + } + + } + + public class MyRow + { + public int Id { get; set; } + public string Name { get; set; } = ""; + } +} + +internal static class AsyncLinqHelper +{ + public static async ValueTask FirstAsync(this IAsyncEnumerable source, CancellationToken cancellationToken = default) + { + await using var iter = source.GetAsyncEnumerator(cancellationToken); + if (!await iter.MoveNextAsync()) Array.Empty().First(); // for consistent error + return iter.Current; + } +} diff --git a/tests/Dapper.Tests/SqlBuilderTests.cs b/tests/Dapper.Tests/SqlBuilderTests.cs new file mode 100644 index 000000000..899a81cea --- /dev/null +++ b/tests/Dapper.Tests/SqlBuilderTests.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using Xunit; + +namespace Dapper.Tests +{ + [Collection("SqlBuilderTests")] + public sealed class SystemSqlClientSqlBuilderTests : SqlBuilderTests { } +#if MSSQLCLIENT + [Collection("SqlBuilderTests")] + public sealed class MicrosoftSqlClientSqlBuilderTests : SqlBuilderTests { } +#endif + public abstract class SqlBuilderTests : TestBase where TProvider : DatabaseProvider + { + [Fact] + public void TestSqlBuilderWithDapperQuery() + { + var sb = new SqlBuilder(); + var template = sb.AddTemplate("SELECT /**select**/ FROM #Users /**where**/"); + sb.Where("Age <= @Age", new { Age = 18 }) + .Where("Country = @Country", new { Country = "USA" }) + .Select("Name,Age,Country"); + + const string createSql = @" + create table #Users (Name varchar(20),Age int,Country nvarchar(5)); + insert #Users values('Sam',16,'USA'),('Tom',25,'UK'),('Henry',14,'UK')"; + try + { + connection.Execute(createSql); + + var result = connection.Query(template.RawSql,template.Parameters).ToArray(); + + Assert.Equal("SELECT Name,Age,Country\n FROM #Users WHERE Age <= @Age AND Country = @Country\n", template.RawSql); + + Assert.Single(result); + + Assert.Equal(16, (int)result[0].Age); + Assert.Equal("Sam", (string)result[0].Name); + Assert.Equal("USA", (string)result[0].Country); + } + finally + { + connection.Execute("drop table #Users"); + } + } + + [Fact] + public void TestSqlBuilderUpdateSet() + { + var id = 1; + var vip = true; + var updatetime = DateTime.Parse("2020/01/01"); + + var sb = new SqlBuilder() + .Set("Vip = @vip", new { vip }) + .Set("Updatetime = @updatetime", new { updatetime }) + .Where("Id = @id", new { id }) + ; + var template = sb.AddTemplate("update #Users /**set**/ /**where**/"); + + const string createSql = @" + create table #Users (Id int,Name varchar(20),Age int,Country nvarchar(5),Vip bit,Updatetime datetime); + insert #Users (Id,Name,Age,Country) values(1,'Sam',16,'USA'),(2,'Tom',25,'UK'),(3,'Henry',14,'UK')"; + try + { + connection.Execute(createSql); + + var effectCount = connection.Execute(template.RawSql, template.Parameters); + + var result = connection.QueryFirst("select * from #Users where Id = 1"); + + Assert.Equal("update #Users SET Vip = @vip , Updatetime = @updatetime\n WHERE Id = @id\n", template.RawSql); + + + Assert.True((bool)result.Vip); + Assert.Equal(updatetime, (DateTime)result.Updatetime); + } + finally + { + connection.Execute("drop table #Users"); + } + } + } +} diff --git a/tests/Dapper.Tests/TestBase.cs b/tests/Dapper.Tests/TestBase.cs new file mode 100644 index 000000000..e5e2c6560 --- /dev/null +++ b/tests/Dapper.Tests/TestBase.cs @@ -0,0 +1,139 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Globalization; +using System.Threading; +using Xunit; + +namespace Dapper.Tests +{ + public static class DatabaseProvider where TProvider : DatabaseProvider + { + public static TProvider Instance { get; } = Activator.CreateInstance(); + } + public abstract class DatabaseProvider + { + public abstract DbProviderFactory Factory { get; } + + public virtual void Dispose() { } + public abstract string GetConnectionString(); + + protected static string GetConnectionString(string name, string defaultConnectionString) => + Environment.GetEnvironmentVariable(name) ?? defaultConnectionString; + + public DbConnection GetOpenConnection() + { + var conn = Factory.CreateConnection()!; + conn.ConnectionString = GetConnectionString(); + conn.Open(); + if (conn.State != ConnectionState.Open) throw new InvalidOperationException("should be open!"); + return conn; + } + + public DbConnection GetClosedConnection() + { + var conn = Factory.CreateConnection()!; + conn.ConnectionString = GetConnectionString(); + if (conn.State != ConnectionState.Closed) throw new InvalidOperationException("should be closed!"); + return conn; + } + + public DbParameter CreateRawParameter(string name, object value) + { + var p = Factory.CreateParameter()!; + p.ParameterName = name; + p.Value = value ?? DBNull.Value; + return p; + } + } + + public abstract class SqlServerDatabaseProvider : DatabaseProvider + { + public override string GetConnectionString() => GetConnectionString(false); + + private string GetConnectionString(bool mars) + { + var builder = Factory.CreateConnectionStringBuilder()!; + builder.ConnectionString = GetConnectionString("SqlServerConnectionString", "Data Source=.;Initial Catalog=tempdb;Integrated Security=True"); + builder["TrustServerCertificate"] = true; + if (mars) + { + ((dynamic)builder).MultipleActiveResultSets = true; + } + return builder.ConnectionString; + } + + public DbConnection GetOpenConnection(bool mars) + { + var conn = Factory.CreateConnection()!; + conn.ConnectionString = GetConnectionString(mars); + conn.Open(); + if (conn.State != ConnectionState.Open) throw new InvalidOperationException("should be open!"); + return conn; + } + } + public sealed class SystemSqlClientProvider : SqlServerDatabaseProvider + { +#pragma warning disable CS0618 // Type or member is obsolete + public override DbProviderFactory Factory => System.Data.SqlClient.SqlClientFactory.Instance; +#pragma warning restore CS0618 // Type or member is obsolete + } +#if MSSQLCLIENT + public sealed class MicrosoftSqlClientProvider : SqlServerDatabaseProvider + { + public override DbProviderFactory Factory => Microsoft.Data.SqlClient.SqlClientFactory.Instance; + } +#endif + + public abstract class TestBase : IDisposable where TProvider : DatabaseProvider + { + protected void SkipIfMsDataClient() + => Skip.If(connection); + + protected DbConnection GetOpenConnection() => Provider.GetOpenConnection(); + protected DbConnection GetClosedConnection() => Provider.GetClosedConnection(); + protected DbConnection? _connection; + protected DbConnection connection => _connection ??= Provider.GetOpenConnection(); + + public TProvider Provider { get; } = DatabaseProvider.Instance; + + protected static CultureInfo ActiveCulture + { + get { return Thread.CurrentThread.CurrentCulture; } + set { Thread.CurrentThread.CurrentCulture = value; } + } + + static TestBase() + { + Console.WriteLine("Dapper: " + typeof(SqlMapper).AssemblyQualifiedName); + var provider = DatabaseProvider.Instance; + Console.WriteLine("Using Connectionstring: {0}", provider.GetConnectionString()); + var factory = provider.Factory; + Console.WriteLine("Using Provider: {0}", factory.GetType().FullName); + Console.WriteLine(".NET: " + Environment.Version); + Console.Write("Loading native assemblies for SQL types..."); + try + { + SqlServerTypesLoader.LoadNativeAssemblies(AppDomain.CurrentDomain.BaseDirectory); + Console.WriteLine("done."); + } + catch (Exception ex) + { + Console.WriteLine("failed."); + Console.Error.WriteLine(ex.Message); + } + } + + public virtual void Dispose() + { + _connection?.Dispose(); + _connection = null; + Provider?.Dispose(); + } + } + + public static class NonParallelDefinition + { + public const string Name = "NonParallel"; + } +} diff --git a/Dapper.Tests/TransactionTests.cs b/tests/Dapper.Tests/TransactionTests.cs similarity index 82% rename from Dapper.Tests/TransactionTests.cs rename to tests/Dapper.Tests/TransactionTests.cs index 84519feee..105813805 100644 --- a/Dapper.Tests/TransactionTests.cs +++ b/tests/Dapper.Tests/TransactionTests.cs @@ -7,7 +7,13 @@ namespace Dapper.Tests { - public class TransactionTests : TestBase + [Collection("TransactionTests")] + public sealed class SystemSqlClientTransactionTests : TransactionTests { } +#if MSSQLCLIENT + [Collection("TransactionTests")] + public sealed class MicrosoftSqlClientTransactionTests : TransactionTests { } +#endif + public abstract class TransactionTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestTransactionCommit() diff --git a/tests/Dapper.Tests/TupleTests.cs b/tests/Dapper.Tests/TupleTests.cs new file mode 100644 index 000000000..a8cf74586 --- /dev/null +++ b/tests/Dapper.Tests/TupleTests.cs @@ -0,0 +1,202 @@ +using System; +using Xunit; + +namespace Dapper.Tests +{ + [Collection("TupleTests")] + public sealed class SystemSqlClientTupleTests : TupleTests { } +#if MSSQLCLIENT + [Collection("TupleTests")] + public sealed class MicrosoftSqlClientTupleTests : TupleTests { } +#endif + public abstract class TupleTests : TestBase where TProvider : DatabaseProvider + { + [Fact] + public void TupleStructParameter_Fails_HelpfulMessage() + { + var ex = Assert.Throws(() => connection.QuerySingle("select @id", (id: 42, name: "Fred"))); + Assert.Equal("ValueTuple should not be used for parameters - the language-level names are not available to use as parameter names, and it adds unnecessary boxing", ex.Message); + } + + [Fact] + public void TupleClassParameter_Works() + { + Assert.Equal(42, connection.QuerySingle("select @Item1", Tuple.Create(42, "Fred"))); + } + + [Fact] + public void TupleReturnValue_Works_ByPosition() + { + var val = connection.QuerySingle<(int id, string name)>("select 42, 'Fred'"); + Assert.Equal(42, val.id); + Assert.Equal("Fred", val.name); + } + + [Fact] + public void TupleReturnValue_TooManyColumns_Ignored() + { + var val = connection.QuerySingle<(int id, string name)>("select 42, 'Fred', 123"); + Assert.Equal(42, val.id); + Assert.Equal("Fred", val.name); + } + + [Fact] + public void TupleReturnValue_NullableTuple_Works() + { + var val = connection.QuerySingleOrDefault<(int id, string name)?>("select 42, 'Fred', 123"); + Assert.NotNull(val); + Assert.Equal(42, val!.Value.id); + Assert.Equal("Fred", val.Value.name); + } + + [Fact] + public void TupleReturnValue_NullableTuple_Works_When_Null() + { + var val = connection.QuerySingleOrDefault<(int id, string name)?>("select 42, 'Fred', 123 where 1 = 2"); + Assert.Null(val); + } + + [Fact] + public void TupleReturnValue_TooFewColumns_Unmapped() + { + // I'm very wary of making this throw, but I can also see some sense in pointing out the oddness + var val = connection.QuerySingle<(int id, string name, int extra)>("select 42, 'Fred'"); + Assert.Equal(42, val.id); + Assert.Equal("Fred", val.name); + Assert.Equal(0, val.extra); + } + + [Fact] + public void TupleReturnValue_Works_NamesIgnored() + { + var val = connection.QuerySingle<(int id, string name)>("select 42 as [Item2], 'Fred' as [Item1]"); + Assert.Equal(42, val.id); + Assert.Equal("Fred", val.name); + } + + [Fact] + public void TupleReturnValue_Works_With8Elements() + { + // C# encodes an 8-tuple as ValueTuple> + + var val = connection.QuerySingle<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8)>( + "select 1, 2, 3, 4, 5, 6, 7, 8"); + + Assert.Equal(1, val.e1); + Assert.Equal(2, val.e2); + Assert.Equal(3, val.e3); + Assert.Equal(4, val.e4); + Assert.Equal(5, val.e5); + Assert.Equal(6, val.e6); + Assert.Equal(7, val.e7); + Assert.Equal(8, val.e8); + } + + [Fact] + public void Nullable_TupleReturnValue_Works_With8Elements() + { + // C# encodes an 8-tuple as ValueTuple> + + var val = connection.QuerySingle<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8)?>( + "select 1, 2, 3, 4, 5, 6, 7, 8"); + + Assert.NotNull(val); + Assert.Equal(1, val!.Value.e1); + Assert.Equal(2, val.Value.e2); + Assert.Equal(3, val.Value.e3); + Assert.Equal(4, val.Value.e4); + Assert.Equal(5, val.Value.e5); + Assert.Equal(6, val.Value.e6); + Assert.Equal(7, val.Value.e7); + Assert.Equal(8, val.Value.e8); + } + + [Fact] + public void Nullable_TupleReturnValue_Works_With8Elements_When_Null() + { + // C# encodes an 8-tuple as ValueTuple> + + var val = connection.QuerySingleOrDefault<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8)?>( + "select 1, 2, 3, 4, 5, 6, 7, 8 where 1 = 2"); + + Assert.Null(val); + } + + [Fact] + public void TupleReturnValue_Works_With15Elements() + { + // C# encodes a 15-tuple as ValueTuple>> + + var val = connection.QuerySingle<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8, int e9, int e10, int e11, int e12, int e13, int e14, int e15)>( + "select 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15"); + + Assert.Equal(1, val.e1); + Assert.Equal(2, val.e2); + Assert.Equal(3, val.e3); + Assert.Equal(4, val.e4); + Assert.Equal(5, val.e5); + Assert.Equal(6, val.e6); + Assert.Equal(7, val.e7); + Assert.Equal(8, val.e8); + Assert.Equal(9, val.e9); + Assert.Equal(10, val.e10); + Assert.Equal(11, val.e11); + Assert.Equal(12, val.e12); + Assert.Equal(13, val.e13); + Assert.Equal(14, val.e14); + Assert.Equal(15, val.e15); + } + + + [Fact] + public void Nullable_TupleReturnValue_Works_With15Elements() + { + // C# encodes a 15-tuple as ValueTuple>> + + var val = connection.QuerySingle<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8, int e9, int e10, int e11, int e12, int e13, int e14, int e15)?>( + "select 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15"); + + Assert.NotNull(val); + Assert.Equal(1, val!.Value.e1); + Assert.Equal(2, val.Value.e2); + Assert.Equal(3, val.Value.e3); + Assert.Equal(4, val.Value.e4); + Assert.Equal(5, val.Value.e5); + Assert.Equal(6, val.Value.e6); + Assert.Equal(7, val.Value.e7); + Assert.Equal(8, val.Value.e8); + Assert.Equal(9, val.Value.e9); + Assert.Equal(10, val.Value.e10); + Assert.Equal(11, val.Value.e11); + Assert.Equal(12, val.Value.e12); + Assert.Equal(13, val.Value.e13); + Assert.Equal(14, val.Value.e14); + Assert.Equal(15, val.Value.e15); + } + + [Fact] + public void Nullable_TupleReturnValue_Works_With15Elements_When_Null() + { + // C# encodes a 15-tuple as ValueTuple>> + + var val = connection.QuerySingleOrDefault<(int e1, int e2, int e3, int e4, int e5, int e6, int e7, int e8, int e9, int e10, int e11, int e12, int e13, int e14, int e15)?>( + "select 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 where 1 = 2"); + + Assert.Null(val); + } + + [Fact] + public void TupleReturnValue_Works_WithStringField() + { + var val = connection.QuerySingle>("select '42'"); + Assert.Equal("42", val.Item1); + } + + [Fact] + public void TupleReturnValue_Works_WithByteField() + { + var val = connection.QuerySingle>("select 0xDEADBEEF"); + Assert.Equal(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }, val.Item1); + } + } +} diff --git a/Dapper.Tests/TypeHandlerTests.cs b/tests/Dapper.Tests/TypeHandlerTests.cs similarity index 68% rename from Dapper.Tests/TypeHandlerTests.cs rename to tests/Dapper.Tests/TypeHandlerTests.cs index 66440424d..385ffb90b 100644 --- a/Dapper.Tests/TypeHandlerTests.cs +++ b/tests/Dapper.Tests/TypeHandlerTests.cs @@ -9,7 +9,13 @@ namespace Dapper.Tests { [Collection(NonParallelDefinition.Name)] - public class TypeHandlerTests : TestBase + public sealed class SystemSqlClientTypeHandlerTests : TypeHandlerTests { } +#if MSSQLCLIENT + [Collection(NonParallelDefinition.Name)] + public sealed class MicrosoftSqlClientTypeHandlerTests : TypeHandlerTests { } +#endif + + public abstract class TypeHandlerTests : TestBase where TProvider : DatabaseProvider { [Fact] public void TestChangingDefaultStringTypeMappingToAnsiString() @@ -22,12 +28,18 @@ public void TestChangingDefaultStringTypeMappingToAnsiString() SqlMapper.PurgeQueryCache(); - SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString); // Change Default String Handling to AnsiString - var result02 = connection.Query(sql, param).FirstOrDefault(); - Assert.Equal("varchar", result02); + SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString, false); // Change Default String Handling to AnsiString + try + { + var result02 = connection.Query(sql, param).FirstOrDefault(); + Assert.Equal("varchar", result02); - SqlMapper.PurgeQueryCache(); - SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String + SqlMapper.PurgeQueryCache(); + } + finally + { + SqlMapper.AddTypeMap(typeof(string), DbType.String, false); // Restore Default to Unicode String + } } [Fact] @@ -40,13 +52,18 @@ public void TestChangingDefaultStringTypeMappingToAnsiStringFirstOrDefault() Assert.Equal("nvarchar", result01); SqlMapper.PurgeQueryCache(); + SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString, false); // Change Default String Handling to AnsiString + try + { + var result02 = connection.QueryFirstOrDefault(sql, param); + Assert.Equal("varchar", result02); - SqlMapper.AddTypeMap(typeof(string), DbType.AnsiString); // Change Default String Handling to AnsiString - var result02 = connection.QueryFirstOrDefault(sql, param); - Assert.Equal("varchar", result02); - - SqlMapper.PurgeQueryCache(); - SqlMapper.AddTypeMap(typeof(string), DbType.String); // Restore Default to Unicode String + SqlMapper.PurgeQueryCache(); + } + finally + { + SqlMapper.AddTypeMap(typeof(string), DbType.String, false); // Restore Default to Unicode String + } } [Fact] @@ -59,7 +76,7 @@ public void TestCustomTypeMap() // custom mapping var map = new CustomPropertyTypeMap(typeof(TypeWithMapping), - (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName)); + (type, columnName) => type.GetProperties().FirstOrDefault(prop => GetDescriptionFromAttribute(prop) == columnName)!); SqlMapper.SetTypeMap(typeof(TypeWithMapping), map); item = connection.Query("Select 'AVal' as A, 'BVal' as B").Single(); @@ -73,25 +90,20 @@ public void TestCustomTypeMap() Assert.Equal("BVal", item.B); } - private static string GetDescriptionFromAttribute(MemberInfo member) + private static string? GetDescriptionFromAttribute(MemberInfo member) { if (member == null) return null; -#if NETCOREAPP1_0 - var data = member.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(DescriptionAttribute)); - return (string)data?.ConstructorArguments.Single().Value; -#else - var attrib = (DescriptionAttribute)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false); + var attrib = (DescriptionAttribute?)Attribute.GetCustomAttribute(member, typeof(DescriptionAttribute), false); return attrib?.Description; -#endif } public class TypeWithMapping { [Description("B")] - public string A { get; set; } + public string? A { get; set; } [Description("A")] - public string B { get; set; } + public string? B { get; set; } } [Fact] @@ -121,9 +133,9 @@ private LocalDateHandler() { /* private constructor */ } // by mistake. public static readonly SqlMapper.ITypeHandler Default = new LocalDateHandler(); - public override LocalDate Parse(object value) + public override LocalDate Parse(object? value) { - var date = (DateTime)value; + var date = (DateTime)value!; return new LocalDate { Year = date.Year, Month = date.Month, Day = date.Day }; } @@ -233,17 +245,17 @@ private void TestBigIntForEverythingWorks_ByDataType(string dbType) var row = connection.Query(sql).Single(); Assert.True(row.N_Bool); - Assert.Equal(row.N_SByte, (sbyte)1); - Assert.Equal(row.N_Byte, (byte)1); - Assert.Equal(row.N_Int, (int)1); - Assert.Equal(row.N_UInt, (uint)1); - Assert.Equal(row.N_Short, (short)1); - Assert.Equal(row.N_UShort, (ushort)1); - Assert.Equal(row.N_Long, (long)1); - Assert.Equal(row.N_ULong, (ulong)1); - Assert.Equal(row.N_Float, (float)1); - Assert.Equal(row.N_Double, (double)1); - Assert.Equal(row.N_Decimal, (decimal)1); + Assert.Equal((sbyte)1, row.N_SByte); + Assert.Equal((byte)1, row.N_Byte); + Assert.Equal((int)1, row.N_Int); + Assert.Equal((uint)1, row.N_UInt); + Assert.Equal((short)1, row.N_Short); + Assert.Equal((ushort)1, row.N_UShort); + Assert.Equal((long)1, row.N_Long); + Assert.Equal((ulong)1, row.N_ULong); + Assert.Equal((float)1, row.N_Float); + Assert.Equal((double)1, row.N_Double); + Assert.Equal((decimal)1, row.N_Decimal); Assert.Equal(LotsOfNumerics.E_Byte.B, row.P_Byte); Assert.Equal(LotsOfNumerics.E_SByte.B, row.P_SByte); @@ -254,27 +266,27 @@ private void TestBigIntForEverythingWorks_ByDataType(string dbType) Assert.Equal(LotsOfNumerics.E_Long.B, row.P_Long); Assert.Equal(LotsOfNumerics.E_ULong.B, row.P_ULong); - Assert.True(row.N_N_Bool.Value); - Assert.Equal(row.N_N_SByte.Value, (sbyte)1); - Assert.Equal(row.N_N_Byte.Value, (byte)1); - Assert.Equal(row.N_N_Int.Value, (int)1); - Assert.Equal(row.N_N_UInt.Value, (uint)1); - Assert.Equal(row.N_N_Short.Value, (short)1); - Assert.Equal(row.N_N_UShort.Value, (ushort)1); - Assert.Equal(row.N_N_Long.Value, (long)1); - Assert.Equal(row.N_N_ULong.Value, (ulong)1); - Assert.Equal(row.N_N_Float.Value, (float)1); - Assert.Equal(row.N_N_Double.Value, (double)1); - Assert.Equal(row.N_N_Decimal, (decimal)1); - - Assert.Equal(LotsOfNumerics.E_Byte.B, row.N_P_Byte.Value); - Assert.Equal(LotsOfNumerics.E_SByte.B, row.N_P_SByte.Value); - Assert.Equal(LotsOfNumerics.E_Short.B, row.N_P_Short.Value); - Assert.Equal(LotsOfNumerics.E_UShort.B, row.N_P_UShort.Value); - Assert.Equal(LotsOfNumerics.E_Int.B, row.N_P_Int.Value); - Assert.Equal(LotsOfNumerics.E_UInt.B, row.N_P_UInt.Value); - Assert.Equal(LotsOfNumerics.E_Long.B, row.N_P_Long.Value); - Assert.Equal(LotsOfNumerics.E_ULong.B, row.N_P_ULong.Value); + Assert.True(row.N_N_Bool!.Value); + Assert.Equal((sbyte)1, row.N_N_SByte!.Value); + Assert.Equal((byte)1, row.N_N_Byte!.Value); + Assert.Equal((int)1, row.N_N_Int!.Value); + Assert.Equal((uint)1, row.N_N_UInt!.Value); + Assert.Equal((short)1, row.N_N_Short!.Value); + Assert.Equal((ushort)1, row.N_N_UShort!.Value); + Assert.Equal((long)1, row.N_N_Long!.Value); + Assert.Equal((ulong)1, row.N_N_ULong!.Value); + Assert.Equal((float)1, row.N_N_Float!.Value); + Assert.Equal((double)1, row.N_N_Double!.Value); + Assert.Equal((decimal)1, row.N_N_Decimal); + + Assert.Equal(LotsOfNumerics.E_Byte.B, row.N_P_Byte!.Value); + Assert.Equal(LotsOfNumerics.E_SByte.B, row.N_P_SByte!.Value); + Assert.Equal(LotsOfNumerics.E_Short.B, row.N_P_Short!.Value); + Assert.Equal(LotsOfNumerics.E_UShort.B, row.N_P_UShort!.Value); + Assert.Equal(LotsOfNumerics.E_Int.B, row.N_P_Int!.Value); + Assert.Equal(LotsOfNumerics.E_UInt.B, row.N_P_UInt!.Value); + Assert.Equal(LotsOfNumerics.E_Long.B, row.N_P_Long!.Value); + Assert.Equal(LotsOfNumerics.E_ULong.B, row.N_P_ULong!.Value); TestBigIntForEverythingWorksGeneric(true, dbType); TestBigIntForEverythingWorksGeneric((sbyte)1, dbType); @@ -373,23 +385,23 @@ private RatingValueHandler() { } - public static readonly RatingValueHandler Default = new RatingValueHandler(); + public static readonly RatingValueHandler Default = new(); - public override RatingValue Parse(object value) + public override RatingValue Parse(object? value) { - if (value is int) + if (value is int i) { - return new RatingValue() { Value = (int)value }; + return new RatingValue() { Value = i }; } throw new FormatException("Invalid conversion to RatingValue"); } - public override void SetValue(IDbDataParameter parameter, RatingValue value) + public override void SetValue(IDbDataParameter parameter, RatingValue? value) { // ... null, range checks etc ... parameter.DbType = System.Data.DbType.Int32; - parameter.Value = value.Value; + parameter.Value = value?.Value; } } @@ -401,8 +413,8 @@ public class RatingValue public class MyResult { - public string CategoryName { get; set; } - public RatingValue CategoryRating { get; set; } + public string? CategoryName { get; set; } + public RatingValue? CategoryRating { get; set; } } [Fact] @@ -412,7 +424,7 @@ public void SO24740733_TestCustomValueHandler() var foo = connection.Query("SELECT 'Foo' AS CategoryName, 200 AS CategoryRating").Single(); Assert.Equal("Foo", foo.CategoryName); - Assert.Equal(200, foo.CategoryRating.Value); + Assert.Equal(200, foo.CategoryRating?.Value); } [Fact] @@ -430,14 +442,14 @@ private StringListTypeHandler() { } - public static readonly StringListTypeHandler Default = new StringListTypeHandler(); + public static readonly StringListTypeHandler Default = new(); //Just a simple List type handler implementation - public override void SetValue(IDbDataParameter parameter, List value) + public override void SetValue(IDbDataParameter parameter, List? value) { - parameter.Value = string.Join(",", value); + parameter.Value = string.Join(",", value ?? new()); } - public override List Parse(object value) + public override List Parse(object? value) { return ((value as string) ?? "").Split(',').ToList(); } @@ -445,7 +457,7 @@ public override List Parse(object value) public class MyObjectWithStringList { - public List Names { get; set; } + public List? Names { get; set; } } [Fact] @@ -469,7 +481,7 @@ public void Issue253_TestIEnumerableTypeHandlerSetParameterValue() const string names = "Sam,Kyro"; List names_list = names.Split(',').ToList(); var foo = connection.Query("INSERT INTO #Issue253 (Names) VALUES (@Names); SELECT Names FROM #Issue253;", new { Names = names_list }).Single(); - Assert.Equal(foo, names); + Assert.Equal(names, foo); } finally { @@ -479,16 +491,16 @@ public void Issue253_TestIEnumerableTypeHandlerSetParameterValue() public class RecordingTypeHandler : SqlMapper.TypeHandler { - public override void SetValue(IDbDataParameter parameter, T value) + public override void SetValue(IDbDataParameter parameter, T? value) { SetValueWasCalled = true; parameter.Value = value; } - public override T Parse(object value) + public override T Parse(object? value) { ParseWasCalled = true; - return (T)value; + return (T)value!; } public bool SetValueWasCalled { get; set; } @@ -576,20 +588,27 @@ public class WrongTypes public void TestWrongTypes_WithRightTypes() { var item = connection.Query("select 1 as A, cast(2.0 as float) as B, cast(3 as bigint) as C, cast(1 as bit) as D").Single(); - item.A.Equals(1); - item.B.Equals(2.0); - item.C.Equals(3L); - item.D.Equals(true); + Assert.Equal(1, item.A); + Assert.Equal(2.0, item.B); + Assert.Equal(3L, item.C); + Assert.True(item.D); } [Fact] public void TestWrongTypes_WithWrongTypes() { var item = connection.Query("select cast(1.0 as float) as A, 2 as B, 3 as C, cast(1 as bigint) as D").Single(); - item.A.Equals(1); - item.B.Equals(2.0); - item.C.Equals(3L); - item.D.Equals(true); + Assert.Equal(1, item.A); + Assert.Equal(2.0, item.B); + Assert.Equal(3L, item.C); + Assert.True(item.D); + } + + [Fact] + public void TestTreatIntAsABool() + { + Assert.True(connection.Query("select CAST(1 AS BIT)").Single()); + Assert.True(connection.Query("select 1").Single()); } [Fact] @@ -600,7 +619,9 @@ public void SO24607639_NullableBools() insert @vals (A,B,C) values (1,0,null); select * from @vals").Single(); Assert.NotNull(obj); + Assert.True(obj.A.HasValue); Assert.True(obj.A.Value); + Assert.True(obj.B.HasValue); Assert.False(obj.B.Value); Assert.Null(obj.C); } @@ -633,10 +654,10 @@ public void Issue149_TypeMismatch_SequentialAccess() { Guid guid = Guid.Parse("cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e"); var ex = Assert.ThrowsAny(() => connection.Query("select @guid as Id", new { guid }).First()); - Assert.Equal("Error parsing column 0 (Id=cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e - Object)", ex.Message); + Assert.Equal("Error parsing column 0 (Id=cf0ef7ac-b6fe-4e24-aeda-a2b45bb5654e - Guid)", ex.Message); } - public class Issue149_Person { public string Id { get; set; } } + public class Issue149_Person { public string? Id { get; set; } } [Fact] public void Issue295_NullableDateTime_SqlServer() => Common.TestDateTime(connection); @@ -671,13 +692,13 @@ SomeBlargValue nvarchar(200), var parameterlessWorks = connection.QuerySingle("SELECT * FROM #Issue461"); Assert.Equal(1, parameterlessWorks.Id); Assert.Equal("what up?", parameterlessWorks.SomeValue); - Assert.Equal(parameterlessWorks.SomeBlargValue.Value, Expected); + Assert.Equal(Expected, parameterlessWorks.SomeBlargValue?.Value); // test: via constructor var parameterDoesNot = connection.QuerySingle("SELECT * FROM #Issue461"); Assert.Equal(1, parameterDoesNot.Id); Assert.Equal("what up?", parameterDoesNot.SomeValue); - Assert.Equal(parameterDoesNot.SomeBlargValue.Value, Expected); + Assert.Equal(Expected, parameterDoesNot.SomeBlargValue?.Value); } // I would usually expect this to be a struct; using a class @@ -685,24 +706,24 @@ SomeBlargValue nvarchar(200), // to see an InvalidCastException if it is wrong private class Blarg { - public Blarg(string value) { Value = value; } - public string Value { get; } + public Blarg(string? value) { Value = value; } + public string? Value { get; } public override string ToString() { - return Value; + return Value!; } } private class Issue461_BlargHandler : SqlMapper.TypeHandler { - public override void SetValue(IDbDataParameter parameter, Blarg value) + public override void SetValue(IDbDataParameter parameter, Blarg? value) { - parameter.Value = ((object)value.Value) ?? DBNull.Value; + parameter.Value = ((object?)value?.Value) ?? DBNull.Value; } - public override Blarg Parse(object value) + public override Blarg? Parse(object? value) { - string s = (value == null || value is DBNull) ? null : Convert.ToString(value); + string? s = (value == null || value is DBNull) ? null : Convert.ToString(value); return new Blarg(s); } } @@ -711,8 +732,8 @@ private class Issue461_ParameterlessTypeConstructor { public int Id { get; set; } - public string SomeValue { get; set; } - public Blarg SomeBlargValue { get; set; } + public string? SomeValue { get; set; } + public Blarg? SomeBlargValue { get; set; } } private class Issue461_ParameterisedTypeConstructor @@ -729,5 +750,130 @@ public Issue461_ParameterisedTypeConstructor(int id, string someValue, Blarg som public string SomeValue { get; } public Blarg SomeBlargValue { get; } } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Issue1959_TypeHandlerNullability_Subclass(bool isNull) + { + Issue1959_Subclass_Handler.Register(); + Issue1959_Subclass? when = isNull ? null : new(DateTime.Today); + var whenNotNull = when ?? new(new DateTime(1753, 1, 1)); + + var args = new HazIssue1959_Subclass { Id = 42, Nullable = when, NonNullable = whenNotNull }; + var row = connection.QuerySingle( + "select @Id as [Id], @NonNullable as [NonNullable], @Nullable as [Nullable]", + args); + + Assert.NotNull(row); + Assert.Equal(42, row.Id); + Assert.Equal(when, row.Nullable); + Assert.Equal(whenNotNull, row.NonNullable); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Issue1959_TypeHandlerNullability_Raw(bool isNull) + { + Issue1959_Raw_Handler.Register(); + Issue1959_Raw? when = isNull ? null : new(DateTime.Today); + var whenNotNull = when ?? new(new DateTime(1753, 1, 1)); + + var args = new HazIssue1959_Raw { Id = 42, Nullable = when, NonNullable = whenNotNull }; + var row = connection.QuerySingle( + "select @Id as [Id], @NonNullable as [NonNullable], @Nullable as [Nullable]", + args); + + Assert.NotNull(row); + Assert.Equal(42, row.Id); + Assert.Equal(when, row.Nullable); + Assert.Equal(whenNotNull, row.NonNullable); + } + + public class HazIssue1959_Subclass + { + public int Id { get; set; } + public Issue1959_Subclass NonNullable { get; set; } + public Issue1959_Subclass? Nullable { get; set; } + } + + public class HazIssue1959_Raw + { + public int Id { get; set; } + public Issue1959_Raw NonNullable { get; set; } + public Issue1959_Raw? Nullable { get; set; } + } + + public class Issue1959_Subclass_Handler : SqlMapper.TypeHandler + { + public static void Register() => SqlMapper.AddTypeHandler(Instance); + private Issue1959_Subclass_Handler() { } + private static readonly Issue1959_Subclass_Handler Instance = new(); + + public override Issue1959_Subclass Parse(object value) + { + Assert.NotNull(value); + Assert.IsType(value); // checking not DbNull etc + return new Issue1959_Subclass((DateTime)value); + } + public override void SetValue(IDbDataParameter parameter, TypeHandlerTests.Issue1959_Subclass value) + => parameter.Value = value.Value; + } + + public class Issue1959_Raw_Handler : SqlMapper.ITypeHandler + { + public static void Register() => SqlMapper.AddTypeHandler(typeof(Issue1959_Raw), Instance); + private Issue1959_Raw_Handler() { } + private static readonly Issue1959_Raw_Handler Instance = new(); + + void SqlMapper.ITypeHandler.SetValue(IDbDataParameter parameter, object value) + { + Assert.NotNull(value); + if (value is DBNull) + { + parameter.Value = value; + } + else + { + Assert.IsType(value); // checking not DbNull etc + parameter.Value = ((Issue1959_Raw)value).Value; + } + } + object? SqlMapper.ITypeHandler.Parse(Type destinationType, object value) + { + Assert.NotNull(value); + Assert.IsType(value); // checking not DbNull etc + return new Issue1959_Raw((DateTime)value); + } + } + +#pragma warning disable CA2231 // Overload operator equals on overriding value type Equals + public readonly struct Issue1959_Subclass : IEquatable +#pragma warning restore CA2231 // Overload operator equals on overriding value type Equals + { + public Issue1959_Subclass(DateTime value) => Value = value; + public readonly DateTime Value; + public override int GetHashCode() => Value.GetHashCode(); + public override bool Equals(object? obj) + => obj is Issue1959_Subclass other && Equals(other); + public bool Equals(Issue1959_Subclass other) + => other.Value == Value; + public override string ToString() => Value.ToString(); + } + +#pragma warning disable CA2231 // Overload operator equals on overriding value type Equals + public readonly struct Issue1959_Raw : IEquatable +#pragma warning restore CA2231 // Overload operator equals on overriding value type Equals + { + public Issue1959_Raw(DateTime value) => Value = value; + public readonly DateTime Value; + public override int GetHashCode() => Value.GetHashCode(); + public override bool Equals(object? obj) + => obj is Issue1959_Raw other && Equals(other); + public bool Equals(Issue1959_Raw other) + => other.Value == Value; + public override string ToString() => Value.ToString(); + } } } diff --git a/tests/Dapper.Tests/WrappedReaderTests.cs b/tests/Dapper.Tests/WrappedReaderTests.cs new file mode 100644 index 000000000..7f884f420 --- /dev/null +++ b/tests/Dapper.Tests/WrappedReaderTests.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections; +using System.Data; +using System.Data.Common; +using Xunit.Abstractions; + +namespace Dapper.Tests; + +public class WrappedReaderTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public void DbWrappedReader_Dispose_DoesNotThrow() + { + var reader = new DbWrappedReader(new DummyDbCommand(), new ThrowOnCloseDbDataReader(testOutputHelper)); + reader.Dispose(); + } + +#if !NETFRAMEWORK + [Fact] + public async System.Threading.Tasks.Task DbWrappedReader_DisposeAsync_DoesNotThrow() + { + var reader = new DbWrappedReader(new DummyDbCommand(), new ThrowOnCloseDbDataReader(testOutputHelper)); + await reader.DisposeAsync(); + } +#endif + + [Fact] + public void WrappedBasicReader_Dispose_DoesNotThrow() + { + var reader = new WrappedBasicReader(new ThrowOnCloseIDataReader()); + reader.Dispose(); + } + +#if !NETFRAMEWORK + [Fact] + public async System.Threading.Tasks.Task WrappedBasicReader_DisposeAsync_DoesNotThrow() + { + var reader = new WrappedBasicReader(new ThrowOnCloseIDataReader()); + await reader.DisposeAsync(); + } +#endif + + private class DummyDbCommand : DbCommand + { + public override void Cancel() => throw new NotSupportedException(); + public override int ExecuteNonQuery() => throw new NotSupportedException(); + public override object ExecuteScalar() => throw new NotSupportedException(); + public override void Prepare() => throw new NotSupportedException(); + +#pragma warning disable CS8765 // nullability of value + public override string CommandText { get; set; } = ""; +#pragma warning restore CS8765 // nullability of value + public override int CommandTimeout { get; set; } + public override CommandType CommandType { get; set; } + public override UpdateRowSource UpdatedRowSource { get; set; } + protected override DbConnection? DbConnection { get; set; } + protected override DbParameterCollection DbParameterCollection => throw new NotSupportedException(); + protected override DbTransaction? DbTransaction { get; set; } + public override bool DesignTimeVisible { get; set; } + protected override DbParameter CreateDbParameter() => throw new NotSupportedException(); + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotSupportedException(); + } + + private class DummyDbException(string message) : DbException(message); + + private class ThrowOnCloseDbDataReader(ITestOutputHelper testOutputHelper) : DbDataReader + { + // This is basically what SqlClient does, see https://github.com/dotnet/SqlClient/blob/v5.2.1/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlDataReader.cs#L835-L849 + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + Close(); + } + base.Dispose(disposing); + } + catch (DbException e) + { + testOutputHelper.WriteLine($"Ignored exception when disposing {e}"); + } + } + + public override void Close() => throw new DummyDbException("Exception during Close()"); + + public override bool GetBoolean(int ordinal) => throw new NotSupportedException(); + public override byte GetByte(int ordinal) => throw new NotSupportedException(); + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); + public override char GetChar(int ordinal) => throw new NotSupportedException(); + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) => throw new NotSupportedException(); + public override string GetDataTypeName(int ordinal) => throw new NotSupportedException(); + public override DateTime GetDateTime(int ordinal) => throw new NotSupportedException(); + public override decimal GetDecimal(int ordinal) => throw new NotSupportedException(); + public override double GetDouble(int ordinal) => throw new NotSupportedException(); + public override Type GetFieldType(int ordinal) => throw new NotSupportedException(); + public override float GetFloat(int ordinal) => throw new NotSupportedException(); + public override Guid GetGuid(int ordinal) => throw new NotSupportedException(); + public override short GetInt16(int ordinal) => throw new NotSupportedException(); + public override int GetInt32(int ordinal) => throw new NotSupportedException(); + public override long GetInt64(int ordinal) => throw new NotSupportedException(); + public override string GetName(int ordinal) => throw new NotSupportedException(); + public override int GetOrdinal(string name) => throw new NotSupportedException(); + public override string GetString(int ordinal) => throw new NotSupportedException(); + public override object GetValue(int ordinal) => throw new NotSupportedException(); + public override int GetValues(object[] values) => throw new NotSupportedException(); + public override bool IsDBNull(int ordinal) => throw new NotSupportedException(); + public override int FieldCount => throw new NotSupportedException(); + public override object this[int ordinal] => throw new NotSupportedException(); + public override object this[string name] => throw new NotSupportedException(); + public override int RecordsAffected => throw new NotSupportedException(); + public override bool HasRows => throw new NotSupportedException(); + public override bool IsClosed => throw new NotSupportedException(); + public override bool NextResult() => throw new NotSupportedException(); + public override bool Read() => throw new NotSupportedException(); + public override int Depth => throw new NotSupportedException(); + public override IEnumerator GetEnumerator() => throw new NotSupportedException(); + } + + private class ThrowOnCloseIDataReader : IDataReader + { + public void Dispose() + { + // Assume that IDataReader Dispose implementation does not throw + } + + public void Close() => throw new DummyDbException("Exception during Close()"); + + public bool GetBoolean(int i) => throw new NotSupportedException(); + public byte GetByte(int i) => throw new NotSupportedException(); + public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotSupportedException(); + public char GetChar(int i) => throw new NotSupportedException(); + public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotSupportedException(); + public IDataReader GetData(int i) => throw new NotSupportedException(); + public string GetDataTypeName(int i) => throw new NotSupportedException(); + public DateTime GetDateTime(int i) => throw new NotSupportedException(); + public decimal GetDecimal(int i) => throw new NotSupportedException(); + public double GetDouble(int i) => throw new NotSupportedException(); + public Type GetFieldType(int i) => throw new NotSupportedException(); + public float GetFloat(int i) => throw new NotSupportedException(); + public Guid GetGuid(int i) => throw new NotSupportedException(); + public short GetInt16(int i) => throw new NotSupportedException(); + public int GetInt32(int i) => throw new NotSupportedException(); + public long GetInt64(int i) => throw new NotSupportedException(); + public string GetName(int i) => throw new NotSupportedException(); + public int GetOrdinal(string name) => throw new NotSupportedException(); + public string GetString(int i) => throw new NotSupportedException(); + public object GetValue(int i) => throw new NotSupportedException(); + public int GetValues(object[] values) => throw new NotSupportedException(); + public bool IsDBNull(int i) => throw new NotSupportedException(); + public int FieldCount => throw new NotSupportedException(); + public object this[int i] => throw new NotSupportedException(); + public object this[string name] => throw new NotSupportedException(); + public DataTable? GetSchemaTable() => throw new NotSupportedException(); + public bool NextResult() => throw new NotSupportedException(); + public bool Read() => throw new NotSupportedException(); + public int Depth => throw new NotSupportedException(); + public bool IsClosed => throw new NotSupportedException(); + public int RecordsAffected => throw new NotSupportedException(); + } +} diff --git a/tests/Dapper.Tests/XmlTests.cs b/tests/Dapper.Tests/XmlTests.cs new file mode 100644 index 000000000..ee130c4d5 --- /dev/null +++ b/tests/Dapper.Tests/XmlTests.cs @@ -0,0 +1,40 @@ +using System.Xml; +using System.Xml.Linq; +using Xunit; + +namespace Dapper.Tests +{ + [Collection("XmlTests")] + public sealed class SystemSqlClientXmlTests : XmlTests { } +#if MSSQLCLIENT + [Collection("XmlTests")] + public sealed class MicrosoftSqlClientXmlTests : XmlTests { } +#endif + public abstract class XmlTests : TestBase where TProvider : DatabaseProvider + { + [Fact] + public void CommonXmlTypesSupported() + { + var xml = new XmlDocument(); + xml.LoadXml(""); + + var foo = new Foo + { + A = xml, + B = XDocument.Parse(""), + C = XElement.Parse("") + }; + var bar = connection.QuerySingle("select @a as [A], @b as [B], @c as [C]", new { a = foo.A, b = foo.B, c = foo.C }); + Assert.Equal("abc", bar.A?.DocumentElement?.Name); + Assert.Equal("def", bar.B?.Root?.Name.LocalName); + Assert.Equal("ghi", bar.C?.Name.LocalName); + } + + public class Foo + { + public XmlDocument? A { get; set; } + public XDocument? B { get; set; } + public XElement? C { get; set; } + } + } +} diff --git a/tests/Dapper.Tests/xunit.runner.json b/tests/Dapper.Tests/xunit.runner.json new file mode 100644 index 000000000..f23143776 --- /dev/null +++ b/tests/Dapper.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "shadowCopy": false +} \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 000000000..ca3ad8b24 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,40 @@ + + + + Library + false + false + false + false + true + + Full + $(DefineConstants);WINDOWS + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets new file mode 100644 index 000000000..2af8b3c02 --- /dev/null +++ b/tests/Directory.Build.targets @@ -0,0 +1,5 @@ + + + false + + \ No newline at end of file diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 000000000..5e76d0a57 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3" +services: + mysql: + image: mysql:8 + container_name: mysql + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + postgres: + image: postgres:alpine + container_name: postgres + ports: + - 5432:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test + sqlserver: + image: mcr.microsoft.com/mssql/server:2019-latest + container_name: sql-server-db + ports: + - 1433:1433 + environment: + ACCEPT_EULA: Y + SA_PASSWORD: "Password." diff --git a/version.json b/version.json new file mode 100644 index 000000000..5220edbe8 --- /dev/null +++ b/version.json @@ -0,0 +1,17 @@ +{ + "version": "2.1", + "assemblyVersion": "2.0.0.0", + "publicReleaseRefSpec": [ + "^refs/heads/main$", + "^refs/tags/v\\d+\\.\\d+" + ], + "nugetPackageVersion": { + "semVer": 2 + }, + "cloudBuild": { + "buildNumber": { + "enabled": true, + "setVersionVariables": true + } + } +} \ No newline at end of file