From 1688c9f8d16959844d27cf3093baa270cb366eb3 Mon Sep 17 00:00:00 2001 From: ky_sunl Date: Tue, 16 Mar 2021 05:49:42 +0000 Subject: [PATCH] --- ...Arguments.csproj => Ewide.Core.DTO.csproj} | 5 +- .../Ewide.Core.Arguments/TestArgs.cs | 25 + .../Ewide.Core.Common/BaseDisplayJSON.cs | 14 +- .../EnumCode/ResponseStatus.cs | 20 - .../Ewide.Core.Common.csproj | 13 +- .../Ewide.Core.Common/packages.config | 4 + .../Dapper/CommandDefinition.cs | 187 + .../Ewide.Core.Data/Dapper/CommandFlags.cs | 30 + .../Dapper/CustomPropertyTypeMap.cs | 74 + .../Dapper/DataTableHandler.cs | 19 + .../Ewide.Core.Data/Dapper/DbString.cs | 75 + .../Ewide.Core.Data/Dapper/DefaultTypeMap.cs | 203 + .../DynamicParameters.CachedOutputSetters.cs | 16 + .../Dapper/DynamicParameters.ParamInfo.cs | 24 + .../Dapper/DynamicParameters.cs | 505 +++ .../Dapper/ExplicitConstructorAttribute.cs | 12 + .../Ewide.Core.Data/Dapper/FeatureSupport.cs | 33 + .../Ewide.Core.Data/Dapper/SimpleMemberMap.cs | 87 + .../Dapper/SqlDataRecordHandler.cs | 21 + .../Dapper/SqlDataRecordListTVPParameter.cs | 52 + .../Ewide.Core.Data/Dapper/SqlMapper.Async.cs | 907 +++++ .../Dapper/SqlMapper.CacheInfo.cs | 19 + .../Dapper/SqlMapper.DapperRow.cs | 214 + .../Dapper/SqlMapper.DapperRowMetaObject.cs | 84 + .../Dapper/SqlMapper.DapperTable.cs | 50 + .../Dapper/SqlMapper.DeserializerState.cs | 20 + .../Dapper/SqlMapper.DontMap.cs | 10 + .../Dapper/SqlMapper.GridReader.Async.cs | 254 ++ .../Dapper/SqlMapper.GridReader.cs | 381 ++ .../Dapper/SqlMapper.ICustomQueryParameter.cs | 20 + .../Dapper/SqlMapper.IDataReader.cs | 137 + .../Dapper/SqlMapper.IDynamicParameters.cs | 20 + .../Dapper/SqlMapper.IMemberMap.cs | 39 + .../Dapper/SqlMapper.IParameterCallbacks.cs | 16 + .../Dapper/SqlMapper.IParameterLookup.cs | 16 + .../Dapper/SqlMapper.ITypeHandler.cs | 29 + .../Dapper/SqlMapper.ITypeMap.cs | 46 + .../Dapper/SqlMapper.Identity.cs | 122 + .../Ewide.Core.Data/Dapper/SqlMapper.Link.cs | 57 + .../Dapper/SqlMapper.LiteralToken.cs | 31 + .../Dapper/SqlMapper.Settings.cs | 54 + .../Dapper/SqlMapper.TypeDeserializerCache.cs | 160 + .../Dapper/SqlMapper.TypeHandler.cs | 78 + .../Dapper/SqlMapper.TypeHandlerCache.cs | 47 + .../Ewide.Core.Data/Dapper/SqlMapper.cs | 3543 +++++++++++++++++ .../Dapper/TableValuedParameter.cs | 66 + .../Ewide.Core.Data/Dapper/TypeExtensions.cs | 106 + .../Ewide.Core.Data/Dapper/UdtTypeHandler.cs | 43 + .../Dapper/WrappedDataReader.cs | 20 + .../Ewide.Core.Data/Dapper/WrappedReader.cs | 186 + .../Ewide.Core.Data/Dapper/XmlHandlers.cs | 35 + .../Ewide.Core.Data/Dapper/project.json | 79 + .../DapperExtensions/DapperExtensions.cs | 254 ++ .../DapperExtensionsConfiguration.cs | 119 + .../DapperExtensions/DapperImplementor.cs | 478 +++ .../DapperExtensions/Database.cs | 268 ++ .../DapperExtensions/GetMultiplePredicate.cs | 48 + .../DapperExtensions/GetMultipleResult.cs | 44 + .../Mapper/AutoClassMapper.cs | 20 + .../DapperExtensions/Mapper/ClassMapper.cs | 171 + .../Mapper/PluralizedAutoClassMapper.cs | 67 + .../DapperExtensions/Mapper/PropertyMap.cs | 154 + .../DapperExtensions/Predicates.cs | 393 ++ .../DapperExtensions/ReflectionHelper.cs | 107 + .../DapperExtensions/Sql/DB2Dialect.cs | 138 + .../DapperExtensions/Sql/MySqlDialect.cs | 38 + .../DapperExtensions/Sql/OracleDialect.cs | 80 + .../DapperExtensions/Sql/PostgreSqlDialect.cs | 40 + .../DapperExtensions/Sql/SqlCeDialect.cs | 69 + .../DapperExtensions/Sql/SqlDialectBase.cs | 136 + .../DapperExtensions/Sql/SqlGenerator.cs | 264 ++ .../DapperExtensions/Sql/SqlServerDialect.cs | 146 + .../DapperExtensions/Sql/SqliteDialect.cs | 53 + .../Ewide.Core.Data/DapperHelper.cs | 82 + .../Ewide.Core.Data/Ewide.Core.Data.csproj | 167 + .../Properties/AssemblyInfo.cs | 36 + Api/Ewide.Core/Ewide.Core.Data/app.config | 15 + .../Ewide.Core.Data/packages.config | 13 + .../Ewide.Core.Model/Ewide.Core.Model.csproj | 18 +- Api/Ewide.Core/Ewide.Core.Model/app.config | 37 + .../Ewide.Core.Service.csproj | 6 +- Api/Ewide.Core/Ewide.Core.Service/app.config | 19 + .../Ewide.Core.Utility.csproj | 3 +- .../App_Start/WebApiConfig.cs | 5 + .../Areas/Gate/Controllers/LoginController.cs | 29 - .../Areas/Gate/GateAreaRegistration.cs | 24 - .../Areas/Gate/Views/web.config | 36 - .../Controllers/Code/BaseController.cs | 8 +- .../Code/ValidateArgumentsFilter.cs | 63 + .../Controllers/GateController.cs | 37 + .../Ewide.Core.WebApi.csproj | 25 +- Api/Ewide.Core/Ewide.Core.WebApi/Web.config | 82 +- Api/Ewide.Core/Ewide.Core.sln | 11 +- 93 files changed, 11945 insertions(+), 166 deletions(-) rename Api/Ewide.Core/Ewide.Core.Arguments/{Ewide.Core.Arguments.csproj => Ewide.Core.DTO.csproj} (89%) create mode 100644 Api/Ewide.Core/Ewide.Core.Arguments/TestArgs.cs delete mode 100644 Api/Ewide.Core/Ewide.Core.Common/EnumCode/ResponseStatus.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Common/packages.config create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandDefinition.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandFlags.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/CustomPropertyTypeMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/DataTableHandler.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/DbString.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/DefaultTypeMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.CachedOutputSetters.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.ParamInfo.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/ExplicitConstructorAttribute.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/FeatureSupport.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SimpleMemberMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordHandler.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordListTVPParameter.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Async.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.CacheInfo.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRow.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRowMetaObject.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperTable.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DeserializerState.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DontMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.Async.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ICustomQueryParameter.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDataReader.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDynamicParameters.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IMemberMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterCallbacks.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterLookup.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeHandler.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Identity.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Link.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.LiteralToken.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Settings.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeDeserializerCache.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandler.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandlerCache.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/TableValuedParameter.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/TypeExtensions.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/UdtTypeHandler.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedDataReader.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedReader.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/XmlHandlers.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Dapper/project.json create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensions.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensionsConfiguration.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperImplementor.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Database.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultiplePredicate.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultipleResult.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/AutoClassMapper.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/ClassMapper.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PluralizedAutoClassMapper.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PropertyMap.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Predicates.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/ReflectionHelper.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/DB2Dialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/MySqlDialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/OracleDialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/PostgreSqlDialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlCeDialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlDialectBase.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlGenerator.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlServerDialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqliteDialect.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/DapperHelper.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Ewide.Core.Data.csproj create mode 100644 Api/Ewide.Core/Ewide.Core.Data/Properties/AssemblyInfo.cs create mode 100644 Api/Ewide.Core/Ewide.Core.Data/app.config create mode 100644 Api/Ewide.Core/Ewide.Core.Data/packages.config create mode 100644 Api/Ewide.Core/Ewide.Core.Model/app.config create mode 100644 Api/Ewide.Core/Ewide.Core.Service/app.config delete mode 100644 Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Controllers/LoginController.cs delete mode 100644 Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/GateAreaRegistration.cs delete mode 100644 Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Views/web.config create mode 100644 Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/ValidateArgumentsFilter.cs create mode 100644 Api/Ewide.Core/Ewide.Core.WebApi/Controllers/GateController.cs diff --git a/Api/Ewide.Core/Ewide.Core.Arguments/Ewide.Core.Arguments.csproj b/Api/Ewide.Core/Ewide.Core.Arguments/Ewide.Core.DTO.csproj similarity index 89% rename from Api/Ewide.Core/Ewide.Core.Arguments/Ewide.Core.Arguments.csproj rename to Api/Ewide.Core/Ewide.Core.Arguments/Ewide.Core.DTO.csproj index 0dafae7..1f26cf9 100644 --- a/Api/Ewide.Core/Ewide.Core.Arguments/Ewide.Core.Arguments.csproj +++ b/Api/Ewide.Core/Ewide.Core.Arguments/Ewide.Core.DTO.csproj @@ -9,9 +9,10 @@ Properties Ewide.Core.Arguments Ewide.Core.Arguments - v4.5 + v4.5.2 512 true + true @@ -32,6 +33,7 @@ + @@ -42,6 +44,7 @@ + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Arguments/TestArgs.cs b/Api/Ewide.Core/Ewide.Core.Arguments/TestArgs.cs new file mode 100644 index 0000000..7907a78 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Arguments/TestArgs.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ewide.Core.DTO +{ + public class TestArgs + { + [DisplayName("字段1")] + [Required(AllowEmptyStrings = false, ErrorMessage = "不可为空")] + public string TestFirst { get; set; } + + [DisplayName("字段2")] + [RegularExpression(@"^[0-9]\d*$", ErrorMessage = "必须为数字")] + public int RegField { get; set; } + + [Required] + [StringLength(int.MaxValue, MinimumLength = 5, ErrorMessage = "Name is exceeding the length limit")] + public string TestLength { get; set; } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Common/BaseDisplayJSON.cs b/Api/Ewide.Core/Ewide.Core.Common/BaseDisplayJSON.cs index 84fc088..246188e 100644 --- a/Api/Ewide.Core/Ewide.Core.Common/BaseDisplayJSON.cs +++ b/Api/Ewide.Core/Ewide.Core.Common/BaseDisplayJSON.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -9,6 +10,17 @@ namespace Ewide.Core.Common { public class BaseDisplayJSON { + public static object DisplayJSON(object obj) + { + var _result = JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings + { + ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), + DateFormatString = "yyyy-MM-dd HH:mm:ss" + }); + + return JsonConvert.DeserializeObject(_result); + } + public static object Display(HttpStatusCode status, object result) { return new diff --git a/Api/Ewide.Core/Ewide.Core.Common/EnumCode/ResponseStatus.cs b/Api/Ewide.Core/Ewide.Core.Common/EnumCode/ResponseStatus.cs deleted file mode 100644 index 497d43a..0000000 --- a/Api/Ewide.Core/Ewide.Core.Common/EnumCode/ResponseStatus.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Ewide.Core.Common -{ - public enum ResponseStatus - { - // 权限验证失败 - Unauthorized = 401, - // 未找到 - NotFound = 404, - // 接口错误 - Error = 500, - // 接口成功 - Success = 200 - } -} diff --git a/Api/Ewide.Core/Ewide.Core.Common/Ewide.Core.Common.csproj b/Api/Ewide.Core/Ewide.Core.Common/Ewide.Core.Common.csproj index 472afe3..ccfecec 100644 --- a/Api/Ewide.Core/Ewide.Core.Common/Ewide.Core.Common.csproj +++ b/Api/Ewide.Core/Ewide.Core.Common/Ewide.Core.Common.csproj @@ -9,9 +9,10 @@ Properties Ewide.Core.Common Ewide.Core.Common - v4.5 + v4.5.2 512 true + true @@ -31,6 +32,9 @@ 4 + + ..\packages\Newtonsoft.Json.11.0.1\lib\net45\Newtonsoft.Json.dll + @@ -42,8 +46,13 @@ - + + + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Common/packages.config b/Api/Ewide.Core/Ewide.Core.Common/packages.config new file mode 100644 index 0000000..f7c9995 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Common/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandDefinition.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandDefinition.cs new file mode 100644 index 0000000..1d65e08 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandDefinition.cs @@ -0,0 +1,187 @@ +using System; +using System.Data; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; + +namespace Dapper +{ + /// + /// Represents the key aspects of a sql operation + /// + public struct CommandDefinition + { + internal static CommandDefinition ForCallback(object parameters) + { + if (parameters is DynamicParameters) + { + return new CommandDefinition(parameters); + } + else + { + return default(CommandDefinition); + } + } + + internal void OnCompleted() + { + (Parameters as SqlMapper.IParameterCallbacks)?.OnCompleted(); + } + + /// + /// The command (sql or a stored-procedure name) to execute + /// + public string CommandText { get; } + + /// + /// The parameters associated with the command + /// + public object Parameters { get; } + + /// + /// The active transaction for the command + /// + public IDbTransaction Transaction { get; } + + /// + /// The effective timeout for the command + /// + public int? CommandTimeout { get; } + + /// + /// The type of command that the command-text represents + /// + public CommandType? CommandType { get; } + + /// + /// Should data be buffered before returning? + /// + public bool Buffered => (Flags & CommandFlags.Buffered) != 0; + + /// + /// Should the plan for this query be cached? + /// + internal bool AddToCache => (Flags & CommandFlags.NoCache) == 0; + + /// + /// Additional state flags against this command + /// + public CommandFlags Flags { get; } + + /// + /// Can async queries be pipelined? + /// + public bool Pipelined => (Flags & CommandFlags.Pipelined) != 0; + + /// + /// Initialize the command definition + /// + public CommandDefinition(string commandText, object parameters = null, IDbTransaction transaction = null, int? commandTimeout = null, + CommandType? commandType = null, CommandFlags flags = CommandFlags.Buffered +#if ASYNC + , CancellationToken cancellationToken = default(CancellationToken) +#endif + ) + { + CommandText = commandText; + Parameters = parameters; + Transaction = transaction; + CommandTimeout = commandTimeout; + CommandType = commandType; + Flags = flags; +#if ASYNC + CancellationToken = cancellationToken; +#endif + } + + private CommandDefinition(object parameters) : this() + { + Parameters = parameters; + } + +#if ASYNC + + /// + /// For asynchronous operations, the cancellation-token + /// + public CancellationToken CancellationToken { get; } +#endif + + internal IDbCommand SetupCommand(IDbConnection cnn, Action paramReader) + { + var cmd = cnn.CreateCommand(); + var init = GetInit(cmd.GetType()); + init?.Invoke(cmd); + if (Transaction != null) + cmd.Transaction = Transaction; + cmd.CommandText = CommandText; + if (CommandTimeout.HasValue) + { + cmd.CommandTimeout = CommandTimeout.Value; + } + else if (SqlMapper.Settings.CommandTimeout.HasValue) + { + cmd.CommandTimeout = SqlMapper.Settings.CommandTimeout.Value; + } + if (CommandType.HasValue) + cmd.CommandType = CommandType.Value; + paramReader?.Invoke(cmd, Parameters); + return cmd; + } + + private static SqlMapper.Link> commandInitCache; + + private static Action GetInit(Type commandType) + { + if (commandType == null) + return null; // GIGO + Action action; + if (SqlMapper.Link>.TryGet(commandInitCache, commandType, out action)) + { + return action; + } + var bindByName = GetBasicPropertySetter(commandType, "BindByName", typeof(bool)); + var initialLongFetchSize = GetBasicPropertySetter(commandType, "InitialLONGFetchSize", typeof(int)); + + action = null; + if (bindByName != null || initialLongFetchSize != null) + { + var method = new DynamicMethod(commandType.Name + "_init", null, new Type[] { typeof(IDbCommand) }); + var il = method.GetILGenerator(); + + if (bindByName != null) + { + // .BindByName = true + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, commandType); + il.Emit(OpCodes.Ldc_I4_1); + il.EmitCall(OpCodes.Callvirt, bindByName, null); + } + if (initialLongFetchSize != null) + { + // .InitialLONGFetchSize = -1 + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, commandType); + il.Emit(OpCodes.Ldc_I4_M1); + il.EmitCall(OpCodes.Callvirt, initialLongFetchSize, null); + } + il.Emit(OpCodes.Ret); + action = (Action)method.CreateDelegate(typeof(Action)); + } + // cache it + SqlMapper.Link>.TryAdd(ref commandInitCache, commandType, ref action); + return action; + } + + private static MethodInfo GetBasicPropertySetter(Type declaringType, string name, Type expectedType) + { + var prop = declaringType.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + if (prop != null && prop.CanWrite && prop.PropertyType == expectedType && prop.GetIndexParameters().Length == 0) + { + return prop.GetSetMethod(); + } + return null; + } + } + +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandFlags.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandFlags.cs new file mode 100644 index 0000000..e2589c7 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/CommandFlags.cs @@ -0,0 +1,30 @@ +using System; + +namespace Dapper +{ + + /// + /// Additional state flags that control command behaviour + /// + [Flags] + public enum CommandFlags + { + /// + /// No additional flags + /// + None = 0, + /// + /// Should data be buffered before returning? + /// + Buffered = 1, + /// + /// Can async queries be pipelined? + /// + Pipelined = 2, + /// + /// Should the plan cache be bypassed? + /// + NoCache = 4, + } + +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/CustomPropertyTypeMap.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/CustomPropertyTypeMap.cs new file mode 100644 index 0000000..a1eba87 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/CustomPropertyTypeMap.cs @@ -0,0 +1,74 @@ +using System; +using System.Reflection; + +namespace Dapper +{ + + /// + /// Implements custom property mapping by user provided criteria (usually presence of some custom attribute with column to member mapping) + /// + public sealed class CustomPropertyTypeMap : SqlMapper.ITypeMap + { + private readonly Type _type; + private readonly Func _propertySelector; + + /// + /// Creates custom property mapping + /// + /// Target entity type + /// Property selector based on target type and DataReader column name + public CustomPropertyTypeMap(Type type, Func propertySelector) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (propertySelector == null) + throw new ArgumentNullException(nameof(propertySelector)); + + _type = type; + _propertySelector = propertySelector; + } + + /// + /// Always returns default constructor + /// + /// DataReader column names + /// DataReader column types + /// Default constructor + public ConstructorInfo FindConstructor(string[] names, Type[] types) + { + return _type.GetConstructor(new Type[0]); + } + + /// + /// Always returns null + /// + /// + public ConstructorInfo FindExplicitConstructor() + { + return null; + } + + /// + /// Not implemented as far as default constructor used for all cases + /// + /// + /// + /// + public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) + { + throw new NotSupportedException(); + } + + /// + /// Returns property based on selector strategy + /// + /// DataReader column name + /// Poperty member map + public SqlMapper.IMemberMap GetMember(string columnName) + { + var prop = _propertySelector(_type, columnName); + return prop != null ? new SimpleMemberMap(columnName, prop) : null; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/DataTableHandler.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DataTableHandler.cs new file mode 100644 index 0000000..2e2472e --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DataTableHandler.cs @@ -0,0 +1,19 @@ +using System; +using System.Data; +#if !COREFX +namespace Dapper +{ + sealed class DataTableHandler : SqlMapper.ITypeHandler + { + public object Parse(Type destinationType, object value) + { + throw new NotImplementedException(); + } + + public void SetValue(IDbDataParameter parameter, object value) + { + TableValuedParameter.Set(parameter, value as DataTable, null); + } + } +} +#endif \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/DbString.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DbString.cs new file mode 100644 index 0000000..cd57347 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DbString.cs @@ -0,0 +1,75 @@ +using System; +using System.Data; + +namespace Dapper +{ + /// + /// This class represents a SQL string, it can be used if you need to denote your parameter is a Char vs VarChar vs nVarChar vs nChar + /// + public sealed class DbString : SqlMapper.ICustomQueryParameter + { + /// + /// Default value for IsAnsi. + /// + public static bool IsAnsiDefault { get; set; } + + /// + /// A value to set the default value of strings + /// going through Dapper. Default is 4000, any value larger than this + /// field will not have the default value applied. + /// + public const int DefaultLength = 4000; + + /// + /// Create a new DbString + /// + public DbString() + { + Length = -1; + IsAnsi = IsAnsiDefault; + } + /// + /// Ansi vs Unicode + /// + public bool IsAnsi { get; set; } + /// + /// Fixed length + /// + public bool IsFixedLength { get; set; } + /// + /// Length of the string -1 for max + /// + public int Length { get; set; } + /// + /// The value of the string + /// + public string Value { get; set; } + /// + /// Add the parameter to the command... internal use only + /// + /// + /// + public void AddParameter(IDbCommand command, string name) + { + if (IsFixedLength && Length == -1) + { + throw new InvalidOperationException("If specifying IsFixedLength, a Length must also be specified"); + } + var param = command.CreateParameter(); + param.ParameterName = name; +#pragma warning disable 0618 + param.Value = SqlMapper.SanitizeParameterValue(Value); +#pragma warning restore 0618 + if (Length == -1 && Value != null && Value.Length <= DefaultLength) + { + param.Size = DefaultLength; + } + else + { + param.Size = Length; + } + param.DbType = IsAnsi ? (IsFixedLength ? DbType.AnsiStringFixedLength : DbType.AnsiString) : (IsFixedLength ? DbType.StringFixedLength : DbType.String); + command.Parameters.Add(param); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/DefaultTypeMap.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DefaultTypeMap.cs new file mode 100644 index 0000000..cd24607 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DefaultTypeMap.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Dapper +{ + /// + /// Represents default type mapping strategy used by Dapper + /// + public sealed class DefaultTypeMap : SqlMapper.ITypeMap + { + private readonly List _fields; + private readonly Type _type; + + /// + /// Creates default type map + /// + /// Entity type + public DefaultTypeMap(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + _fields = GetSettableFields(type); + Properties = GetSettableProps(type); + _type = type; + } +#if COREFX + static bool IsParameterMatch(ParameterInfo[] x, ParameterInfo[] y) + { + 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; + } +#endif + internal static MethodInfo GetPropertySetter(PropertyInfo propertyInfo, Type type) + { + if (propertyInfo.DeclaringType == type) return propertyInfo.GetSetMethod(true); +#if COREFX + 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( + propertyInfo.Name, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + Type.DefaultBinder, + propertyInfo.PropertyType, + propertyInfo.GetIndexParameters().Select(p => p.ParameterType).ToArray(), + null).GetSetMethod(true); +#endif + } + + internal static List GetSettableProps(Type t) + { + return t + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(p => GetPropertySetter(p, t) != null) + .ToList(); + } + + internal static List GetSettableFields(Type t) + { + return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); + } + + /// + /// Finds best constructor + /// + /// DataReader column names + /// DataReader column types + /// Matching constructor or default one + 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)) + { + ParameterInfo[] ctorParameters = ctor.GetParameters(); + if (ctorParameters.Length == 0) + return ctor; + + if (ctorParameters.Length != types.Length) + continue; + + int i = 0; + for (; i < ctorParameters.Length; i++) + { + if (!String.Equals(ctorParameters[i].Name, names[i], StringComparison.OrdinalIgnoreCase)) + 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 == typeof(char) && types[i] == typeof(string)) + && !(unboxedType.IsEnum() && types[i] == typeof(string))) + { + break; + } + } + + if (i == ctorParameters.Length) + return ctor; + } + + return null; + } + + /// + /// Returns the constructor, if any, that has the ExplicitConstructorAttribute on it. + /// + public ConstructorInfo FindExplicitConstructor() + { + var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); +#if COREFX + 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) + { + return withAttr[0]; + } + + return null; + } + + /// + /// Gets mapping for constructor parameter + /// + /// Constructor to resolve + /// DataReader column name + /// Mapping implementation + public SqlMapper.IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName) + { + var parameters = constructor.GetParameters(); + + return new SimpleMemberMap(columnName, parameters.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase))); + } + + /// + /// Gets member mapping for column + /// + /// DataReader column name + /// Mapping implementation + public SqlMapper.IMemberMap GetMember(string columnName) + { + var property = Properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) + ?? Properties.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)); + + if (property == null && MatchNamesWithUnderscores) + { + property = Properties.FirstOrDefault(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.Ordinal)) + ?? Properties.FirstOrDefault(p => string.Equals(p.Name, columnName.Replace("_", ""), StringComparison.OrdinalIgnoreCase)); + } + + if (property != 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.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.Ordinal)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, columnName, StringComparison.OrdinalIgnoreCase)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); + + if (field == null && MatchNamesWithUnderscores) + { + var effectiveColumnName = columnName.Replace("_", ""); + backingFieldName = "<" +effectiveColumnName + ">k__BackingField"; + + field = _fields.FirstOrDefault(p => string.Equals(p.Name, effectiveColumnName, StringComparison.Ordinal)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, backingFieldName, StringComparison.Ordinal)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, effectiveColumnName, StringComparison.OrdinalIgnoreCase)) + ?? _fields.FirstOrDefault(p => string.Equals(p.Name, backingFieldName, StringComparison.OrdinalIgnoreCase)); + } + + if (field != null) + return new SimpleMemberMap(columnName, field); + + return null; + } + /// + /// Should column names like User_Id be allowed to match properties/fields like UserId ? + /// + public static bool MatchNamesWithUnderscores { get; set; } + + /// + /// The settable properties for this typemap + /// + public List Properties { get; } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.CachedOutputSetters.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.CachedOutputSetters.cs new file mode 100644 index 0000000..e7697c6 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.CachedOutputSetters.cs @@ -0,0 +1,16 @@ +using System.Collections; + +namespace Dapper +{ + partial class DynamicParameters + { + // The type here is used to differentiate the cache by type via generics + // ReSharper disable once UnusedTypeParameter + internal static class CachedOutputSetters + { + // Intentional, abusing generics to get our cache splits + // ReSharper disable once StaticMemberInGenericType + public static readonly Hashtable Cache = new Hashtable(); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.ParamInfo.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.ParamInfo.cs new file mode 100644 index 0000000..e323cae --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.ParamInfo.cs @@ -0,0 +1,24 @@ +using System; +using System.Data; + +namespace Dapper +{ + partial class DynamicParameters + { + sealed class ParamInfo + { + public string Name { get; set; } + 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; } + internal bool CameFromTemplate { get; set; } + + public byte? Precision { get; set; } + public byte? Scale { get; set; } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.cs new file mode 100644 index 0000000..563c556 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/DynamicParameters.cs @@ -0,0 +1,505 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; + +#if COREFX +using ApplicationException = System.InvalidOperationException; +#endif + +namespace Dapper +{ + + /// + /// A bag of parameters that can be passed to the Dapper Query and Execute methods + /// + public partial class DynamicParameters : SqlMapper.IDynamicParameters, SqlMapper.IParameterLookup, SqlMapper.IParameterCallbacks + { + internal const DbType EnumerableMultiParameter = (DbType)(-1); + static Dictionary> paramReaderCache = new Dictionary>(); + + Dictionary parameters = new Dictionary(); + List templates; + + object SqlMapper.IParameterLookup.this[string member] + { + get + { + ParamInfo param; + return parameters.TryGetValue(member, out param) ? param.Value : null; + } + } + + + /// + /// construct a dynamic parameter bag + /// + public DynamicParameters() + { + RemoveUnused = true; + } + + /// + /// construct a dynamic parameter bag + /// + /// can be an anonymous type or a DynamicParameters bag + public DynamicParameters(object template) + { + RemoveUnused = true; + AddDynamicParams(template); + } + + /// + /// Append a whole object full of params to the dynamic + /// EG: AddDynamicParams(new {A = 1, B = 2}) // will add property A and B to the dynamic + /// + /// + public void AddDynamicParams(object param) + { + var obj = param; + if (obj != null) + { + var subDynamic = obj as DynamicParameters; + if (subDynamic == null) + { + var dictionary = obj as IEnumerable>; + if (dictionary == null) + { + templates = templates ?? new List(); + templates.Add(obj); + } + else + { + foreach (var kvp in dictionary) + { + Add(kvp.Key, kvp.Value, null, null, null); + } + } + } + else + { + if (subDynamic.parameters != null) + { + foreach (var kvp in subDynamic.parameters) + { + parameters.Add(kvp.Key, kvp.Value); + } + } + + if (subDynamic.templates != null) + { + templates = templates ?? new List(); + foreach (var t in subDynamic.templates) + { + templates.Add(t); + } + } + } + } + } + + /// + /// Add a parameter to this dynamic parameter list + /// + public void Add(string name, object value, DbType? dbType, ParameterDirection? direction, int? size) + { + parameters[Clean(name)] = new ParamInfo + { + Name = name, + Value = value, + ParameterDirection = direction ?? ParameterDirection.Input, + DbType = dbType, + Size = size + }; + } + + /// + /// Add a parameter to this dynamic parameter list + /// + 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 + { + Name = name, + Value = value, + ParameterDirection = direction ?? ParameterDirection.Input, + DbType = dbType, + Size = size, + Precision = precision, + Scale = scale + }; + } + + static string Clean(string name) + { + if (!string.IsNullOrEmpty(name)) + { + switch (name[0]) + { + case '@': + case ':': + case '?': + return name.Substring(1); + } + } + return name; + } + + void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + AddParameters(command, identity); + } + + /// + /// If true, the command-text is inspected and only values that are clearly used are included on the connection + /// + public bool RemoveUnused { get; set; } + + /// + /// Add all the parameters needed to the command just before it executes + /// + /// The raw command prior to execution + /// Information about the query + protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + var literals = SqlMapper.GetLiteralTokens(identity.sql); + + if (templates != null) + { + foreach (var template in templates) + { + var newIdent = identity.ForDynamicParameters(template.GetType()); + Action appender; + + lock (paramReaderCache) + { + if (!paramReaderCache.TryGetValue(newIdent, out appender)) + { + appender = SqlMapper.CreateParamInfoGenerator(newIdent, true, RemoveUnused, literals); + paramReaderCache[newIdent] = appender; + } + } + + appender(command, template); + } + + // The parameters were added to the command, but not the + // DynamicParameters until now. + foreach (IDbDataParameter param in command.Parameters) + { + // If someone makes a DynamicParameters with a template, + // then explicitly adds a parameter of a matching name, + // it will already exist in 'parameters'. + if (!parameters.ContainsKey(param.ParameterName)) + { + parameters.Add(param.ParameterName, new ParamInfo + { + AttachedParam = param, + CameFromTemplate = true, + DbType = param.DbType, + Name = param.ParameterName, + ParameterDirection = param.Direction, + Size = param.Size, + Value = param.Value + }); + } + } + + // 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(); + } + } + } + + foreach (var param in parameters.Values) + { + if (param.CameFromTemplate) continue; + + var dbType = param.DbType; + var val = param.Value; + string name = Clean(param.Name); + var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter; + + SqlMapper.ITypeHandler handler = null; + if (dbType == null && val != null && !isCustomQueryParameter) + { +#pragma warning disable 618 + dbType = SqlMapper.LookupDbType(val.GetType(), name, true, out handler); +#pragma warning disable 618 + } + if (isCustomQueryParameter) + { + ((SqlMapper.ICustomQueryParameter)val).AddParameter(command, name); + } + else if (dbType == EnumerableMultiParameter) + { +#pragma warning disable 612, 618 + SqlMapper.PackListParameters(command, name, val); +#pragma warning restore 612, 618 + } + else + { + + bool add = !command.Parameters.Contains(name); + IDbDataParameter p; + if (add) + { + p = command.CreateParameter(); + p.ParameterName = name; + } + else + { + p = (IDbDataParameter)command.Parameters[name]; + } + + p.Direction = param.ParameterDirection; + if (handler == null) + { +#pragma warning disable 0618 + p.Value = SqlMapper.SanitizeParameterValue(val); +#pragma warning restore 0618 + if (dbType != null && p.DbType != dbType) + { + p.DbType = dbType.Value; + } + 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; + } + 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; + handler.SetValue(p, val ?? DBNull.Value); + } + + if (add) + { + command.Parameters.Add(p); + } + param.AttachedParam = p; + } + } + + // note: most non-priveleged implementations would use: this.ReplaceLiterals(command); + if (literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals); + } + + /// + /// All the names of the param in the bag, use Get to yank them out + /// + public IEnumerable ParameterNames => parameters.Select(p => p.Key); + + + /// + /// Get the value of a parameter + /// + /// + /// + /// The value, note DBNull.Value is not returned, instead the value is returned as null + public T Get(string name) + { + var paramInfo = parameters[Clean(name)]; + var attachedParam = paramInfo.AttachedParam; + object val = attachedParam == null ? paramInfo.Value : attachedParam.Value; + if (val == DBNull.Value) + { + if (default(T) != 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 (T)val; + } + + /// + /// Allows you to automatically populate a target property/field from output parameters. It actually + /// creates an InputOutput parameter, so you can still pass data in. + /// + /// + /// The object whose property/field you wish to populate. + /// A MemberExpression targeting a property/field of the target (or descendant thereof.) + /// + /// 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) + { + 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); }; + + // Is it even a MemberExpression? + var lastMemberAccess = expression.Body as MemberExpression; + + if (lastMemberAccess == 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) + { + // It's got to be unboxed + lastMemberAccess = (MemberExpression)((UnaryExpression)expression.Body).Operand; + } + else @throw(); + } + + // Does the chain consist of MemberExpressions leading to a ParameterExpression of type T? + 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(); + + do + { + // Insert the names in the right order so expression + // "Post.Author.Name" becomes parameter "PostAuthorName" + names.Insert(0, diving?.Member.Name); + chain.Insert(0, diving); + + var constant = diving?.Expression as ParameterExpression; + diving = diving?.Expression as MemberExpression; + + if (constant != null && + constant.Type == typeof(T)) + { + break; + } + else if (diving == null || + (!(diving.Member is PropertyInfo) && + !(diving.Member is FieldInfo))) + { + @throw(); + } + } + while (diving != null); + + var dynamicParamName = string.Join(string.Empty, names.ToArray()); + + // Before we get all emitty... + var lookup = string.Join("|", names.ToArray()); + + var cache = CachedOutputSetters.Cache; + var setter = (Action)cache[lookup]; + if (setter != 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); + var il = dm.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); // [object] + il.Emit(OpCodes.Castclass, typeof(T)); // [T] + + // Count - 1 to skip the last member access + var i = 0; + for (; i < (chain.Count - 1); i++) + { + var member = chain[0].Member; + + if (member is PropertyInfo) + { + var get = ((PropertyInfo)member).GetGetMethod(true); + il.Emit(OpCodes.Callvirt, get); // [Member{i}] + } + else // Else it must be a field! + { + il.Emit(OpCodes.Ldfld, ((FieldInfo)member)); // [Member{i}] + } + } + + 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] + il.Emit(OpCodes.Callvirt, paramGetter); // [target] [value], it's already typed thanks to generic method + + // GET READY + var lastMember = lastMemberAccess.Member; + if (lastMember is PropertyInfo) + { + var set = ((PropertyInfo)lastMember).GetSetMethod(true); + il.Emit(OpCodes.Callvirt, set); // SET + } + else + { + il.Emit(OpCodes.Stfld, ((FieldInfo)lastMember)); // SET + } + + il.Emit(OpCodes.Ret); // GO + + setter = (Action)dm.CreateDelegate(typeof(Action)); + lock (cache) + { + cache[lookup] = setter; + } + + // Queue the preparation to be fired off when adding parameters to the DbCommand + MAKECALLBACK: + (outputCallbacks ?? (outputCallbacks = new List())).Add(() => + { + // Finally, prep the parameter and attach the callback to it + ParamInfo parameter; + var targetMemberType = lastMemberAccess?.Type; + int sizeToSet = (!size.HasValue && targetMemberType == typeof(string)) ? DbString.DefaultLength : size ?? 0; + + if (parameters.TryGetValue(dynamicParamName, out parameter)) + { + parameter.ParameterDirection = parameter.AttachedParam.Direction = ParameterDirection.InputOutput; + + if (parameter.AttachedParam.Size == 0) + { + parameter.Size = parameter.AttachedParam.Size = sizeToSet; + } + } + else + { + SqlMapper.ITypeHandler handler; + dbType = (!dbType.HasValue) +#pragma warning disable 618 + ? SqlMapper.LookupDbType(targetMemberType, targetMemberType?.Name, true, out 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); + } + + parameter = parameters[dynamicParamName]; + parameter.OutputCallback = setter; + parameter.OutputTarget = target; + }); + + return this; + } + + private List outputCallbacks; + + void SqlMapper.IParameterCallbacks.OnCompleted() + { + foreach (var param in (from p in parameters select p.Value)) + { + param.OutputCallback?.Invoke(param.OutputTarget, this); + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/ExplicitConstructorAttribute.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/ExplicitConstructorAttribute.cs new file mode 100644 index 0000000..d801af3 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/ExplicitConstructorAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Dapper +{ + /// + /// Tell Dapper to use an explicit constructor, passing nulls or 0s for all parameters + /// + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false)] + public sealed class ExplicitConstructorAttribute : Attribute + { + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/FeatureSupport.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/FeatureSupport.cs new file mode 100644 index 0000000..4133860 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/FeatureSupport.cs @@ -0,0 +1,33 @@ +using System; +using System.Data; + +namespace Dapper +{ + /// + /// Handles variances in features per DBMS + /// + class FeatureSupport + { + private static readonly FeatureSupport + Default = new FeatureSupport(false), + Postgres = new FeatureSupport(true); + + /// + /// Gets the feature set based on the passed connection + /// + public static FeatureSupport Get(IDbConnection connection) + { + string name = connection?.GetType().Name; + if (string.Equals(name, "npgsqlconnection", StringComparison.OrdinalIgnoreCase)) return Postgres; + return Default; + } + private FeatureSupport(bool arrays) + { + Arrays = arrays; + } + /// + /// True if the db supports array columns e.g. Postgresql + /// + public bool Arrays { get; } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SimpleMemberMap.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SimpleMemberMap.cs new file mode 100644 index 0000000..3c96855 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SimpleMemberMap.cs @@ -0,0 +1,87 @@ +using System; +using System.Reflection; + +namespace Dapper +{ + /// + /// Represents simple member map for one of target parameter or property or field to source DataReader column + /// + sealed class SimpleMemberMap : SqlMapper.IMemberMap + { + /// + /// Creates instance for simple property mapping + /// + /// DataReader column name + /// Target property + public SimpleMemberMap(string columnName, PropertyInfo property) + { + if (columnName == null) + throw new ArgumentNullException(nameof(columnName)); + + if (property == null) + throw new ArgumentNullException(nameof(property)); + + ColumnName = columnName; + Property = property; + } + + /// + /// Creates instance for simple field mapping + /// + /// DataReader column name + /// Target property + public SimpleMemberMap(string columnName, FieldInfo field) + { + if (columnName == null) + throw new ArgumentNullException(nameof(columnName)); + + if (field == null) + throw new ArgumentNullException(nameof(field)); + + ColumnName = columnName; + Field = field; + } + + /// + /// Creates instance for simple constructor parameter mapping + /// + /// DataReader column name + /// Target constructor parameter + public SimpleMemberMap(string columnName, ParameterInfo parameter) + { + if (columnName == null) + throw new ArgumentNullException(nameof(columnName)); + + if (parameter == null) + throw new ArgumentNullException(nameof(parameter)); + + ColumnName = columnName; + Parameter = parameter; + } + + /// + /// DataReader column name + /// + public string ColumnName { get; } + + /// + /// Target member type + /// + public Type MemberType => Field?.FieldType ?? Property?.PropertyType ?? Parameter?.ParameterType; + + /// + /// Target property + /// + public PropertyInfo Property { get; } + + /// + /// Target field + /// + public FieldInfo Field { get; } + + /// + /// Target constructor parameter + /// + public ParameterInfo Parameter { get; } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordHandler.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordHandler.cs new file mode 100644 index 0000000..4034771 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordHandler.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Data; + +#if !COREFX +namespace Dapper +{ + sealed class SqlDataRecordHandler : SqlMapper.ITypeHandler + { + public object Parse(Type destinationType, object value) + { + throw new NotSupportedException(); + } + + public void SetValue(IDbDataParameter parameter, object value) + { + SqlDataRecordListTVPParameter.Set(parameter, value as IEnumerable, null); + } + } +} +#endif \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordListTVPParameter.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordListTVPParameter.cs new file mode 100644 index 0000000..e17cb8c --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlDataRecordListTVPParameter.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; +#if !COREFX +namespace Dapper +{ + /// + /// Used to pass a IEnumerable<SqlDataRecord> as a SqlDataRecordListTVPParameter + /// + sealed class SqlDataRecordListTVPParameter : SqlMapper.ICustomQueryParameter + { + private readonly IEnumerable data; + private readonly string typeName; + /// + /// Create a new instance of SqlDataRecordListTVPParameter + /// + public SqlDataRecordListTVPParameter(IEnumerable data, string typeName) + { + this.data = data; + this.typeName = typeName; + } + static readonly Action setTypeName; + static SqlDataRecordListTVPParameter() + { + var prop = typeof(System.Data.SqlClient.SqlParameter).GetProperty(nameof(System.Data.SqlClient.SqlParameter.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(); + param.ParameterName = name; + Set(param, data, typeName); + command.Parameters.Add(param); + } + internal static void Set(IDbDataParameter parameter, IEnumerable data, string typeName) + { + parameter.Value = (object)data ?? DBNull.Value; + var sqlParam = parameter as System.Data.SqlClient.SqlParameter; + if (sqlParam != null) + { + sqlParam.SqlDbType = SqlDbType.Structured; + sqlParam.TypeName = typeName; + } + } + } +} +#endif \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Async.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Async.cs new file mode 100644 index 0000000..5be5815 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Async.cs @@ -0,0 +1,907 @@ +#if ASYNC +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Dapper +{ + public static partial class SqlMapper + { + /// + /// Execute a query asynchronously using .NET 4.5 Task. + /// + /// 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) + { + return QueryAsync(cnn, typeof(DapperRow), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + } + + /// + /// Execute a query asynchronously using .NET 4.5 Task. + /// + /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command) + { + return QueryAsync(cnn, typeof(DapperRow), command); + } + + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> + public static Task QueryFirstAsync(this IDbConnection cnn, CommandDefinition command) + { + return QueryRowAsync(cnn, Row.First, typeof(DapperRow), command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + /// 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) + { + return QueryRowAsync(cnn, Row.FirstOrDefault, typeof(DapperRow), command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + /// Note: the row can be accessed via "dynamic", or by casting to an IDictionary<string,object> + public static Task QuerySingleAsync(this IDbConnection cnn, CommandDefinition command) + { + return QueryRowAsync(cnn, Row.Single, typeof(DapperRow), command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + /// 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) + { + return QueryRowAsync(cnn, Row.SingleOrDefault, typeof(DapperRow), command); + } + + /// + /// Execute a query asynchronously using .NET 4.5 Task. + /// + public static Task> QueryAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + return QueryAsync(cnn, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + } + + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QueryFirstAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + return QueryRowAsync(cnn, Row.First, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + return QueryRowAsync(cnn, Row.FirstOrDefault, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QuerySingleAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + return QueryRowAsync(cnn, Row.Single, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + return QueryRowAsync(cnn, Row.SingleOrDefault, typeof(T), new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None, default(CancellationToken))); + } + + /// + /// Execute a query asynchronously using .NET 4.5 Task. + /// + 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))); + } + + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + 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))); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + 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))); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + 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))); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + 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))); + } + + /// + /// Execute a query asynchronously using .NET 4.5 Task. + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command) + { + return QueryAsync(cnn, typeof(T), command); + } + + /// + /// Execute a query asynchronously using .NET 4.5 Task. + /// + public static Task> QueryAsync(this IDbConnection cnn, Type type, CommandDefinition command) + { + return QueryAsync(cnn, type, command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QueryFirstAsync(this IDbConnection cnn, Type type, CommandDefinition command) + { + return QueryRowAsync(cnn, Row.First, type, command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QueryFirstOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command) + { + return QueryRowAsync(cnn, Row.FirstOrDefault, type, command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QuerySingleAsync(this IDbConnection cnn, Type type, CommandDefinition command) + { + return QueryRowAsync(cnn, Row.Single, type, command); + } + /// + /// Execute a single-row query asynchronously using .NET 4.5 Task. + /// + public static Task QuerySingleOrDefaultAsync(this IDbConnection cnn, Type type, CommandDefinition command) + { + return QueryRowAsync(cnn, Row.SingleOrDefault, type, 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 && DisableCommandBehaviorOptimizations(behavior, task.Exception.InnerException)) + { // we can retry; this time it will have different flags + task = cmd.ExecuteReaderAsync(GetBehavior(wasClosed, behavior), cancellationToken); + } + return task; + } + 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); + 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)) + { + 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); + + 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; + + if (command.Buffered) + { + List 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)) { } + command.OnCompleted(); + return buffer; + } + else + { + // 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); + 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)) + { + 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); + + 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; + + 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)) { } + } + else if ((row & Row.FirstOrDefault) == 0) // demanding a row, and don't have one + { + ThrowZeroRows(row); + } + while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { } + return result; + } + finally + { + using (reader) { } // dispose if non-null + if (wasClosed) cnn.Close(); + } + } + } + + /// + /// Execute a command asynchronously using .NET 4.5 Task. + /// + public static Task ExecuteAsync(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null) + { + return ExecuteAsync(cnn, new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered, default(CancellationToken))); + } + + /// + /// Execute a command asynchronously using .NET 4.5 Task. + /// + public static Task ExecuteAsync(this IDbConnection cnn, CommandDefinition command) + { + object param = command.Parameters; + IEnumerable multiExec = GetMultiExec(param); + if (multiExec != null) + { + return ExecuteMultiImplAsync(cnn, command, multiExec); + } + else + { + return ExecuteImplAsync(cnn, command, param); + } + } + + private struct AsyncExecState + { + public readonly DbCommand Command; + public readonly Task Task; + public AsyncExecState(DbCommand command, Task task) + { + Command = command; + Task = task; + } + } + private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandDefinition command, IEnumerable multiExec) + { + bool isFirst = true; + int total = 0; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); + + 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; + try + { + foreach (var obj in multiExec) + { + if (isFirst) + { + isFirst = false; + cmd = (DbCommand)command.SetupCommand(cnn, null); + masterSql = cmd.CommandText; + var identity = new Identity(command.CommandText, cmd.CommandType, cnn, null, obj.GetType(), null); + info = GetCacheInfo(identity, obj, command.AddToCache); + } else if(pending.Count >= MAX_PENDING) + { + var recycled = pending.Dequeue(); + total += await recycled.Task.ConfigureAwait(false); + cmd = recycled.Command; + cmd.CommandText = masterSql; // because we do magic replaces on "in" etc + cmd.Parameters.Clear(); // current code is Add-tastic + } + else + { + cmd = (DbCommand)command.SetupCommand(cnn, null); + } + info.ParamReader(cmd, obj); + + var task = cmd.ExecuteNonQueryAsync(command.CancellationToken); + pending.Enqueue(new AsyncExecState(cmd, task)); + cmd = null; // note the using in the finally: this avoids a double-dispose + } + while (pending.Count != 0) + { + var pair = pending.Dequeue(); + using (pair.Command) { } // dispose commands + total += await pair.Task.ConfigureAwait(false); + } + } finally + { + // this only has interesting work to do if there are failures + using (cmd) { } // dispose commands + while (pending.Count != 0) + { // dispose tasks even in failure + using (pending.Dequeue().Command) { } // dispose commands + } + } + } + else + { + using (var cmd = (DbCommand)command.SetupCommand(cnn, null)) + { + foreach (var obj in multiExec) + { + 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); + } + } + } + + command.OnCompleted(); + } + finally + { + if (wasClosed) cnn.Close(); + } + return total; + } + 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 info = GetCacheInfo(identity, param, command.AddToCache); + bool wasClosed = cnn.State == ConnectionState.Closed; + using (var cmd = (DbCommand)command.SetupCommand(cnn, info.ParamReader)) + { + 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(); + } + } + } + + /// + /// Maps a query to objects + /// + /// The first type in the recordset + /// The second type in the recordset + /// The return type + /// + /// + /// + /// + /// + /// + /// The field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// Is it a stored proc or a batch? + /// + 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) + { + return MultiMapAsync(cnn, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + } + + /// + /// Maps a query to objects + /// + /// The first type in the recordset + /// The second type in the recordset + /// The return type + /// + /// The field we should split and read the second object from (default: id) + /// The command to execute + /// + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") + { + return MultiMapAsync(cnn, command, map, splitOn); + } + + /// + /// Maps a query to objects + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// + /// + 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) + { + return MultiMapAsync(cnn, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + } + + /// + /// Maps a query to objects + /// + /// + /// + /// + /// + /// + /// The field we should split and read the second object from (default: id) + /// The command to execute + /// + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") + { + return MultiMapAsync(cnn, command, map, splitOn); + } + + /// + /// Perform a multi mapping query with 4 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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) + { + return MultiMapAsync(cnn, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + } + + /// + /// Perform a multi mapping query with 4 input parameters + /// + /// + /// + /// + /// + /// + /// + /// The field we should split and read the second object from (default: id) + /// The command to execute + /// + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") + { + return MultiMapAsync(cnn, command, map, splitOn); + } + + /// + /// Perform a multi mapping query with 5 input parameters + /// + 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) + { + return MultiMapAsync(cnn, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + } + + /// + /// Perform a multi mapping query with 5 input parameters + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") + { + return MultiMapAsync(cnn, command, map, splitOn); + } + + /// + /// Perform a multi mapping query with 6 input parameters + /// + 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) + { + return MultiMapAsync(cnn, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + } + + /// + /// Perform a multi mapping query with 6 input parameters + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") + { + return MultiMapAsync(cnn, command, map, splitOn); + } + + /// + /// Perform a multi mapping query with 7 input parameters + /// + 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) + { + return MultiMapAsync(cnn, + new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None, default(CancellationToken)), map, splitOn); + } + + /// + /// Perform a multi mapping query with 7 input parameters + /// + public static Task> QueryAsync(this IDbConnection cnn, CommandDefinition command, Func map, string splitOn = "Id") + { + return 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) }); + 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; + } + } finally + { + if (wasClosed) cnn.Close(); + } + } + + /// + /// Perform a multi mapping query with arbitrary input parameters + /// + /// The return type + /// + /// + /// array of types in the recordset + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// Is it a stored proc or a batch? + /// + 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)); + return MultiMapAsync(cnn, command, types, map, splitOn); + } + + private static async Task> MultiMapAsync(this IDbConnection cnn, CommandDefinition command, Type[] types, Func map, string splitOn) + { + if (types.Length < 1) + { + 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); + 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; + } + } + finally { + if (wasClosed) cnn.Close(); + } + } + + private static IEnumerable ExecuteReaderSync(IDataReader reader, Func func, object parameters) + { + using (reader) + { + while (reader.Read()) + { + yield return (T)func(reader); + } + while (reader.NextResult()) { } + (parameters as IParameterCallbacks)?.OnCompleted(); + } + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static Task QueryMultipleAsync( + 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 QueryMultipleAsync(cnn, command); + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static async Task QueryMultipleAsync(this IDbConnection cnn, CommandDefinition command) + { + object param = command.Parameters; + Identity identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType(), null); + CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); + + DbCommand cmd = null; + IDataReader 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); + 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); + wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader + // 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 + return result; + } + catch + { + if (reader != null) + { + if (!reader.IsClosed) + try + { cmd.Cancel(); } + catch + { /* don't spoil the existing exception */ } + reader.Dispose(); + } + cmd?.Dispose(); + if (wasClosed) cnn.Close(); + throw; + } + } + + + /// + /// Execute parameterized SQL and return an + /// + /// An that can be used to iterate over the results of the SQL query. + /// + /// This is typically used when the results of a query are not processed by Dapper, for example, used to fill a + /// or . + /// + /// + /// + /// + /// + /// + public static Task ExecuteReaderAsync( + 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 ExecuteReaderImplAsync(cnn, command); + } + + /// + /// Execute parameterized SQL and return an + /// + /// An that can be used to iterate over the results of the SQL query. + /// + /// This is typically used when the results of a query are not processed by Dapper, for example, used to fill a + /// or . + /// + public static Task ExecuteReaderAsync(this IDbConnection cnn, CommandDefinition command) + { + return ExecuteReaderImplAsync(cnn, command); + } + + private static async Task ExecuteReaderImplAsync(IDbConnection cnn, CommandDefinition command) + { + Action paramReader = GetParameterReader(cnn, ref command); + + DbCommand cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + cmd = (DbCommand)command.SetupCommand(cnn, paramReader); + if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); + var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.Default, command.CancellationToken).ConfigureAwait(false); + wasClosed = false; + return reader; + } + finally + { + if (wasClosed) cnn.Close(); + cmd?.Dispose(); + } + } + + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + public static Task ExecuteScalarAsync( + 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 ExecuteScalarImplAsync(cnn, command); + } + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + public static Task ExecuteScalarAsync( + 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 ExecuteScalarImplAsync(cnn, command); + } + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + public static Task ExecuteScalarAsync(this IDbConnection cnn, CommandDefinition command) + { + return ExecuteScalarImplAsync(cnn, command); + } + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + public static Task ExecuteScalarAsync(this IDbConnection cnn, CommandDefinition command) + { + return ExecuteScalarImplAsync(cnn, command); + } + + private static async Task ExecuteScalarImplAsync(IDbConnection cnn, CommandDefinition command) + { + Action paramReader = null; + object param = command.Parameters; + if (param != null) + { + var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; + } + + DbCommand cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + object result; + try + { + cmd = (DbCommand)command.SetupCommand(cnn, paramReader); + if (wasClosed) await ((DbConnection)cnn).OpenAsync(command.CancellationToken).ConfigureAwait(false); + result = await cmd.ExecuteScalarAsync(command.CancellationToken).ConfigureAwait(false); + command.OnCompleted(); + } + finally + { + if (wasClosed) cnn.Close(); + cmd?.Dispose(); + } + return Parse(result); + } + } +} +#endif \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.CacheInfo.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.CacheInfo.cs new file mode 100644 index 0000000..1207d25 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.CacheInfo.cs @@ -0,0 +1,19 @@ +using System; +using System.Data; +using System.Threading; + +namespace Dapper +{ + partial class SqlMapper + { + class CacheInfo + { + public DeserializerState Deserializer { 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/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRow.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRow.cs new file mode 100644 index 0000000..d6871d5 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRow.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Dapper +{ + partial class SqlMapper + { + private sealed class DapperRow + : System.Dynamic.IDynamicMetaObjectProvider + , IDictionary + { + readonly DapperTable table; + object[] values; + + public DapperRow(DapperTable table, object[] values) + { + if (table == null) throw new ArgumentNullException(nameof(table)); + if (values == null) throw new ArgumentNullException(nameof(values)); + this.table = table; + this.values = values; + } + private sealed class DeadValue + { + public static readonly DeadValue Default = new DeadValue(); + private DeadValue() { } + } + int ICollection>.Count + { + get + { + int count = 0; + for (int i = 0; i < values.Length; i++) + { + if (!(values[i] is DeadValue)) count++; + } + return count; + } + } + + public bool TryGetValue(string name, out object value) + { + var index = table.IndexOfName(name); + if (index < 0) + { // doesn't exist + value = null; + return false; + } + // exists, **even if** we don't have a value; consider table rows heterogeneous + value = index < values.Length ? values[index] : null; + if (value is DeadValue) + { // pretend it isn't here + value = null; + return false; + } + return true; + } + + public override string ToString() + { + var sb = GetStringBuilder().Append("{DapperRow"); + foreach (var kv in this) + { + var value = kv.Value; + sb.Append(", ").Append(kv.Key); + if (value != null) + { + sb.Append(" = '").Append(kv.Value).Append('\''); + } + else + { + sb.Append(" = NULL"); + } + } + + 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); + } + + public IEnumerator> GetEnumerator() + { + var names = table.FieldNames; + for (var i = 0; i < names.Length; i++) + { + object value = i < values.Length ? values[i] : null; + if (!(value is DeadValue)) + { + yield return new KeyValuePair(names[i], value); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #region Implementation of ICollection> + + void ICollection>.Add(KeyValuePair item) + { + IDictionary dic = this; + dic.Add(item.Key, item.Value); + } + + 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) + { + object value; + return TryGetValue(item.Key, out value) && Equals(value, item.Value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kv in this) + { + array[arrayIndex++] = kv; // if they didn't leave enough space; not our fault + } + } + + bool ICollection>.Remove(KeyValuePair item) + { + IDictionary dic = this; + return dic.Remove(item.Key); + } + + bool ICollection>.IsReadOnly => false; + #endregion + + #region Implementation of IDictionary + + 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) + { + SetValue(key, value, true); + } + + bool IDictionary.Remove(string key) + { + 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] + { + get { object val; TryGetValue(key, out val); return val; } + set { SetValue(key, value, false); } + } + + public object SetValue(string key, object value) + { + return SetValue(key, value, false); + } + + private object SetValue(string key, object value, bool isAdd) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + int index = table.IndexOfName(key); + if (index < 0) + { + index = table.AddField(key); + } + else if (isAdd && index < values.Length && !(values[index] is DeadValue)) + { + // then semantically, this value already exists + throw new ArgumentException("An item with the same key has already been added", nameof(key)); + } + int oldLength = values.Length; + if (oldLength <= index) + { + // we'll assume they're doing lots of things, and + // grow it to the full width of the table + Array.Resize(ref values, table.FieldCount); + for (int i = oldLength; i < values.Length; i++) + { + values[i] = DeadValue.Default; + } + } + return values[index] = value; + } + + ICollection IDictionary.Keys + { + get { return this.Select(kv => kv.Key).ToArray(); } + } + + ICollection IDictionary.Values + { + get { return this.Select(kv => kv.Value).ToArray(); } + } + + #endregion + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRowMetaObject.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRowMetaObject.cs new file mode 100644 index 0000000..3401e42 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperRowMetaObject.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +namespace Dapper +{ + partial class SqlMapper + { + sealed class DapperRowMetaObject : System.Dynamic.DynamicMetaObject + { + static readonly MethodInfo getValueMethod = typeof(IDictionary).GetProperty("Item").GetGetMethod(); + static readonly MethodInfo setValueMethod = typeof(DapperRow).GetMethod("SetValue", new Type[] { typeof(string), typeof(object) }); + + public DapperRowMetaObject( + System.Linq.Expressions.Expression expression, + System.Dynamic.BindingRestrictions restrictions + ) + : base(expression, restrictions) + { + } + + public DapperRowMetaObject( + System.Linq.Expressions.Expression expression, + System.Dynamic.BindingRestrictions restrictions, + object value + ) + : base(expression, restrictions, value) + { + } + + System.Dynamic.DynamicMetaObject CallMethod( + MethodInfo method, + System.Linq.Expressions.Expression[] parameters + ) + { + var callMethod = new System.Dynamic.DynamicMetaObject( + System.Linq.Expressions.Expression.Call( + System.Linq.Expressions.Expression.Convert(Expression, LimitType), + method, + parameters), + System.Dynamic.BindingRestrictions.GetTypeRestriction(Expression, LimitType) + ); + return callMethod; + } + + public override System.Dynamic.DynamicMetaObject BindGetMember(System.Dynamic.GetMemberBinder binder) + { + var parameters = new System.Linq.Expressions.Expression[] + { + System.Linq.Expressions.Expression.Constant(binder.Name) + }; + + var callMethod = CallMethod(getValueMethod, parameters); + + return callMethod; + } + + // Needed for Visual basic dynamic support + public override System.Dynamic.DynamicMetaObject BindInvokeMember(System.Dynamic.InvokeMemberBinder binder, System.Dynamic.DynamicMetaObject[] args) + { + var parameters = new System.Linq.Expressions.Expression[] + { + System.Linq.Expressions.Expression.Constant(binder.Name) + }; + + var callMethod = CallMethod(getValueMethod, parameters); + + return callMethod; + } + + public override System.Dynamic.DynamicMetaObject BindSetMember(System.Dynamic.SetMemberBinder binder, System.Dynamic.DynamicMetaObject value) + { + var parameters = new System.Linq.Expressions.Expression[] + { + System.Linq.Expressions.Expression.Constant(binder.Name), + value.Expression, + }; + + var callMethod = CallMethod(setValueMethod, parameters); + + return callMethod; + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperTable.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperTable.cs new file mode 100644 index 0000000..a8ce2da --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DapperTable.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; + +namespace Dapper +{ + partial class SqlMapper + { + private sealed class DapperTable + { + string[] fieldNames; + readonly Dictionary fieldNameLookup; + + internal string[] FieldNames => fieldNames; + + public DapperTable(string[] fieldNames) + { + if (fieldNames == null) throw new ArgumentNullException(nameof(fieldNames)); + this.fieldNames = fieldNames; + + fieldNameLookup = new Dictionary(fieldNames.Length, StringComparer.Ordinal); + // if there are dups, we want the **first** key to be the "winner" - so iterate backwards + for (int i = fieldNames.Length - 1; i >= 0; i--) + { + string key = fieldNames[i]; + if (key != null) fieldNameLookup[key] = i; + } + } + + internal int IndexOfName(string name) + { + int result; + return (name != null && fieldNameLookup.TryGetValue(name, out result)) ? result : -1; + } + internal int AddField(string name) + { + if (name == 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 + fieldNames[oldLen] = name; + fieldNameLookup[name] = oldLen; + return oldLen; + } + + internal bool FieldExists(string key) => key != null && fieldNameLookup.ContainsKey(key); + + public int FieldCount => fieldNames.Length; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DeserializerState.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DeserializerState.cs new file mode 100644 index 0000000..ccf11eb --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DeserializerState.cs @@ -0,0 +1,20 @@ +using System; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + struct DeserializerState + { + public readonly int Hash; + public readonly Func Func; + + public DeserializerState(int hash, Func func) + { + Hash = hash; + Func = func; + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DontMap.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DontMap.cs new file mode 100644 index 0000000..404a7ea --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.DontMap.cs @@ -0,0 +1,10 @@ +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Dummy type for excluding from multi-map + /// + class DontMap { } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.Async.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.Async.cs new file mode 100644 index 0000000..74ba904 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.Async.cs @@ -0,0 +1,254 @@ +#if ASYNC +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Dapper +{ + partial class SqlMapper + { + partial class GridReader + { + 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 + /// + /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> + public Task> ReadAsync(bool buffered = true) + { + return ReadAsyncImpl(typeof(DapperRow), buffered); + } + + /// + /// 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 ReadFirstAsync() + { + return ReadRowAsyncImpl(typeof(DapperRow), Row.First); + } + /// + /// 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() + { + return ReadRowAsyncImpl(typeof(DapperRow), Row.FirstOrDefault); + } + /// + /// 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 ReadSingleAsync() + { + return ReadRowAsyncImpl(typeof(DapperRow), Row.Single); + } + /// + /// 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() + { + return ReadRowAsyncImpl(typeof(DapperRow), Row.SingleOrDefault); + } + + /// + /// Read the next grid of results + /// + public Task> ReadAsync(Type type, bool buffered = true) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadAsyncImpl(type, buffered); + } + + /// + /// Read an individual row of the next grid of results + /// + public Task ReadFirstAsync(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRowAsyncImpl(type, Row.First); + } + /// + /// Read an individual row of the next grid of results + /// + public Task ReadFirstOrDefaultAsync(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRowAsyncImpl(type, Row.FirstOrDefault); + } + /// + /// Read an individual row of the next grid of results + /// + public Task ReadSingleAsync(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRowAsyncImpl(type, Row.Single); + } + /// + /// Read an individual row of the next grid of results + /// + public Task ReadSingleOrDefaultAsync(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRowAsyncImpl(type, Row.SingleOrDefault); + } + + /// + /// Read the next grid of results + /// + public Task> ReadAsync(bool buffered = true) + { + return ReadAsyncImpl(typeof(T), buffered); + } + + /// + /// Read an individual row of the next grid of results + /// + public Task ReadFirstAsync() + { + return ReadRowAsyncImpl(typeof(T), Row.First); + } + /// + /// Read an individual row of the next grid of results + /// + public Task ReadFirstOrDefaultAsync() + { + return ReadRowAsyncImpl(typeof(T), Row.FirstOrDefault); + } + /// + /// Read an individual row of the next grid of results + /// + public Task ReadSingleAsync() + { + return ReadRowAsyncImpl(typeof(T), Row.Single); + } + /// + /// Read an individual row of the next grid of results + /// + public Task ReadSingleOrDefaultAsync() + { + return ReadRowAsyncImpl(typeof(T), Row.SingleOrDefault); + } + + private async Task NextResultAsync() + { + if (await ((DbDataReader)reader).NextResultAsync(cancel).ConfigureAwait(false)) + { + readCount++; + gridIndex++; + IsConsumed = false; + } + else + { + // happy path; close the reader cleanly - no + // need for "Cancel" etc + reader.Dispose(); + reader = null; + callbacks?.OnCompleted(); + Dispose(); + } + } + + 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) + { + 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, typedIdentity); + } + else + { + var result = ReadDeferred(gridIndex, deserializer.Func, typedIdentity, type); + if (buffered) result = result.ToList(); // for the "not a DbDataReader" scenario + return Task.FromResult(result); + } + } + + private Task ReadRowAsyncImpl(Type type, Row row) + { + var dbReader = reader as DbDataReader; + if (dbReader != null) return ReadRowAsyncImplViaDbReader(dbReader, type, row); + + // no async API available; use non-async and fake it + return Task.FromResult(ReadRow(type, row)); + } + + private async Task ReadRowAsyncImplViaDbReader(DbDataReader reader, 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; + T result = default(T); + if (await reader.ReadAsync(cancel).ConfigureAwait(false) && reader.FieldCount != 0) + { + 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) + { + deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); + cache.Deserializer = deserializer; + } + result = (T)deserializer.Func(reader); + if ((row & Row.Single) != 0 && await reader.ReadAsync(cancel).ConfigureAwait(false)) ThrowMultipleRows(row); + while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { } + } + else if ((row & Row.FirstOrDefault) == 0) // demanding a row, and don't have one + { + ThrowZeroRows(row); + } + await NextResultAsync().ConfigureAwait(false); + return result; + } + + private async Task> ReadBufferedAsync(int index, Func deserializer, Identity typedIdentity) + { + try + { + var reader = (DbDataReader)this.reader; + List buffer = new List(); + while (index == gridIndex && await reader.ReadAsync(cancel).ConfigureAwait(false)) + { + buffer.Add((T)deserializer(reader)); + } + return buffer; + } + finally // finally so that First etc progresses things even when multiple rows + { + if (index == gridIndex) + { + await NextResultAsync().ConfigureAwait(false); + } + } + } + } + + } +} +#endif \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.cs new file mode 100644 index 0000000..71f8d2a --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.GridReader.cs @@ -0,0 +1,381 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Globalization; +namespace Dapper +{ + partial class SqlMapper + { + /// + /// The grid reader provides interfaces for reading multiple result sets from a Dapper query + /// + public partial class GridReader : IDisposable + { + private IDataReader reader; + private Identity identity; + private bool addToCache; + + internal GridReader(IDbCommand command, IDataReader reader, Identity identity, IParameterCallbacks callbacks, bool addToCache) + { + Command = command; + this.reader = reader; + this.identity = identity; + this.callbacks = callbacks; + this.addToCache = addToCache; + } + + /// + /// Read the next grid of results, returned as a dynamic object + /// + /// Note: each row can be accessed via "dynamic", or by casting to an IDictionary<string,object> + public IEnumerable Read(bool buffered = true) + { + return ReadImpl(typeof(DapperRow), buffered); + } + + /// + /// 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 ReadFirst() + { + return ReadRow(typeof(DapperRow), Row.First); + } + /// + /// 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() + { + return ReadRow(typeof(DapperRow), Row.FirstOrDefault); + } + /// + /// 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 ReadSingle() + { + return ReadRow(typeof(DapperRow), Row.Single); + } + /// + /// 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() + { + return ReadRow(typeof(DapperRow), Row.SingleOrDefault); + } + + /// + /// Read the next grid of results + /// + public IEnumerable Read(bool buffered = true) + { + return ReadImpl(typeof(T), buffered); + } + + /// + /// Read an individual row of the next grid of results + /// + public T ReadFirst() + { + return ReadRow(typeof(T), Row.First); + } + /// + /// Read an individual row of the next grid of results + /// + public T ReadFirstOrDefault() + { + return ReadRow(typeof(T), Row.FirstOrDefault); + } + /// + /// Read an individual row of the next grid of results + /// + public T ReadSingle() + { + return ReadRow(typeof(T), Row.Single); + } + /// + /// Read an individual row of the next grid of results + /// + public T ReadSingleOrDefault() + { + return ReadRow(typeof(T), Row.SingleOrDefault); + } + + /// + /// Read the next grid of results + /// + public IEnumerable Read(Type type, bool buffered = true) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadImpl(type, buffered); + } + + /// + /// Read an individual row of the next grid of results + /// + public object ReadFirst(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRow(type, Row.First); + } + /// + /// Read an individual row of the next grid of results + /// + public object ReadFirstOrDefault(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRow(type, Row.FirstOrDefault); + } + /// + /// Read an individual row of the next grid of results + /// + public object ReadSingle(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRow(type, Row.Single); + } + /// + /// Read an individual row of the next grid of results + /// + public object ReadSingleOrDefault(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + return ReadRow(type, Row.SingleOrDefault); + } + + private IEnumerable ReadImpl(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) + { + deserializer = new DeserializerState(hash, GetDeserializer(type, reader, 0, -1, false)); + cache.Deserializer = deserializer; + } + IsConsumed = true; + var result = ReadDeferred(gridIndex, deserializer.Func, typedIdentity, 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; + + T result = default(T); + if(reader.Read() && reader.FieldCount != 0) + { + 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) + { + 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); + } + if ((row & Row.Single) != 0 && reader.Read()) ThrowMultipleRows(row); + while (reader.Read()) { } + } + else if((row & Row.FirstOrDefault) == 0) // demanding a row, and don't have one + { + ThrowZeroRows(row); + } + NextResult(); + 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); + try + { + foreach (var r in MultiMapImpl(null, default(CommandDefinition), func, splitOn, reader, identity, false)) + { + yield return r; + } + } + finally + { + NextResult(); + } + } + + private IEnumerable MultiReadInternal(Type[] types, Func map, string splitOn) + { + var identity = this.identity.ForGrid(typeof(TReturn), types, gridIndex); + try + { + foreach (var r in MultiMapImpl(null, default(CommandDefinition), types, map, splitOn, reader, identity, false)) + { + yield return r; + } + } + finally + { + NextResult(); + } + } + + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } + + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } + + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } + + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(func, splitOn); + return buffered ? result.ToList() : result; + } + + /// + /// Read multiple objects from a single record set on the grid + /// + public IEnumerable Read(Type[] types, Func map, string splitOn = "id", bool buffered = true) + { + var result = MultiReadInternal(types, map, splitOn); + return buffered ? result.ToList() : result; + } + + private IEnumerable ReadDeferred(int index, Func deserializer, Identity typedIdentity, Type effectiveType) + { + try + { + var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; + while (index == gridIndex && reader.Read()) + { + object val = deserializer(reader); + if (val == null || val is T) { + yield return (T)val; + } else { + yield return (T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture); + } + } + } + finally // finally so that First etc progresses things even when multiple rows + { + if (index == gridIndex) + { + NextResult(); + } + } + } + private int gridIndex, readCount; + private IParameterCallbacks callbacks; + + /// + /// Has the underlying reader been consumed? + /// + public bool IsConsumed { get; private set; } + + /// + /// The command associated with the reader + /// + public IDbCommand Command { get; set; } + + private void NextResult() + { + if (reader.NextResult()) + { + readCount++; + gridIndex++; + IsConsumed = false; + } + else + { + // happy path; close the reader cleanly - no + // need for "Cancel" etc + reader.Dispose(); + reader = null; + callbacks?.OnCompleted(); + Dispose(); + } + } + /// + /// Dispose the grid, closing and disposing both the underlying reader and command. + /// + public void Dispose() + { + if (reader != null) + { + if (!reader.IsClosed) Command?.Cancel(); + reader.Dispose(); + reader = null; + } + if (Command != null) + { + Command.Dispose(); + Command = null; + } + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ICustomQueryParameter.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ICustomQueryParameter.cs new file mode 100644 index 0000000..6ea5782 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ICustomQueryParameter.cs @@ -0,0 +1,20 @@ +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Implement this interface to pass an arbitrary db specific parameter to Dapper + /// + public interface ICustomQueryParameter + { + /// + /// Add the parameter needed to the command before it executes + /// + /// The raw command prior to execution + /// Parameter name + void AddParameter(IDbCommand command, string name); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDataReader.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDataReader.cs new file mode 100644 index 0000000..4db2fa9 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDataReader.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Parses a data reader to a sequence of data of the supplied type. Used for deserializing a reader without a connection, etc. + /// + public static IEnumerable Parse(this IDataReader reader) + { + if(reader.Read()) + { + var deser = GetDeserializer(typeof(T), reader, 0, -1, false); + do + { + yield return (T)deser(reader); + } while (reader.Read()); + } + } + + /// + /// Parses a data reader to a sequence of data of the supplied type (as object). Used for deserializing a reader without a connection, etc. + /// + public static IEnumerable Parse(this IDataReader reader, Type type) + { + if (reader.Read()) + { + var deser = GetDeserializer(type, reader, 0, -1, false); + do + { + yield return deser(reader); + } while (reader.Read()); + } + } + + /// + /// Parses a data reader to a sequence of dynamic. Used for deserializing a reader without a connection, etc. + /// + public static IEnumerable Parse(this IDataReader reader) + { + if (reader.Read()) + { + var deser = GetDapperRowDeserializer(reader, 0, -1, false); + do + { + yield return deser(reader); + } while (reader.Read()); + } + } + + /// + /// 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 IDataReader reader, Type type, + int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) + { + return GetDeserializer(type, 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. + /// + /// var result = new List<BaseType>(); + /// 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<BaseType>(typeof(Foo)); + /// var toBar = reader.GetRowParser<BaseType>(typeof(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()); + /// } + /// } + /// + /// abstract class BaseType + /// { + /// public abstract int Type { get; } + /// } + /// class Foo : BaseType + /// { + /// public string Name { get; set; } + /// public override int Type => 1; + /// } + /// class Bar : BaseType + /// { + /// public float Value { get; set; } + /// public override int Type => 2; + /// } + /// + public static Func GetRowParser(this IDataReader reader, Type concreteType = null, + int startIndex = 0, int length = -1, bool returnNullIfFirstMissing = false) + { + if (concreteType == null) concreteType = typeof(T); + var func = GetDeserializer(concreteType, reader, startIndex, length, returnNullIfFirstMissing); + if (concreteType.IsValueType()) + { + return _ => (T)func(_); + } + else + { + return (Func)(Delegate)func; + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDynamicParameters.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDynamicParameters.cs new file mode 100644 index 0000000..37e5514 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IDynamicParameters.cs @@ -0,0 +1,20 @@ +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Implement this interface to pass an arbitrary db specific set of parameters to Dapper + /// + public interface IDynamicParameters + { + /// + /// Add all the parameters needed to the command just before it executes + /// + /// The raw command prior to execution + /// Information about the query + void AddParameters(IDbCommand command, Identity identity); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IMemberMap.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IMemberMap.cs new file mode 100644 index 0000000..e58e859 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IMemberMap.cs @@ -0,0 +1,39 @@ +using System; +using System.Reflection; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Implements this interface to provide custom member mapping + /// + public interface IMemberMap + { + /// + /// Source DataReader column name + /// + string ColumnName { get; } + + /// + /// Target member type + /// + Type MemberType { get; } + + /// + /// Target property + /// + PropertyInfo Property { get; } + + /// + /// Target field + /// + FieldInfo Field { get; } + + /// + /// Target constructor parameter + /// + ParameterInfo Parameter { get; } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterCallbacks.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterCallbacks.cs new file mode 100644 index 0000000..84e4814 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterCallbacks.cs @@ -0,0 +1,16 @@ +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Extends IDynamicParameters with facilities for executing callbacks after commands have completed + /// + public interface IParameterCallbacks : IDynamicParameters + { + /// + /// Invoked when the command has executed + /// + void OnCompleted(); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterLookup.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterLookup.cs new file mode 100644 index 0000000..79653ee --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.IParameterLookup.cs @@ -0,0 +1,16 @@ +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Extends IDynamicParameters providing by-name lookup of parameter values + /// + public interface IParameterLookup : IDynamicParameters + { + /// + /// Get the value of the specified parameter (return null if not found) + /// + object this[string name] { get; } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeHandler.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeHandler.cs new file mode 100644 index 0000000..4bb0df7 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeHandler.cs @@ -0,0 +1,29 @@ +using System; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Implement this interface to perform custom type-based parameter handling and value parsing + /// + public interface ITypeHandler + { + /// + /// Assign the value of a parameter before a command executes + /// + /// The parameter to configure + /// Parameter value + void SetValue(IDbDataParameter parameter, object value); + + /// + /// Parse a database value back to a typed value + /// + /// The value from the database + /// The type to parse to + /// The typed value + object Parse(Type destinationType, object value); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeMap.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeMap.cs new file mode 100644 index 0000000..202ff74 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.ITypeMap.cs @@ -0,0 +1,46 @@ +using System; +using System.Reflection; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Implement this interface to change default mapping of reader columns to type members + /// + public interface ITypeMap + { + /// + /// Finds best constructor + /// + /// DataReader column names + /// DataReader column types + /// Matching constructor or default one + ConstructorInfo FindConstructor(string[] names, Type[] types); + + /// + /// Returns a constructor which should *always* be used. + /// + /// Parameters will be default values, nulls for reference types and zero'd for value types. + /// + /// Use this class to force object creation away from parameterless constructors you don't control. + /// + ConstructorInfo FindExplicitConstructor(); + + /// + /// Gets mapping for constructor parameter + /// + /// Constructor to resolve + /// DataReader column name + /// Mapping implementation + IMemberMap GetConstructorParameter(ConstructorInfo constructor, string columnName); + + /// + /// Gets member mapping for column + /// + /// DataReader column name + /// Mapping implementation + IMemberMap GetMember(string columnName); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Identity.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Identity.cs new file mode 100644 index 0000000..f151615 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Identity.cs @@ -0,0 +1,122 @@ +using System; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Identity of a cached query in Dapper, used for extensibility + /// + public class Identity : IEquatable + { + internal Identity ForGrid(Type primaryType, int gridIndex) + { + return new Identity(sql, commandType, connectionString, primaryType, parametersType, null, gridIndex); + } + + internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) + { + return new Identity(sql, commandType, connectionString, primaryType, parametersType, otherTypes, gridIndex); + } + /// + /// Create an identity for use with DynamicParameters, internal use only + /// + /// + /// + public Identity ForDynamicParameters(Type type) + { + return new Identity(sql, commandType, connectionString, this.type, type, null, -1); + } + + internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type type, Type parametersType, Type[] otherTypes) + : this(sql, commandType, connection.ConnectionString, type, parametersType, otherTypes, 0) + { } + private Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType, Type[] otherTypes, int gridIndex) + { + this.sql = sql; + this.commandType = commandType; + this.connectionString = connectionString; + this.type = type; + this.parametersType = parametersType; + this.gridIndex = gridIndex; + unchecked + { + hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this + hashCode = hashCode * 23 + commandType.GetHashCode(); + 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 + (parametersType?.GetHashCode() ?? 0); + } + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + return Equals(obj as Identity); + } + /// + /// The sql + /// + public readonly string sql; + /// + /// The command type + /// + public readonly CommandType? commandType; + + /// + /// + /// + public readonly int hashCode, gridIndex; + /// + /// + /// + public readonly Type type; + /// + /// + /// + public readonly string connectionString; + /// + /// + /// + public readonly Type parametersType; + /// + /// + /// + /// + public override int GetHashCode() + { + return hashCode; + } + /// + /// Compare 2 Identity objects + /// + /// + /// + public bool Equals(Identity other) + { + return + other != null && + gridIndex == other.gridIndex && + type == other.type && + sql == other.sql && + commandType == other.commandType && + connectionStringComparer.Equals(connectionString, other.connectionString) && + parametersType == other.parametersType; + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Link.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Link.cs new file mode 100644 index 0000000..beb49cf --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Link.cs @@ -0,0 +1,57 @@ +using System.Threading; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// This is a micro-cache; suitable when the number of terms is controllable (a few hundred, for example), + /// and strictly append-only; you cannot change existing values. All key matches are on **REFERENCE** + /// equality. The type is fully thread-safe. + /// + internal class Link where TKey : class + { + public static bool TryGet(Link link, TKey key, out TValue value) + { + while (link != null) + { + if ((object)key == (object)link.Key) + { + value = link.Value; + return true; + } + link = link.Tail; + } + value = default(TValue); + return false; + } + public static bool TryAdd(ref Link head, TKey key, ref TValue value) + { + bool tryAgain; + do + { + var snapshot = Interlocked.CompareExchange(ref head, null, null); + TValue found; + if (TryGet(snapshot, key, out found)) + { // existing match; report the existing value instead + value = found; + return false; + } + var newNode = new Link(key, value, snapshot); + // did somebody move our cheese? + tryAgain = Interlocked.CompareExchange(ref head, newNode, snapshot) != snapshot; + } while (tryAgain); + return true; + } + private Link(TKey key, TValue value, Link tail) + { + Key = key; + Value = value; + Tail = tail; + } + public TKey Key { get; } + public TValue Value { get; } + public Link Tail { get; } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.LiteralToken.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.LiteralToken.cs new file mode 100644 index 0000000..1e32349 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.LiteralToken.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Represents a placeholder for a value that should be replaced as a literal value in the resulting sql + /// + internal struct LiteralToken + { + /// + /// The text in the original command that should be replaced + /// + public string Token { get; } + + /// + /// The name of the member referred to by the token + /// + public string Member { get; } + + internal LiteralToken(string token, string member) + { + Token = token; + Member = member; + } + + internal static readonly IList None = new LiteralToken[0]; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Settings.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Settings.cs new file mode 100644 index 0000000..fa17566 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.Settings.cs @@ -0,0 +1,54 @@ +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Permits specifying certain SqlMapper values globally. + /// + public static class Settings + { + static Settings() + { + SetDefaults(); + } + + /// + /// Resets all Settings to their default values + /// + public static void SetDefaults() + { + CommandTimeout = null; + ApplyNullValues = false; + } + + /// + /// Specifies the default Command Timeout for all Queries + /// + public static int? CommandTimeout { get; set; } + + /// + /// Indicates whether nulls in data are silently ignored (default) vs actively applied and assigned to members + /// + public static bool ApplyNullValues { get; set; } + + + /// + /// Should list expansions be padded with null-valued parameters, to prevent query-plan saturation? For example, + /// an 'in @foo' expansion with 7, 8 or 9 values will be sent as a list of 10 values, with 3, 2 or 1 of them null. + /// The padding size is relative to the size of the list; "next 10" under 150, "next 50" under 500, + /// "next 100" under 1500, etc. + /// + /// + /// Caution: this should be treated with care if your DB provider (or the specific configuration) allows for null + /// equality (aka "ansi nulls off"), as this may change the intent of your query; as such, this is disabled by + /// default and must be enabled. + /// + 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). + /// + public static int InListStringSplitCount { get; set; } = -1; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeDeserializerCache.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeDeserializerCache.cs new file mode 100644 index 0000000..c50bd31 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeDeserializerCache.cs @@ -0,0 +1,160 @@ +using System; +using System.Data; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace Dapper +{ + partial class SqlMapper + { + + private class TypeDeserializerCache + { + private TypeDeserializerCache(Type type) + { + this.type = type; + } + static readonly Hashtable byType = new Hashtable(); + private readonly Type type; + internal static void Purge(Type type) + { + lock (byType) + { + byType.Remove(type); + } + } + internal static void Purge() + { + lock (byType) + { + byType.Clear(); + } + } + + internal static Func GetReader(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + { + var found = (TypeDeserializerCache)byType[type]; + if (found == null) + { + lock (byType) + { + found = (TypeDeserializerCache)byType[type]; + if (found == null) + { + byType[type] = found = new TypeDeserializerCache(type); + } + } + } + return found.GetReader(reader, startBound, length, returnNullIfFirstMissing); + } + private Dictionary> readers = new Dictionary>(); + 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 int hashCode; + + public DeserializerKey(int hashCode, int startBound, int length, bool returnNullIfFirstMissing, IDataReader reader, bool copyDown) + { + this.hashCode = hashCode; + this.startBound = startBound; + this.length = length; + this.returnNullIfFirstMissing = returnNullIfFirstMissing; + + if (copyDown) + { + this.reader = null; + names = new string[length]; + types = new Type[length]; + int index = startBound; + for (int i = 0; i < length; i++) + { + names[i] = reader.GetName(index); + types[i] = reader.GetFieldType(index++); + } + } + else + { + this.reader = reader; + names = null; + types = null; + } + } + + public override int GetHashCode() + { + return hashCode; + } + public override string ToString() + { // only used in the debugger + if (names != null) + { + return string.Join(", ", names); + } + if (reader != null) + { + var sb = new StringBuilder(); + int index = startBound; + for (int i = 0; i < length; i++) + { + if (i != 0) sb.Append(", "); + sb.Append(reader.GetName(index++)); + } + return sb.ToString(); + } + return base.ToString(); + } + public override bool Equals(object obj) + { + return obj is DeserializerKey && Equals((DeserializerKey)obj); + } + public bool Equals(DeserializerKey other) + { + if (this.hashCode != other.hashCode + || this.startBound != other.startBound + || this.length != other.length + || this.returnNullIfFirstMissing != other.returnNullIfFirstMissing) + { + return false; // clearly different + } + for (int i = 0; i < length; i++) + { + if ((this.names?[i] ?? this.reader?.GetName(startBound + i)) != (other.names?[i] ?? other.reader?.GetName(startBound + i)) + || + (this.types?[i] ?? this.reader?.GetFieldType(startBound + i)) != (other.types?[i] ?? other.reader?.GetFieldType(startBound + i)) + ) + { + return false; // different column name or type + } + } + return true; + } + } + private Func GetReader(IDataReader 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; + lock (readers) + { + 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 + key = new DeserializerKey(hash, startBound, length, returnNullIfFirstMissing, reader, true); + lock (readers) + { + return readers[key] = deser; + } + } + } + + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandler.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandler.cs new file mode 100644 index 0000000..5570f3b --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandler.cs @@ -0,0 +1,78 @@ +using System; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Base-class for simple type-handlers + /// + public abstract class TypeHandler : ITypeHandler + { + /// + /// Assign the value of a parameter before a command executes + /// + /// The parameter to configure + /// Parameter 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); + + void ITypeHandler.SetValue(IDbDataParameter parameter, object value) + { + if (value is DBNull) + { + parameter.Value = value; + } + else + { + SetValue(parameter, (T)value); + } + } + + object ITypeHandler.Parse(Type destinationType, object value) + { + return Parse(value); + } + } + /// + /// Base-class for simple type-handlers that are based around strings + /// + public abstract class StringTypeHandler : TypeHandler + { + /// + /// Parse a string into the expected type (the string will never be null) + /// + protected abstract T Parse(string xml); + /// + /// Format an instace into a string (the instance will never be null) + /// + protected abstract string Format(T xml); + /// + /// Assign the value of a parameter before a command executes + /// + /// The parameter to configure + /// Parameter value + public override void SetValue(IDbDataParameter parameter, T value) + { + parameter.Value = value == null ? (object)DBNull.Value : Format(value); + } + /// + /// Parse a database value back to a typed value + /// + /// The value from the database + /// The typed value + public override T Parse(object value) + { + if (value == null || value is DBNull) return default(T); + return Parse((string)value); + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandlerCache.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandlerCache.cs new file mode 100644 index 0000000..a743795 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.TypeHandlerCache.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { + /// + /// Not intended for direct usage + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] +#if !COREFX + [Browsable(false)] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + public static class TypeHandlerCache + { + /// + /// Not intended for direct usage + /// + [Obsolete(ObsoleteInternalUsageOnly, true)] + public static T Parse(object value) + { + return (T)handler.Parse(typeof(T), value); + } + + /// + /// Not intended for direct usage + /// + [Obsolete(ObsoleteInternalUsageOnly, true)] + public static void SetValue(IDbDataParameter parameter, object value) + { + handler.SetValue(parameter, value); + } + + internal static void SetHandler(ITypeHandler handler) + { +#pragma warning disable 618 + TypeHandlerCache.handler = handler; +#pragma warning restore 618 + } + + private static ITypeHandler handler; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.cs new file mode 100644 index 0000000..39e9cfc --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/SqlMapper.cs @@ -0,0 +1,3543 @@ +/* + License: http://www.apache.org/licenses/LICENSE-2.0 + Home page: https://github.com/StackExchange/dapper-dot-net + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Xml; +using System.Xml.Linq; + +#if COREFX +using DataException = System.InvalidOperationException; +#endif + +namespace Dapper +{ + + /// + /// Dapper, a light weight object mapper for ADO.NET + /// + public static partial class SqlMapper + { + static int GetColumnHash(IDataReader reader, int startBound = 0, int length = -1) + { + unchecked + { + int max = length < 0 ? reader.FieldCount : startBound + length; + int hash = (-37 * startBound) + max; + for (int i = startBound; i < max; i++) + { + object tmp = reader.GetName(i); + hash = -79 * ((hash * 31) + (tmp?.GetHashCode() ?? 0)) + (reader.GetFieldType(i)?.GetHashCode() ?? 0); + } + return hash; + } + } + + + /// + /// Called if the query cache is purged via PurgeQueryCache + /// + public static event EventHandler QueryCachePurged; + private static void OnQueryCachePurged() + { + var handler = QueryCachePurged; + handler?.Invoke(null, EventArgs.Empty); + } + + static readonly System.Collections.Concurrent.ConcurrentDictionary _queryCache = new System.Collections.Concurrent.ConcurrentDictionary(); + private static void SetQueryCache(Identity key, CacheInfo value) + { + if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS) + { + CollectCacheGarbage(); + } + _queryCache[key] = value; + } + + private static void CollectCacheGarbage() + { + try + { + foreach (var pair in _queryCache) + { + if (pair.Value.GetHitCount() <= COLLECT_HIT_COUNT_MIN) + { + CacheInfo cache; + _queryCache.TryRemove(pair.Key, out cache); + } + } + } + + finally + { + Interlocked.Exchange(ref collect, 0); + } + } + + 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) + { + if (_queryCache.TryGetValue(key, out value)) + { + value.RecordHit(); + return true; + } + value = null; + return false; + } + + /// + /// Purge the query cache + /// + public static void PurgeQueryCache() + { + _queryCache.Clear(); + TypeDeserializerCache.Purge(); + OnQueryCachePurged(); + } + + private static void PurgeQueryCacheByType(Type type) + { + foreach (var entry in _queryCache) + { + CacheInfo cache; + if (entry.Key.type == type) + _queryCache.TryRemove(entry.Key, out cache); + } + TypeDeserializerCache.Purge(type); + } + + /// + /// Return a count of all the cached queries by dapper + /// + /// + public static int GetCachedSQLCount() + { + return _queryCache.Count; + } + + /// + /// Return a list of all the queries cached by dapper + /// + /// + /// + public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue) + { + var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount())); + if (ignoreHitCountAbove < int.MaxValue) data = data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove); + return data; + } + + /// + /// Deep diagnostics only: find any hash collisions in the cache + /// + /// + public static IEnumerable> GetHashCollissions() + { + var counts = new Dictionary(); + foreach (var key in _queryCache.Keys) + { + int count; + if (!counts.TryGetValue(key.hashCode, out count)) + { + counts.Add(key.hashCode, 1); + } + else + { + counts[key.hashCode] = count + 1; + } + } + return from pair in counts + where pair.Value > 1 + select Tuple.Create(pair.Key, pair.Value); + + } + + + static Dictionary typeMap; + + static SqlMapper() + { + typeMap = new Dictionary + { + [typeof(byte)] = DbType.Byte, + [typeof(sbyte)] = DbType.SByte, + [typeof(short)] = DbType.Int16, + [typeof(ushort)] = DbType.UInt16, + [typeof(int)] = DbType.Int32, + [typeof(uint)] = DbType.UInt32, + [typeof(long)] = DbType.Int64, + [typeof(ulong)] = DbType.UInt64, + [typeof(float)] = DbType.Single, + [typeof(double)] = DbType.Double, + [typeof(decimal)] = DbType.Decimal, + [typeof(bool)] = DbType.Boolean, + [typeof(string)] = DbType.String, + [typeof(char)] = DbType.StringFixedLength, + [typeof(Guid)] = DbType.Guid, + [typeof(DateTime)] = DbType.DateTime, + [typeof(DateTimeOffset)] = DbType.DateTimeOffset, + [typeof(TimeSpan)] = DbType.Time, + [typeof(byte[])] = DbType.Binary, + [typeof(byte?)] = DbType.Byte, + [typeof(sbyte?)] = DbType.SByte, + [typeof(short?)] = DbType.Int16, + [typeof(ushort?)] = DbType.UInt16, + [typeof(int?)] = DbType.Int32, + [typeof(uint?)] = DbType.UInt32, + [typeof(long?)] = DbType.Int64, + [typeof(ulong?)] = DbType.UInt64, + [typeof(float?)] = DbType.Single, + [typeof(double?)] = DbType.Double, + [typeof(decimal?)] = DbType.Decimal, + [typeof(bool?)] = DbType.Boolean, + [typeof(char?)] = DbType.StringFixedLength, + [typeof(Guid?)] = DbType.Guid, + [typeof(DateTime?)] = DbType.DateTime, + [typeof(DateTimeOffset?)] = DbType.DateTimeOffset, + [typeof(TimeSpan?)] = DbType.Time, + [typeof(object)] = DbType.Object + }; + ResetTypeHandlers(false); + } + + /// + /// Clear the registered type handlers + /// + public static void ResetTypeHandlers() + { + ResetTypeHandlers(true); + } + private static void ResetTypeHandlers(bool clone) + { + typeHandlers = new Dictionary(); +#if !COREFX + AddTypeHandlerImpl(typeof(DataTable), new DataTableHandler(), clone); + try // see https://github.com/StackExchange/dapper-dot-net/issues/424 + { + AddSqlDataRecordsTypeHandler(clone); + } + catch { } +#endif + AddTypeHandlerImpl(typeof(XmlDocument), new XmlDocumentHandler(), clone); + AddTypeHandlerImpl(typeof(XDocument), new XDocumentHandler(), clone); + AddTypeHandlerImpl(typeof(XElement), new XElementHandler(), clone); + + allowedCommandBehaviors = DefaultAllowedCommandBehaviors; + } +#if !COREFX + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AddSqlDataRecordsTypeHandler(bool clone) + { + AddTypeHandlerImpl(typeof(IEnumerable), new SqlDataRecordHandler(), clone); + } +#endif + + /// + /// Configure the specified type to be mapped to a given db-type + /// + public static void AddTypeMap(Type type, DbType dbType) + { + // use clone, mutate, replace to avoid threading issues + var snapshot = typeMap; + + DbType oldValue; + if (snapshot.TryGetValue(type, out oldValue) && oldValue == dbType) return; // nothing to do + + var newCopy = new Dictionary(snapshot) { [type] = dbType }; + typeMap = newCopy; + } + + /// + /// Configure the specified type to be processed by a custom handler + /// + public static void AddTypeHandler(Type type, ITypeHandler handler) + { + AddTypeHandlerImpl(type, handler, true); + } + + internal static bool HasTypeHandler(Type type) + { + return typeHandlers.ContainsKey(type); + } + + /// + /// Configure the specified type to be processed by a custom handler + /// + public static void AddTypeHandlerImpl(Type type, ITypeHandler handler, bool clone) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + Type secondary = null; + if(type.IsValueType()) + { + var underlying = Nullable.GetUnderlyingType(type); + if(underlying == null) + { + secondary = typeof(Nullable<>).MakeGenericType(type); // the Nullable + // type is already the T + } + else + { + secondary = type; // the Nullable + type = underlying; // the T + } + } + + var snapshot = typeHandlers; + ITypeHandler oldValue; + if (snapshot.TryGetValue(type, out oldValue) && handler == oldValue) return; // nothing to do + + var newCopy = clone ? new Dictionary(snapshot) : snapshot; + +#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 }); + } +#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; + } + typeHandlers = newCopy; + } + + /// + /// Configure the specified type to be processed by a custom handler + /// + public static void AddTypeHandler(TypeHandler handler) + { + AddTypeHandlerImpl(typeof(T), handler, true); + } + + private static Dictionary typeHandlers; + + internal const string LinqBinary = "System.Data.Linq.Binary"; + + private const string ObsoleteInternalUsageOnly = "This method is for internal use only"; + + /// + /// Get the DbType that maps to a given value + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] +#if !COREFX + [Browsable(false)] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + public static DbType GetDbType(object value) + { + if (value == null || value is DBNull) return DbType.Object; + + ITypeHandler handler; + return LookupDbType(value.GetType(), "n/a", false, out handler); + + } + + /// + /// OBSOLETE: For internal usage only. Lookup the DbType and handler for a given Type and member + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] +#if !COREFX + [Browsable(false)] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + public static DbType LookupDbType(Type type, string name, bool demand, out ITypeHandler handler) + { + DbType dbType; + handler = null; + var nullUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullUnderlyingType != null) type = nullUnderlyingType; + if (type.IsEnum() && !typeMap.ContainsKey(type)) + { + type = Enum.GetUnderlyingType(type); + } + if (typeMap.TryGetValue(type, out dbType)) + { + return dbType; + } + if (type.FullName == LinqBinary) + { + return DbType.Binary; + } + if (typeHandlers.TryGetValue(type, out handler)) + { + return DbType.Object; + } + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + return DynamicParameters.EnumerableMultiParameter; + } + +#if !COREFX + switch (type.FullName) + { + case "Microsoft.SqlServer.Types.SqlGeography": + AddTypeHandler(type, handler = new UdtTypeHandler("geography")); + return DbType.Object; + case "Microsoft.SqlServer.Types.SqlGeometry": + AddTypeHandler(type, handler = new UdtTypeHandler("geometry")); + return DbType.Object; + case "Microsoft.SqlServer.Types.SqlHierarchyId": + 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; + + } + + + + /// + /// Obtains the data as a list; if it is *already* a list, the original object is returned without + /// any duplication; otherwise, ToList() is invoked. + /// + public static List AsList(this IEnumerable source) + { + return (source == null || source is List) ? (List)source : source.ToList(); + } + + /// + /// Execute parameterized SQL + /// + /// 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 + ) + { + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered); + return ExecuteImpl(cnn, ref command); + } + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute(this IDbConnection cnn, CommandDefinition command) + { + return ExecuteImpl(cnn, ref command); + } + + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + 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); + } + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + 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); + } + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + public static object ExecuteScalar(this IDbConnection cnn, CommandDefinition command) + { + return ExecuteScalarImpl(cnn, ref command); + } + + /// + /// Execute parameterized SQL that selects a single value + /// + /// The first cell selected + public static T ExecuteScalar(this IDbConnection cnn, CommandDefinition command) + { + return ExecuteScalarImpl(cnn, ref command); + } + + private static IEnumerable GetMultiExec(object param) + { + return (param is IEnumerable && + !(param is string || + param is IEnumerable> || + param is IDynamicParameters) + ) ? (IEnumerable) param : null; + } + + private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition command) + { + object param = command.Parameters; + IEnumerable multiExec = GetMultiExec(param); + Identity identity; + CacheInfo info = null; + if (multiExec != null) + { +#if ASYNC + if((command.Flags & CommandFlags.Pipelined) != 0) + { + // this includes all the code for concurrent/overlapped query + return ExecuteMultiImplAsync(cnn, command, multiExec).Result; + } +#endif + bool isFirst = true; + int total = 0; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + if (wasClosed) cnn.Open(); + using (var cmd = command.SetupCommand(cnn, 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); + 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 += cmd.ExecuteNonQuery(); + } + } + command.OnCompleted(); + } finally + { + if (wasClosed) cnn.Close(); + } + return total; + } + + // nice and simple + if (param != null) + { + identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + info = GetCacheInfo(identity, param, command.AddToCache); + } + return ExecuteCommand(cnn, ref command, param == null ? null : info.ParamReader); + } + + /// + /// Execute parameterized SQL and return an + /// + /// An that can be used to iterate over the results of the SQL query. + /// + /// This is typically used when the results of a query are not processed by Dapper, for example, used to fill a + /// or . + /// + /// + /// + /// + /// + /// + 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); + IDbCommand dbcmd; + var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out dbcmd); + return new WrappedReader(dbcmd, reader); + } + + /// + /// Execute parameterized SQL and return an + /// + /// An that can be used to iterate over the results of the SQL query. + /// + /// This is typically used when the results of a query are not processed by Dapper, for example, used to fill a + /// or . + /// + public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command) + { + IDbCommand dbcmd; + var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out dbcmd); + return new WrappedReader(dbcmd, reader); + } + /// + /// Execute parameterized SQL and return an + /// + /// An that can be used to iterate over the results of the SQL query. + /// + /// This is typically used when the results of a query are not processed by Dapper, for example, used to fill a + /// or . + /// + public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) + { + IDbCommand dbcmd; + var reader = ExecuteReaderImpl(cnn, ref command, commandBehavior, out dbcmd); + return new WrappedReader(dbcmd, reader); + } + + /// + /// Return a sequence of dynamic objects with properties matching the columns + /// + /// 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) + { + return Query(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType); + } + + /// + /// Return a dynamic object with properties matching the columns + /// + /// 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) + { + return QueryFirst(cnn, sql, param as object, transaction, commandTimeout, commandType); + } + /// + /// Return a dynamic object with properties matching the columns + /// + /// 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) + { + return QueryFirstOrDefault(cnn, sql, param as object, transaction, commandTimeout, commandType); + } + /// + /// Return a dynamic object with properties matching the columns + /// + /// 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) + { + return QuerySingle(cnn, sql, param as object, transaction, commandTimeout, commandType); + } + /// + /// Return a dynamic object with properties matching the columns + /// + /// 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) + { + return QuerySingleOrDefault(cnn, sql, param as object, transaction, commandTimeout, commandType); + } + + /// + /// Executes a query, returning the data typed as per T + /// + /// 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 + /// 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 + ) + { + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); + var data = QueryImpl(cnn, command, typeof(T)); + return command.Buffered ? data.ToList() : data; + } + + /// + /// Executes a single-row query, returning the data typed as per T + /// + /// 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 + /// 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 + ) + { + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); + return QueryRowImpl(cnn, Row.First, ref command, typeof(T)); + } + /// + /// Executes a single-row query, returning the data typed as per T + /// + /// 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 + /// 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 + ) + { + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); + return QueryRowImpl(cnn, Row.FirstOrDefault, ref command, typeof(T)); + } + /// + /// Executes a single-row query, returning the data typed as per T + /// + /// 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 + /// 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 + ) + { + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); + return QueryRowImpl(cnn, Row.Single, ref command, typeof(T)); + } + /// + /// Executes a single-row query, returning the data typed as per T + /// + /// 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 + /// 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 + ) + { + var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None); + return QueryRowImpl(cnn, Row.SingleOrDefault, ref command, typeof(T)); + } + + /// + /// Executes a single-row query, returning the data typed as per the Type suggested + /// + /// 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 + /// 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 + ) + { + if (type == 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; + } + /// + /// Executes a single-row query, returning the data typed as per the Type suggested + /// + /// 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 + /// 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 + ) + { + if (type == 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); + } + /// + /// Executes a single-row query, returning the data typed as per the Type suggested + /// + /// 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 + /// 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 + ) + { + if (type == 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); + } + /// + /// Executes a single-row query, returning the data typed as per the Type suggested + /// + /// 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 + /// 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 + ) + { + if (type == 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); + } + /// + /// Executes a single-row query, returning the data typed as per the Type suggested + /// + /// 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 + /// 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 + ) + { + if (type == 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); + } + /// + /// Executes a query, returning the data typed as per T + /// + /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object + /// 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 + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static IEnumerable Query(this IDbConnection cnn, CommandDefinition command) + { + var data = QueryImpl(cnn, command, typeof(T)); + return command.Buffered ? data.ToList() : data; + } + + /// + /// Executes a query, returning the data typed as per T + /// + /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object + /// 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 + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static T QueryFirst(this IDbConnection cnn, CommandDefinition command) + { + return QueryRowImpl(cnn, Row.First, ref command, typeof(T)); + } + /// + /// Executes a query, returning the data typed as per T + /// + /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object + /// 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 + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static T QueryFirstOrDefault(this IDbConnection cnn, CommandDefinition command) + { + return QueryRowImpl(cnn, Row.FirstOrDefault, ref command, typeof(T)); + } + /// + /// Executes a query, returning the data typed as per T + /// + /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object + /// 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 + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static T QuerySingle(this IDbConnection cnn, CommandDefinition command) + { + return QueryRowImpl(cnn, Row.Single, ref command, typeof(T)); + } + /// + /// Executes a query, returning the data typed as per T + /// + /// the dynamic param may seem a bit odd, but this works around a major usability issue in vs, if it is Object vs completion gets annoying. Eg type new [space] get new object + /// 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 + /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). + /// + public static T QuerySingleOrDefault(this IDbConnection cnn, CommandDefinition command) + { + return QueryRowImpl(cnn, Row.SingleOrDefault, ref command, typeof(T)); + } + + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + 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); + } + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static GridReader QueryMultiple(this IDbConnection cnn, CommandDefinition command) + { + return QueryMultipleImpl(cnn, ref command); + } + + private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandDefinition command) + { + object param = command.Parameters; + Identity identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType(), null); + CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); + + IDbCommand cmd = null; + IDataReader reader = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + if (wasClosed) cnn.Open(); + cmd = command.SetupCommand(cnn, info.ParamReader); + reader = ExecuteReaderWithFlagsFallback(cmd, wasClosed, CommandBehavior.SequentialAccess); + + var result = new GridReader(cmd, reader, identity, command.Parameters as DynamicParameters, command.AddToCache); + cmd = null; // now owned by result + wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader + // 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 + return result; + } + catch + { + if (reader != null) + { + if (!reader.IsClosed) try { cmd?.Cancel(); } + catch { /* don't spoil the existing exception */ } + reader.Dispose(); + } + cmd?.Dispose(); + if (wasClosed) cnn.Close(); + throw; + } + } + private static IDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool wasClosed, CommandBehavior behavior) + { + try + { + return cmd.ExecuteReader(GetBehavior(wasClosed, behavior)); + } + catch (ArgumentException ex) + { // thanks, Sqlite! + if (DisableCommandBehaviorOptimizations(behavior, ex)) + { + // we can retry; this time it will have different flags + return cmd.ExecuteReader(GetBehavior(wasClosed, behavior)); + } + throw; + } + } + 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); + var info = GetCacheInfo(identity, param, command.AddToCache); + + IDbCommand cmd = null; + IDataReader reader = null; + + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + cmd = command.SetupCommand(cnn, info.ParamReader); + + if (wasClosed) cnn.Open(); + reader = ExecuteReaderWithFlagsFallback(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult); + wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader + // 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) + { + if (reader.FieldCount == 0) //https://code.google.com/p/dapper-dot-net/issues/detail?id=57 + 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 (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); + } + } + while (reader.NextResult()) { } + // happy path; close the reader cleanly - no + // need for "Cancel" etc + reader.Dispose(); + reader = null; + + command.OnCompleted(); + } + finally + { + if (reader != null) + { + if (!reader.IsClosed) try { cmd.Cancel(); } + catch { /* don't spoil the existing exception */ } + reader.Dispose(); + } + if (wasClosed) cnn.Close(); + cmd?.Dispose(); + } + } + + [Flags] + internal enum Row + { + First = 0, + FirstOrDefault = 1, // &FirstOrDefault != 0: allow zero rows + Single = 2, // & Single != 0: demand at least one row + SingleOrDefault = 3 + } + static readonly int[] ErrTwoRows = new int[2], ErrZeroRows = new int[0]; + 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(); + } + } + 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(); + } + } + 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); + var info = GetCacheInfo(identity, param, command.AddToCache); + + IDbCommand cmd = null; + IDataReader reader = null; + + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + cmd = command.SetupCommand(cnn, info.ParamReader); + + if (wasClosed) cnn.Open(); + reader = ExecuteReaderWithFlagsFallback(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); + wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader + + T result = default(T); + 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); + } + + 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()) { } + } + else if ((row & Row.FirstOrDefault) == 0) // demanding a row, and don't have one + { + ThrowZeroRows(row); + } + while (reader.NextResult()) { } + // happy path; close the reader cleanly - no + // need for "Cancel" etc + reader.Dispose(); + reader = null; + + command.OnCompleted(); + return result; + } + finally + { + if (reader != null) + { + if (!reader.IsClosed) try { cmd.Cancel(); } + catch { /* don't spoil the existing exception */ } + reader.Dispose(); + } + if (wasClosed) cnn.Close(); + cmd?.Dispose(); + } + } + + /// + /// Maps a query to objects + /// + /// The first type in the record set + /// The second type in the record set + /// The return type + /// + /// + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// Is it a stored proc or a batch? + /// + 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 + ) + { + return MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Maps a query to objects + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// + /// + 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 + ) + { + return MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Perform a multi mapping query with 4 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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 + ) + { + return MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Perform a multi mapping query with 5 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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 +) + { + return MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Perform a multi mapping query with 6 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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 +) + { + return MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + + /// + /// Perform a multi mapping query with 7 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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) + { + return MultiMap(cnn, sql, map, param, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Perform a multi mapping query with arbitrary input parameters + /// + /// The return type + /// + /// + /// array of types in the record set + /// + /// + /// + /// + /// The Field we should split and read the second object from (default: id) + /// Number of seconds before command execution timeout + /// Is it a stored proc or a batch? + /// + 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); + return buffered ? results.ToList() : results; + } + + static IEnumerable MultiMap( + 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; + } + + static IEnumerable MultiMapImpl(this IDbConnection cnn, CommandDefinition command, Delegate map, string splitOn, IDataReader 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) }); + CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); + + IDbCommand ownedCommand = null; + IDataReader ownedReader = null; + + bool wasClosed = cnn != null && cnn.State == ConnectionState.Closed; + try + { + if (reader == null) + { + ownedCommand = command.SetupCommand(cnn, cinfo.ParamReader); + if (wasClosed) cnn.Open(); + ownedReader = ExecuteReaderWithFlagsFallback(ownedCommand, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult); + reader = ownedReader; + } + DeserializerState deserializer = default(DeserializerState); + Func[] otherDeserializers; + + int hash = GetColumnHash(reader); + if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash) + { + var deserializers = GenerateDeserializers(new [] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth), typeof(TSixth), typeof(TSeventh) }, 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); + + if (mapIt != null) + { + while (reader.Read()) + { + yield return mapIt(reader); + } + if(finalize) + { + while (reader.NextResult()) { } + command.OnCompleted(); + } + } + } + finally + { + try + { + ownedReader?.Dispose(); + } + finally + { + ownedCommand?.Dispose(); + if (wasClosed) cnn.Close(); + } + } + } + const CommandBehavior DefaultAllowedCommandBehaviors = ~((CommandBehavior)0); + static CommandBehavior allowedCommandBehaviors = DefaultAllowedCommandBehaviors; + private static bool DisableCommandBehaviorOptimizations(CommandBehavior behavior, Exception ex) + { + if(allowedCommandBehaviors == DefaultAllowedCommandBehaviors + && (behavior & (CommandBehavior.SingleResult | CommandBehavior.SingleRow)) != 0) + { + 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 + allowedCommandBehaviors = ~(CommandBehavior.SingleResult | CommandBehavior.SingleRow); + return true; + } + } + return false; + } + private static CommandBehavior GetBehavior(bool close, CommandBehavior @default) + { + return (close ? (@default | CommandBehavior.CloseConnection) : @default) & allowedCommandBehaviors; + } + static IEnumerable MultiMapImpl(this IDbConnection cnn, CommandDefinition command, Type[] types, Func map, string splitOn, IDataReader 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); + CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); + + IDbCommand ownedCommand = null; + IDataReader ownedReader = null; + + bool wasClosed = cnn != null && cnn.State == ConnectionState.Closed; + try + { + if (reader == null) + { + ownedCommand = command.SetupCommand(cnn, cinfo.ParamReader); + if (wasClosed) cnn.Open(); + ownedReader = ExecuteReaderWithFlagsFallback(ownedCommand, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult); + reader = ownedReader; + } + DeserializerState deserializer; + Func[] otherDeserializers; + + int hash = GetColumnHash(reader); + if ((deserializer = cinfo.Deserializer).Func == null || (otherDeserializers = cinfo.OtherDeserializers) == null || hash != deserializer.Hash) + { + var deserializers = GenerateDeserializers(types, splitOn, reader); + deserializer = cinfo.Deserializer = new DeserializerState(hash, deserializers[0]); + otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); + SetQueryCache(identity, cinfo); + } + + Func mapIt = GenerateMapper(types.Length, deserializer.Func, otherDeserializers, map); + + if (mapIt != null) + { + while (reader.Read()) + { + yield return mapIt(reader); + } + if (finalize) + { + while (reader.NextResult()) { } + command.OnCompleted(); + } + } + } + finally + { + try + { + ownedReader?.Dispose(); + } + finally + { + ownedCommand?.Dispose(); + if (wasClosed) cnn.Close(); + } + } + } + + private static Func GenerateMapper(Func deserializer, Func[] otherDeserializers, object map) + { + 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) + { + return r => + { + var objects = new object[length]; + objects[0] = deserializer(r); + + for (var i = 1; i < length; ++i) + { + objects[i] = otherDeserializers[i - 1](r); + } + + return map(objects); + }; + } + + private static Func[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader) + { + var deserializers = new List>(); + var splits = splitOn.Split(',').Select(s => s.Trim()).ToArray(); + bool isMultiSplit = splits.Length > 1; + if (types.First() == typeof(object)) + { + // we go left to right for dynamic multi-mapping so that the madness of TestMultiMappingVariations + // is supported + bool first = true; + int currentPos = 0; + int splitIdx = 0; + string currentSplit = splits[splitIdx]; + foreach (var type in types) + { + if (type == typeof(DontMap)) + { + break; + } + + int splitPoint = GetNextSplitDynamic(currentPos, currentSplit, reader); + if (isMultiSplit && splitIdx < splits.Length - 1) + { + currentSplit = splits[++splitIdx]; + } + deserializers.Add((GetDeserializer(type, reader, currentPos, splitPoint - currentPos, !first))); + currentPos = splitPoint; + first = false; + } + } + else + { + // in this we go right to left through the data reader in order to cope with properties that are + // named the same as a subsequent primary key that we split on + int currentPos = reader.FieldCount; + int splitIdx = splits.Length - 1; + var currentSplit = splits[splitIdx]; + for (var typeIdx = types.Length - 1; typeIdx >= 0; --typeIdx) + { + var type = types[typeIdx]; + if (type == typeof (DontMap)) + { + continue; + } + + int splitPoint = 0; + if (typeIdx > 0) + { + splitPoint = GetNextSplit(currentPos, currentSplit, reader); + if (isMultiSplit && splitIdx > 0) + { + currentSplit = splits[--splitIdx]; + } + } + + deserializers.Add((GetDeserializer(type, reader, splitPoint, currentPos - splitPoint, typeIdx > 0))); + currentPos = splitPoint; + } + + deserializers.Reverse(); + + } + return deserializers.ToArray(); + } + + private static int GetNextSplitDynamic(int startIdx, string splitOn, IDataReader reader) + { + if (startIdx == reader.FieldCount) + { + throw MultiMapException(reader); + } + + if (splitOn == "*") + { + return ++startIdx; + } + + for (var i = startIdx + 1; i < reader.FieldCount; ++i) + { + if (string.Equals(splitOn, reader.GetName(i), StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return reader.FieldCount; + } + + private static int GetNextSplit(int startIdx, string splitOn, IDataReader reader) + { + if (splitOn == "*") + { + return --startIdx; + } + + for (var i = startIdx - 1; i > 0; --i) + { + if (string.Equals(splitOn, reader.GetName(i), StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + throw MultiMapException(reader); + } + + private static CacheInfo GetCacheInfo(Identity identity, object exampleParameters, bool addToCache) + { + CacheInfo info; + if (!TryGetQueryCache(identity, out info)) + { + if(GetMultiExec(exampleParameters) != 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) + { + Action reader; + if (exampleParameters is IDynamicParameters) + { + reader = (cmd, obj) => { ((IDynamicParameters)obj).AddParameters(cmd, identity); }; + } + else if (exampleParameters is IEnumerable>) + { + reader = (cmd, obj) => + { + IDynamicParameters mapped = new DynamicParameters(obj); + mapped.AddParameters(cmd, identity); + }; + } + else + { + var literals = GetLiteralTokens(identity.sql); + reader = CreateParamInfoGenerator(identity, false, true, literals); + } + if((identity.commandType == null || identity.commandType == CommandType.Text) && ShouldPassByPosition(identity.sql)) + { + var tail = reader; + reader = (cmd, obj) => + { + tail(cmd, obj); + PassByPosition(cmd); + }; + } + info.ParamReader = reader; + } + if(addToCache) SetQueryCache(identity, info); + } + return info; + } + + private static bool ShouldPassByPosition(string sql) + { + return sql != null && sql.IndexOf('?') >= 0 && pseudoPositional.IsMatch(sql); + } + + private static void PassByPosition(IDbCommand cmd) + { + if (cmd.Parameters.Count == 0) return; + + Dictionary parameters = new Dictionary(StringComparer.Ordinal); + + foreach(IDbDataParameter param in cmd.Parameters) + { + if (!string.IsNullOrEmpty(param.ParameterName)) parameters[param.ParameterName] = param; + } + HashSet consumed = new HashSet(StringComparer.Ordinal); + bool firstMatch = true; + cmd.CommandText = pseudoPositional.Replace(cmd.CommandText, match => + { + string key = match.Groups[1].Value; + IDbDataParameter param; + 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 param)) + { + if(firstMatch) + { + firstMatch = false; + cmd.Parameters.Clear(); // only clear if we are pretty positive that we've found this pattern successfully + } + // if found, return the anonymous token "?" + cmd.Parameters.Add(param); + parameters.Remove(key); + consumed.Add(key); + return "?"; + } + else + { + // otherwise, leave alone for simple debugging + return match.Value; + } + }); + } + + private static Func GetDeserializer(Type type, IDataReader 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()))) + { + ITypeHandler handler; + if (typeHandlers.TryGetValue(type, out handler)) + { + return GetHandlerDeserializer(handler, type, startBound); + } + return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing); + } + return GetStructDeserializer(type, underlyingType ?? type, startBound); + } + private static Func GetHandlerDeserializer(ITypeHandler handler, Type type, int startBound) + { + return reader => handler.Parse(type, reader.GetValue(startBound)); + } + + + private static Exception MultiMapException(IDataRecord reader) + { + bool hasFields = false; + try { + hasFields = reader != null && reader.FieldCount != 0; + } catch { } + 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"); + else + return new InvalidOperationException("No columns were selected"); + } + + internal static Func GetDapperRowDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) + { + var fieldCount = reader.FieldCount; + if (length == -1) + { + length = fieldCount - startBound; + } + + if (fieldCount <= startBound) + { + throw MultiMapException(reader); + } + + var effectiveFieldCount = Math.Min(fieldCount - startBound, length); + + DapperTable table = null; + + return + r => + { + if (table == null) + { + string[] names = new string[effectiveFieldCount]; + for (int i = 0; i < effectiveFieldCount; i++) + { + names[i] = r.GetName(i + startBound); + } + table = new DapperTable(names); + } + + var values = new object[effectiveFieldCount]; + + if (returnNullIfFirstMissing) + { + values[0] = r.GetValue(startBound); + if (values[0] is DBNull) + { + return null; + } + } + + if (startBound == 0) + { + for (int i = 0; i < values.Length; i++) + { + object val = r.GetValue(i); + values[i] = val is DBNull ? null : val; + } + } + else + { + var begin = returnNullIfFirstMissing ? 1 : 0; + for (var iter = begin; iter < effectiveFieldCount; ++iter) + { + object obj = r.GetValue(iter + startBound); + values[iter] = obj is DBNull ? null : obj; + } + } + return new DapperRow(table, values); + }; + } + /// + /// Internal use only + /// + /// + /// +#if !COREFX + [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)); + string s = value as string; + if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", nameof(value)); + return s[0]; + } + + /// + /// Internal use only + /// +#if !COREFX + [Browsable(false)] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete(ObsoleteInternalUsageOnly, false)] + public static char? ReadNullableChar(object value) + { + if (value == null || value is DBNull) return null; + string s = value as string; + if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", nameof(value)); + return s[0]; + } + + + /// + /// Internal use only + /// +#if !COREFX + [Browsable(false)] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete(ObsoleteInternalUsageOnly, true)] + public static IDbDataParameter FindOrAddParameter(IDataParameterCollection parameters, IDbCommand command, string name) + { + IDbDataParameter result; + if (parameters.Contains(name)) + { + result = (IDbDataParameter)parameters[name]; + } + else + { + result = command.CreateParameter(); + result.ParameterName = name; + parameters.Add(result); + } + return result; + } + + internal static int GetListPaddingExtraCount(int count) + { + switch(count) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + return 0; // no padding + } + if (count < 0) return 0; + + int padFactor; + if (count <= 150) padFactor = 10; + else if (count <= 750) padFactor = 50; + else if (count <= 2000) padFactor = 100; // note: max param count for SQL Server + else if (count <= 2070) padFactor = 10; // try not to over-pad as we approach that limit + else if (count <= 2100) return 0; // just don't pad between 2070 and 2100, to minimize the crazy + else padFactor = 200; // above that, all bets are off! + + // if we have 17, factor = 10; 17 % 10 = 7, we need 3 more + int intoBlock = count % padFactor; + return intoBlock == 0 ? 0 : (padFactor - intoBlock); + } + + 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))?"); + /// + /// Internal use only + /// +#if !COREFX + [Browsable(false)] +#endif + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete(ObsoleteInternalUsageOnly, false)] + 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 + + if (FeatureSupport.Get(command.Connection).Arrays) + { + var arrayParm = command.CreateParameter(); + arrayParm.Value = SanitizeParameterValue(value); + arrayParm.ParameterName = namePrefix; + command.Parameters.Add(arrayParm); + } + else + { + bool byPosition = ShouldPassByPosition(command.CommandText); + var list = value as IEnumerable; + var count = 0; + bool isString = value is IEnumerable; + bool isDbString = value is IEnumerable; + DbType dbType = 0; + + int splitAt = SqlMapper.Settings.InListStringSplitCount; + bool viaSplit = splitAt >= 0 + && TryStringSplit(ref list, splitAt, namePrefix, command, byPosition); + + if (list != null && !viaSplit) + { + object lastValue = null; + foreach (var item in list) + { + if (++count == 1) // first item: fetch some type info + { + if(item == null) + { + throw new NotSupportedException("The first item in a list-expansion cannot be null"); + } + if (!isDbString) + { + ITypeHandler handler; + dbType = LookupDbType(item.GetType(), "", true, out handler); + } + } + var nextName = namePrefix + count.ToString(); + if (isDbString && item as DbString != null) + { + var str = item as DbString; + str.AddParameter(command, nextName); + } + else + { + var listParam = command.CreateParameter(); + listParam.ParameterName = nextName; + if (isString) + { + listParam.Size = DbString.DefaultLength; + if (item != null && ((string)item).Length > DbString.DefaultLength) + { + listParam.Size = -1; + } + } + + var tmp = listParam.Value = SanitizeParameterValue(item); + if (tmp != null && !(tmp is DBNull)) + lastValue = tmp; // only interested in non-trivial values for padding + + if (listParam.DbType != dbType) + { + listParam.DbType = dbType; + } + command.Parameters.Add(listParam); + } + } + if (Settings.PadListExpansions && !isDbString && lastValue != null) + { + int padCount = GetListPaddingExtraCount(count); + for (int i = 0; i < padCount; i++) + { + count++; + var padParam = command.CreateParameter(); + padParam.ParameterName = namePrefix + count.ToString(); + if(isString) padParam.Size = DbString.DefaultLength; + padParam.DbType = dbType; + padParam.Value = lastValue; + command.Parameters.Add(padParam); + } + } + } + + + if(viaSplit) + { + // already done + } + else + { + var regexIncludingUnknown = GetInListRegex(namePrefix, byPosition); + if (count == 0) + { + command.CommandText = Regex.Replace(command.CommandText, regexIncludingUnknown, match => + { + var variableName = match.Groups[1].Value; + if (match.Groups[2].Success) + { + // looks like an optimize hint; leave it alone! + return match.Value; + } + else + { + return "(SELECT " + variableName + " WHERE 1 = 0)"; + } + }, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant); + var dummyParam = command.CreateParameter(); + dummyParam.ParameterName = namePrefix; + dummyParam.Value = DBNull.Value; + command.Parameters.Add(dummyParam); + } + else + { + command.CommandText = Regex.Replace(command.CommandText, regexIncludingUnknown, match => + { + var variableName = match.Groups[1].Value; + if (match.Groups[2].Success) + { + // looks like an optimize hint; expand it + var suffix = match.Groups[2].Value; + + var sb = GetStringBuilder().Append(variableName).Append(1).Append(suffix); + for (int i = 2; i <= count; i++) + { + sb.Append(',').Append(variableName).Append(i).Append(suffix); + } + return sb.__ToStringRecycle(); + } + else + { + + var sb = GetStringBuilder().Append('(').Append(variableName); + if(!byPosition) sb.Append(1); + for (int i = 2; i <= count; i++) + { + sb.Append(',').Append(variableName); + if (!byPosition) sb.Append(i); + } + return sb.Append(')').__ToStringRecycle(); + } + }, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant); + } + } + } + } + + private static bool TryStringSplit(ref IEnumerable list, int splitAt, string namePrefix, IDbCommand command, bool byPosition) + { + if (list == null || splitAt < 0) return false; + if (list is IEnumerable) return TryStringSplit(ref list, splitAt, namePrefix, command, "int", byPosition, + (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); + if (list is IEnumerable) return TryStringSplit(ref list, splitAt, namePrefix, command, "bigint", byPosition, + (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); + if (list is IEnumerable) return TryStringSplit(ref list, splitAt, namePrefix, command, "smallint", byPosition, + (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); + if (list is IEnumerable) return TryStringSplit(ref list, splitAt, namePrefix, command, "tinyint", byPosition, + (sb, i) => sb.Append(i.ToString(CultureInfo.InvariantCulture))); + return false; + } + private static bool TryStringSplit(ref IEnumerable list, int splitAt, string namePrefix, IDbCommand command, string colType, bool byPosition, + Action append) + { + ICollection typed = list as ICollection; + if(typed == null) + { + typed = ((IEnumerable)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; + var regexIncludingUnknown = GetInListRegex(namePrefix, byPosition); + var sql = Regex.Replace(command.CommandText, regexIncludingUnknown, match => + { + var variableName = match.Groups[1].Value; + if (match.Groups[2].Success) + { + // looks like an optimize hint; leave it alone! + return match.Value; + } + else + { + varName = variableName; + 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! + + command.CommandText = sql; + var concatenatedParam = command.CreateParameter(); + concatenatedParam.ParameterName = namePrefix; + concatenatedParam.DbType = DbType.AnsiString; + concatenatedParam.Size = -1; + string val; + using (var iter = typed.GetEnumerator()) + { + if(iter.MoveNext()) + { + var sb = GetStringBuilder(); + append(sb, iter.Current); + while(iter.MoveNext()) + { + append(sb.Append(','), iter.Current); + } + val = sb.ToString(); + } + else + { + val = ""; + } + } + concatenatedParam.Value = val; + command.Parameters.Add(concatenatedParam); + return true; + } + + /// + /// OBSOLETE: For internal usage only. Sanitizes the paramter value with proper type casting. + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] + public static object SanitizeParameterValue(object value) + { + if (value == 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())); + } + switch (typeCode) + { + case TypeCode.Byte: return (byte)value; + case TypeCode.SByte: return (sbyte)value; + case TypeCode.Int16: return (short)value; + case TypeCode.Int32: return (int)value; + case TypeCode.Int64: return (long)value; + case TypeCode.UInt16: return (ushort)value; + case TypeCode.UInt32: return (uint)value; + case TypeCode.UInt64: return (ulong)value; + } + } + return value; + } + private static IEnumerable FilterParameters(IEnumerable parameters, string sql) + { + return parameters.Where(p => Regex.IsMatch(sql, @"[?@:]" + p.Name + "([^a-z0-9_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)); + } + + // look for ? / @ / : *by itself* + static readonly Regex smellsLikeOleDb = new Regex(@"(? + /// Replace all literal tokens with their text form + /// + 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); + /// + /// Convert numeric values to their string form for SQL literal purposes + /// + [Obsolete(ObsoleteInternalUsageOnly)] + public static string Format(object value) + { + if (value == null) + { + return "null"; + } + else + { + switch (TypeExtensions.GetTypeCode(value.GetType())) + { +#if !COREFX + case TypeCode.DBNull: + return "null"; +#endif + case TypeCode.Boolean: + return ((bool)value) ? "1" : "0"; + case TypeCode.Byte: + return ((byte)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.SByte: + return ((sbyte)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.UInt16: + return ((ushort)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.Int16: + return ((short)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.UInt32: + return ((uint)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.Int32: + return ((int)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.UInt64: + return ((ulong)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.Int64: + return ((long)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.Single: + return ((float)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.Double: + return ((double)value).ToString(CultureInfo.InvariantCulture); + case TypeCode.Decimal: + return ((decimal)value).ToString(CultureInfo.InvariantCulture); + default: + var multiExec = GetMultiExec(value); + if(multiExec != null) + { + StringBuilder sb = null; + bool first = true; + foreach (object subval in multiExec) + { + if(first) + { + sb = GetStringBuilder().Append('('); + first = false; + } + else + { + sb.Append(','); + } + sb.Append(Format(subval)); + } + if(first) + { + return "(select null where 1=0)"; + } + else + { + return sb.Append(')').__ToStringRecycle(); + } + } + throw new NotSupportedException(value.GetType().Name); + } + } + } + + + internal static void ReplaceLiterals(IParameterLookup parameters, IDbCommand command, IList tokens) + { + var sql = command.CommandText; + foreach (var token in tokens) + { + object value = parameters[token.Member]; +#pragma warning disable 0618 + string text = Format(value); +#pragma warning restore 0618 + sql = sql.Replace(token.Token, text); + } + command.CommandText = sql; + } + + internal static IList GetLiteralTokens(string sql) + { + if (string.IsNullOrEmpty(sql)) return LiteralToken.None; + if (!literalTokens.IsMatch(sql)) return LiteralToken.None; + + var matches = literalTokens.Matches(sql); + var found = new HashSet(StringComparer.Ordinal); + List list = new List(matches.Count); + foreach(Match match in matches) + { + string token = match.Value; + if(found.Add(match.Value)) + { + list.Add(new LiteralToken(token, match.Groups[1].Value)); + } + } + return list.Count == 0 ? LiteralToken.None : list; + } + + /// + /// Internal use only + /// + public static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) + { + return CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.sql)); + } + + internal static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList literals) + { + Type type = identity.parametersType; + + bool filterParams = false; + if (removeUnused && identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) + { + filterParams = !smellsLikeOleDb.IsMatch(identity.sql); + } + var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); + + var il = dm.GetILGenerator(); + + bool isStruct = type.IsValueType(); + bool haveInt32Arg1 = false; + il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param] + if (isStruct) + { + il.DeclareLocal(type.MakePointerType()); + il.Emit(OpCodes.Unbox, type); // stack is now [typed-param] + } + else + { + il.DeclareLocal(type); // 0 + il.Emit(OpCodes.Castclass, type); // stack is now [typed-param] + } + il.Emit(OpCodes.Stloc_0);// 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] + + var propsArr = type.GetProperties().Where(p => p.GetIndexParameters().Length == 0).ToArray(); + var ctors = type.GetConstructors(); + ParameterInfo[] ctorParams; + 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 && propsArr.Length == (ctorParams = ctors[0].GetParameters()).Length) + { + // check if reflection was kind enough to put everything in the right order for us + bool ok = true; + for (int i = 0; i < propsArr.Length; i++) + { + if (!string.Equals(propsArr[i].Name, ctorParams[i].Name, StringComparison.OrdinalIgnoreCase)) + { + ok = false; + break; + } + } + if(ok) + { + // pre-sorted; the reflection gods have smiled upon us + props = propsArr; + } + else { // might still all be accounted for; check the hard way + var positionByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(var param in ctorParams) + { + positionByName[param.Name] = param.Position; + } + if (positionByName.Count == propsArr.Length) + { + int[] positions = new int[propsArr.Length]; + ok = true; + for (int i = 0; i < propsArr.Length; i++) + { + int pos; + if (!positionByName.TryGetValue(propsArr[i].Name, out pos)) + { + ok = false; + break; + } + positions[i] = pos; + } + if (ok) + { + Array.Sort(positions, propsArr); + props = propsArr; + } + } + } + } + if(props == null) props = propsArr.OrderBy(x => x.Name); + if (filterParams) + { + props = FilterParameters(props, identity.sql); + } + + var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt; + foreach (var prop in props) + { + 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.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] + continue; + } + ITypeHandler handler; +#pragma warning disable 618 + DbType dbType = LookupDbType(prop.PropertyType, prop.Name, true, out 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.Box, prop.PropertyType); // stack is [parameters] [command] [name] [boxed-value] + } + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(nameof(SqlMapper.PackListParameters)), null); // stack is [parameters] + continue; + } + il.Emit(OpCodes.Dup); // stack is now [parameters] [parameters] + + il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [parameters] [command] + + if (checkForDuplicates) + { + // 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] + } + 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.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] + } + 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 + { + il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] + if (dbType == 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] + } + else + { + // constant value; nice and simple + EmitInt32(il, (int)dbType);// 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.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.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] + bool checkForNull; + if (prop.PropertyType.IsValueType()) + { + var propType = prop.PropertyType; + var nullType = Nullable.GetUnderlyingType(propType); + bool callSanitize = false; + + if((nullType ?? propType).IsEnum()) + { + if(nullType != null) + { + // Nullable; we want to box as the underlying type; that's just *hard*; for + // simplicity, box as Nullable and call SanitizeParameterValue + callSanitize = checkForNull = true; + } + else + { + checkForNull = false; + // non-nullable enum; we can do that! just box to the wrong type! (no, really) + switch (TypeExtensions.GetTypeCode(Enum.GetUnderlyingType(propType))) + { + case TypeCode.Byte: propType = typeof(byte); break; + case TypeCode.SByte: propType = typeof(sbyte); break; + case TypeCode.Int16: propType = typeof(short); break; + case TypeCode.Int32: propType = typeof(int); break; + case TypeCode.Int64: propType = typeof(long); break; + case TypeCode.UInt16: propType = typeof(ushort); break; + case TypeCode.UInt32: propType = typeof(uint); break; + case TypeCode.UInt64: propType = typeof(ulong); break; + } + } + } + else + { + checkForNull = nullType != 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); + // stack is [parameters] [[parameters]] [parameter] [parameter] [boxed-value] + } + } else + { + checkForNull = true; // if not a value-type, need to check + } + 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(); + Label? allDone = (dbType == DbType.String || dbType == DbType.AnsiString) ? il.DefineLabel() : (Label?)null; + 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] + if (dbType == DbType.String || dbType == DbType.AnsiString) + { + EmitInt32(il, 0); + il.Emit(OpCodes.Stloc_1); + } + if (allDone != 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] + EmitInt32(il, DbString.DefaultLength); // [string] [length] [4000] + il.Emit(OpCodes.Cgt); // [string] [0 or 1] + Label isLong = il.DefineLabel(), lenDone = il.DefineLabel(); + il.Emit(OpCodes.Brtrue_S, isLong); + EmitInt32(il, DbString.DefaultLength); // [string] [4000] + il.Emit(OpCodes.Br_S, lenDone); + il.MarkLabel(isLong); + EmitInt32(il, -1); // [string] [-1] + il.MarkLabel(lenDone); + il.Emit(OpCodes.Stloc_1); // [string] + } + if (prop.PropertyType.FullName == LinqBinary) + { + il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod("ToArray", BindingFlags.Public | BindingFlags.Instance), null); + } + if (allDone != null) il.MarkLabel(allDone.Value); + // relative stack [boxed value or DBNull] + } + + if (handler != null) + { +#pragma warning disable 618 + 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] + } + + if (prop.PropertyType == typeof(string)) + { + var endOfSize = il.DefineLabel(); + // don't set if 0 + il.Emit(OpCodes.Ldloc_1); // [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.MarkLabel(endOfSize); + } + if (checkForDuplicates) + { + // stack is now [parameters] [parameter] + il.Emit(OpCodes.Pop); // don't need parameter any more + } + else + { + // stack is now [parameters] [parameters] [parameter] + // blindly add + 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 + } + } + + // stack is currently [parameters] + il.Emit(OpCodes.Pop); // stack is now empty + + if(literals.Count != 0 && propsArr != 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; + foreach (var literal in literals) + { + // find the best member, preferring case-sensitive + PropertyInfo exact = null, fallback = null; + string huntName = literal.Member; + for(int i = 0; i < propsArr.Length;i++) + { + string thisName = propsArr[i].Name; + if(string.Equals(thisName, huntName, StringComparison.OrdinalIgnoreCase)) + { + fallback = propsArr[i]; + if(string.Equals(thisName, huntName, StringComparison.Ordinal)) + { + exact = fallback; + break; + } + } + } + var prop = exact ?? fallback; + + if(prop != 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 + Type propType = prop.PropertyType; + var typeCode = TypeExtensions.GetTypeCode(propType); + switch (typeCode) + { + case TypeCode.Boolean: + Label ifTrue = il.DefineLabel(), allDone = il.DefineLabel(); + il.Emit(OpCodes.Brtrue_S, ifTrue); + il.Emit(OpCodes.Ldstr, "0"); + il.Emit(OpCodes.Br_S, allDone); + il.MarkLabel(ifTrue); + il.Emit(OpCodes.Ldstr, "1"); + il.MarkLabel(allDone); + break; + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.Int16: + case TypeCode.UInt32: + case TypeCode.Int32: + case TypeCode.UInt64: + case TypeCode.Int64: + case TypeCode.Single: + case TypeCode.Double: + 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) + { + if (locals == null) + { + locals = new Dictionary(); + local = null; + } + else + { + if (!locals.TryGetValue(propType, out local)) local = null; + } + if (local == null) + { + local = il.DeclareLocal(propType); + locals.Add(propType, local); + } + } + il.Emit(OpCodes.Stloc, local); // command, sql + il.Emit(OpCodes.Ldloca, local); // command, sql, ref-to-value + il.EmitCall(OpCodes.Call, InvariantCulture, null); // command, sql, ref-to-value, culture + il.EmitCall(OpCodes.Call, convert, null); // command, sql, string value + break; + default: + 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.Emit(OpCodes.Ret); + return (Action)dm.CreateDelegate(typeof(Action)); + } + 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) })); + static MethodInfo GetToString(TypeCode typeCode) + { + MethodInfo method; + return toStrings.TryGetValue(typeCode, out method) ? method : null; + } + 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 int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action paramReader) + { + IDbCommand cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + try + { + cmd = command.SetupCommand(cnn, paramReader); + if (wasClosed) cnn.Open(); + int result = cmd.ExecuteNonQuery(); + command.OnCompleted(); + return result; + } + finally + { + if (wasClosed) cnn.Close(); + cmd?.Dispose(); + } + } + + private static T ExecuteScalarImpl(IDbConnection cnn, ref CommandDefinition command) + { + Action paramReader = null; + object param = command.Parameters; + if (param != null) + { + var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; + } + + IDbCommand cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed; + object result; + try + { + cmd = command.SetupCommand(cnn, paramReader); + if (wasClosed) cnn.Open(); + result =cmd.ExecuteScalar(); + command.OnCompleted(); + } + finally + { + if (wasClosed) cnn.Close(); + cmd?.Dispose(); + } + return Parse(result); + } + + private static IDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefinition command, CommandBehavior commandBehavior, out IDbCommand cmd) + { + Action paramReader = GetParameterReader(cnn, ref command); + cmd = null; + bool wasClosed = cnn.State == ConnectionState.Closed, disposeCommand = true; + try + { + cmd = command.SetupCommand(cnn, paramReader); + if (wasClosed) cnn.Open(); + var reader = ExecuteReaderWithFlagsFallback(cmd, wasClosed, commandBehavior); + wasClosed = false; // don't dispose before giving it to them! + disposeCommand = false; + // note: command.FireOutputCallbacks(); would be useless here; parameters come at the **end** of the TDS stream + return reader; + } + finally + { + if (wasClosed) cnn.Close(); + if (cmd != null && disposeCommand) cmd.Dispose(); + } + } + + private static Action GetParameterReader(IDbConnection cnn, ref CommandDefinition command) + { + object param = command.Parameters; + IEnumerable multiExec = GetMultiExec(param); + CacheInfo info = null; + if (multiExec != null) + { + throw new NotSupportedException("MultiExec is not supported by ExecuteReader"); + } + + // nice and simple + if (param != null) + { + var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType(), null); + info = GetCacheInfo(identity, param, command.AddToCache); + } + var paramReader = info?.ParamReader; + return paramReader; + } + + private static Func GetStructDeserializer(Type type, Type effectiveType, int index) + { + // 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 + if (type == typeof(char)) + { // this *does* need special handling, though + return r => ReadChar(r.GetValue(index)); + } + if (type == typeof(char?)) + { + return r => ReadNullableChar(r.GetValue(index)); + } + if (type.FullName == LinqBinary) + { + 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 + return r => + { + var val = r.GetValue(index); + if(val is float || val is double || val is decimal) + { + val = Convert.ChangeType(val, Enum.GetUnderlyingType(effectiveType), CultureInfo.InvariantCulture); + } + return val is DBNull ? null : Enum.ToObject(effectiveType, val); + }; + } + ITypeHandler handler; + if(typeHandlers.TryGetValue(type, out handler)) + { + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null : handler.Parse(type, val); + }; + } + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null : val; + }; + } + + private static T Parse(object value) + { + if (value == null || value is DBNull) return default(T); + if (value is T) return (T)value; + var type = typeof(T); + type = Nullable.GetUnderlyingType(type) ?? type; + if (type.IsEnum()) + { + 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); + } + ITypeHandler handler; + if (typeHandlers.TryGetValue(type, out handler)) + { + return (T)handler.Parse(type, value); + } + return (T)Convert.ChangeType(value, type, CultureInfo.InvariantCulture); + } + + 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) + .Where(p => p.GetIndexParameters().Any() && p.GetIndexParameters()[0].ParameterType == typeof(int)) + .Select(p => p.GetGetMethod()).First(); + + /// + /// Gets type-map for the given type + /// + /// Type map instance, default is to create new instance of DefaultTypeMap + public static Func TypeMapProvider = ( Type type ) => new DefaultTypeMap( type ); + + /// + /// Gets type-map for the given type + /// + /// 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) + { + 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]; + + if (map == null) + { + map = TypeMapProvider( type ); + _typeMaps[type] = map; + } + } + } + return map; + } + + // use Hashtable to get free lockless reading + private static readonly Hashtable _typeMaps = new Hashtable(); + + /// + /// 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) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (map == null || map is DefaultTypeMap) + { + lock (_typeMaps) + { + _typeMaps.Remove(type); + } + } + else + { + lock (_typeMaps) + { + _typeMaps[type] = map; + } + } + + PurgeQueryCacheByType(type); + } + + /// + /// Internal use only + /// + /// + /// + /// + /// + /// + /// + public static Func GetTypeDeserializer( + Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false + ) + { + return TypeDeserializerCache.GetReader(type, reader, startBound, length, returnNullIfFirstMissing); + } + static LocalBuilder GetTempLocal(ILGenerator il, ref Dictionary locals, Type type, bool initAndLoad) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (locals == null) locals = new Dictionary(); + LocalBuilder found; + if (!locals.TryGetValue(type, out found)) + { + found = il.DeclareLocal(type); + locals.Add(type, found); + } + if (initAndLoad) + { + il.Emit(OpCodes.Ldloca, (short)found.LocalIndex); + il.Emit(OpCodes.Initobj, type); + il.Emit(OpCodes.Ldloca, (short)found.LocalIndex); + il.Emit(OpCodes.Ldobj, type); + } + return found; + } + private static Func GetTypeDeserializerImpl( + Type type, IDataReader 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; + } + + if (reader.FieldCount <= startBound) + { + throw MultiMapException(reader); + } + + var names = Enumerable.Range(startBound, length).Select(i => reader.GetName(i)).ToArray(); + + ITypeMap typeMap = GetTypeMap(type); + + int index = startBound; + + ConstructorInfo specializedConstructor = null; + +#if !COREFX + bool supportInitialize = false; +#endif + Dictionary structLocals = null; + if (type.IsValueType()) + { + il.Emit(OpCodes.Ldloca_S, (byte)1); + il.Emit(OpCodes.Initobj, type); + } + else + { + var types = new Type[length]; + for (int i = startBound; i < startBound + length; i++) + { + types[i - startBound] = reader.GetFieldType(i); + } + + var explicitConstr = typeMap.FindExplicitConstructor(); + if (explicitConstr != null) + { + var consPs = explicitConstr.GetParameters(); + foreach(var p in consPs) + { + if(!p.ParameterType.IsValueType()) + { + il.Emit(OpCodes.Ldnull); + } + else + { + GetTempLocal(il, ref structLocals, p.ParameterType, true); + } + } + + il.Emit(OpCodes.Newobj, explicitConstr); + il.Emit(OpCodes.Stloc_1); +#if !COREFX + supportInitialize = typeof(ISupportInitialize).IsAssignableFrom(type); + if (supportInitialize) + { + il.Emit(OpCodes.Ldloc_1); + il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.BeginInit)), null); + } +#endif + } + else + { + var ctor = typeMap.FindConstructor(names, types); + if (ctor == 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"); + } + + if (ctor.GetParameters().Length == 0) + { + il.Emit(OpCodes.Newobj, ctor); + il.Emit(OpCodes.Stloc_1); +#if !COREFX + supportInitialize = typeof(ISupportInitialize).IsAssignableFrom(type); + if (supportInitialize) + { + il.Emit(OpCodes.Ldloc_1); + il.EmitCall(OpCodes.Callvirt, typeof(ISupportInitialize).GetMethod(nameof(ISupportInitialize.BeginInit)), null); + } +#endif + } + else + { + specializedConstructor = ctor; + } + } + } + + il.BeginExceptionBlock(); + if (type.IsValueType()) + { + il.Emit(OpCodes.Ldloca_S, (byte)1);// [target] + } + else if (specializedConstructor == null) + { + il.Emit(OpCodes.Ldloc_1);// [target] + } + + var members = (specializedConstructor != null + ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) + : names.Select(n => typeMap.GetMember(n))).ToList(); + + // stack is now [target] + + bool first = true; + var allDone = il.DefineLabel(); + int enumDeclareLocal = -1, valueCopyLocal = il.DeclareLocal(typeof(object)).LocalIndex; + bool applyNullSetting = Settings.ApplyNullValues; + foreach (var item in members) + { + if (item != null) + { + if (specializedConstructor == 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 + + var nullUnderlyingType = Nullable.GetUnderlyingType(memberType); + var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum() ? 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); + } + + 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) + { + // Store the value in the property/field + if (item.Property != null) + { + il.Emit(type.IsValueType() ? OpCodes.Call : OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); + } + else + { + 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.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); + } + } + else if(applyNullSetting && (!memberType.IsValueType() || Nullable.GetUnderlyingType(memberType) != null)) + { + il.Emit(OpCodes.Pop); // stack is now [target][target] + // can load a null with this value + if (memberType.IsValueType()) + { // must be Nullable for some T + GetTempLocal(il, ref structLocals, memberType, true); // stack is now [target][target][null] + } + else + { // regular reference-type + il.Emit(OpCodes.Ldnull); // stack is now [target][target][null] + } + + // Store the value in the property/field + if (item.Property != null) + { + il.Emit(type.IsValueType() ? OpCodes.Call : OpCodes.Callvirt, DefaultTypeMap.GetPropertySetter(item.Property, type)); + // stack is now [target] + } + else + { + 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); + il.Emit(OpCodes.Br, allDone); + } + + il.MarkLabel(finishLabel); + } + first = false; + index += 1; + } + if (type.IsValueType()) + { + il.Emit(OpCodes.Pop); + } + else + { + if (specializedConstructor != null) + { + il.Emit(OpCodes.Newobj, specializedConstructor); + } + il.Emit(OpCodes.Stloc_1); // stack is empty +#if !COREFX + if (supportInitialize) + { + il.Emit(OpCodes.Ldloc_1); + 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.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.EndExceptionBlock(); + + il.Emit(OpCodes.Ldloc_1); // 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 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) + { + // this is handy for things like decimal <===> double + il.Emit(OpCodes.Unbox_Any, from); // stack is now [target][target][data-typed-value] + il.Emit(OpCodes.Call, op); // stack is now [target][target][typed-value] + } + else + { + bool handled = false; + OpCode opCode = default(OpCode); + switch (TypeExtensions.GetTypeCode(from)) + { + case TypeCode.Boolean: + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.UInt16: + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + case TypeCode.Single: + case TypeCode.Double: + handled = true; + switch (TypeExtensions.GetTypeCode(via ?? to)) + { + case TypeCode.Byte: + opCode = OpCodes.Conv_Ovf_I1_Un; break; + case TypeCode.SByte: + opCode = OpCodes.Conv_Ovf_I1; break; + case TypeCode.UInt16: + opCode = OpCodes.Conv_Ovf_I2_Un; break; + case TypeCode.Int16: + opCode = OpCodes.Conv_Ovf_I2; break; + case TypeCode.UInt32: + opCode = OpCodes.Conv_Ovf_I4_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; + case TypeCode.Int64: + opCode = OpCodes.Conv_Ovf_I8; break; + case TypeCode.Single: + opCode = OpCodes.Conv_R4; break; + case TypeCode.Double: + opCode = OpCodes.Conv_R8; break; + default: + handled = false; + break; + } + break; + } + if (handled) + { + il.Emit(OpCodes.Unbox_Any, from); // stack is now [target][target][col-typed-value] + il.Emit(opCode); // stack is now [target][target][typed-value] + if (to == typeof(bool)) + { // compare to zero; I checked "csc" - this is the trick it uses; nice + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + } + } + 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.Emit(OpCodes.Unbox_Any, to); // stack is now [target][target][typed-value] + } + } + } + + static MethodInfo GetOperator(Type from, Type to) + { + if (to == 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") + ?? ResolveOperator(fromMethods, from, to, "op_Explicit") + ?? ResolveOperator(toMethods, from, to, "op_Explicit"); + } + + static MethodInfo ResolveOperator(MethodInfo[] methods, Type from, Type to, string name) + { + for (int i = 0; i < methods.Length; i++) + { + if (methods[i].Name != name || methods[i].ReturnType != to) continue; + var args = methods[i].GetParameters(); + if (args.Length != 1 || args[0].ParameterType != from) continue; + return methods[i]; + } + 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); + } + } + + /// + /// Throws a data exception, only used internally + /// + [Obsolete(ObsoleteInternalUsageOnly, false)] + 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) + { + name = reader.GetName(index); + try + { + if (value == null || value is DBNull) + { + formattedValue = ""; + } + else + { + formattedValue = Convert.ToString(value) + " - " + TypeExtensions.GetTypeCode(value.GetType()); + } + } + catch (Exception valEx) + { + formattedValue = valEx.Message; + } + } + toThrow = new DataException($"Error parsing column {index} ({name}={formattedValue})", ex); + } + catch + { // throw the **original** exception, wrapped as DataException + toThrow = new DataException(ex.Message, ex); + } + throw toThrow; + } + + private static void EmitInt32(ILGenerator il, int value) + { + switch (value) + { + case -1: il.Emit(OpCodes.Ldc_I4_M1); break; + case 0: il.Emit(OpCodes.Ldc_I4_0); break; + case 1: il.Emit(OpCodes.Ldc_I4_1); break; + case 2: il.Emit(OpCodes.Ldc_I4_2); break; + case 3: il.Emit(OpCodes.Ldc_I4_3); break; + case 4: il.Emit(OpCodes.Ldc_I4_4); break; + case 5: il.Emit(OpCodes.Ldc_I4_5); break; + case 6: il.Emit(OpCodes.Ldc_I4_6); break; + case 7: il.Emit(OpCodes.Ldc_I4_7); break; + case 8: il.Emit(OpCodes.Ldc_I4_8); break; + default: + if (value >= -128 && value <= 127) + { + il.Emit(OpCodes.Ldc_I4_S, (sbyte)value); + } + else + { + il.Emit(OpCodes.Ldc_I4, value); + } + break; + } + } + + /// + /// How should connection strings be compared for equivalence? Defaults to StringComparer.Ordinal. + /// Providing a custom implementation can be useful for allowing multi-tenancy databases with identical + /// schema to share strategies. Note that usual equivalence rules apply: any equivalent connection strings + /// MUST yield the same hash-code. + /// + public static IEqualityComparer ConnectionStringComparer + { + get { return connectionStringComparer; } + set { connectionStringComparer = value ?? StringComparer.Ordinal; } + } + private static IEqualityComparer connectionStringComparer = StringComparer.Ordinal; + +#if !COREFX + /// + /// Key used to indicate the type name associated with a DataTable + /// + private const string DataTableTypeNameKey = "dapper:TypeName"; + + /// + /// Used to pass a DataTable as a TableValuedParameter + /// + public static ICustomQueryParameter AsTableValuedParameter(this DataTable table, string typeName = null) + { + return new TableValuedParameter(table, typeName); + } + + /// + /// Associate a DataTable with a type name + /// + public static void SetTypeName(this DataTable table, string typeName) + { + if (table != null) + { + if (string.IsNullOrEmpty(typeName)) + table.ExtendedProperties.Remove(DataTableTypeNameKey); + else + table.ExtendedProperties[DataTableTypeNameKey] = typeName; + } + } + + /// + /// Fetch the type name associated with a DataTable + /// + public static string GetTypeName(this DataTable table) + { + return table?.ExtendedProperties[DataTableTypeNameKey] as string; + } + + /// + /// Used to pass a IEnumerable<SqlDataRecord> as a TableValuedParameter + /// + public static ICustomQueryParameter AsTableValuedParameter(this IEnumerable list, string typeName = null) + { + return new SqlDataRecordListTVPParameter(list, typeName); + } + +#endif + + // one per thread + [ThreadStatic] + private static StringBuilder perThreadStringBuilderCache; + private static StringBuilder GetStringBuilder() + { + var tmp = perThreadStringBuilderCache; + if (tmp != null) + { + perThreadStringBuilderCache = null; + tmp.Length = 0; + return tmp; + } + return new StringBuilder(); + } + + private static string __ToStringRecycle(this StringBuilder obj) + { + if (obj == null) return ""; + var s = obj.ToString(); + if(perThreadStringBuilderCache == null) + { + perThreadStringBuilderCache = obj; + } + return s; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/TableValuedParameter.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/TableValuedParameter.cs new file mode 100644 index 0000000..6110344 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/TableValuedParameter.cs @@ -0,0 +1,66 @@ +using System; +using System.Data; +using System.Reflection; + +#if !COREFX +namespace Dapper +{ + /// + /// Used to pass a DataTable as a TableValuedParameter + /// + sealed class TableValuedParameter : SqlMapper.ICustomQueryParameter + { + private readonly DataTable table; + private readonly string typeName; + + /// + /// Create a new instance of TableValuedParameter + /// + public TableValuedParameter(DataTable table) : this(table, null) { } + /// + /// Create a new instance of TableValuedParameter + /// + public TableValuedParameter(DataTable table, string typeName) + { + this.table = table; + this.typeName = typeName; + } + 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(); + param.ParameterName = name; + Set(param, table, typeName); + command.Parameters.Add(param); + } + 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) + { + typeName = table.GetTypeName(); + } + if (!string.IsNullOrEmpty(typeName)) + { + var sqlParam = parameter as System.Data.SqlClient.SqlParameter; + if (sqlParam != null) + { + setTypeName?.Invoke(sqlParam, typeName); + sqlParam.SqlDbType = SqlDbType.Structured; + } + } + } + } +} +#endif \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/TypeExtensions.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/TypeExtensions.cs new file mode 100644 index 0000000..c89e190 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/TypeExtensions.cs @@ -0,0 +1,106 @@ +using System; +using System.Reflection; +using System.Collections.Generic; + +namespace Dapper +{ + internal static class TypeExtensions + { + public static string Name(this Type type) + { +#if COREFX + return type.GetTypeInfo().Name; +#else + return type.Name; +#endif + } + + public static bool IsValueType(this Type type) + { +#if COREFX + return type.GetTypeInfo().IsValueType; +#else + return type.IsValueType; +#endif + } + public static bool IsEnum(this Type type) + { +#if COREFX + return type.GetTypeInfo().IsEnum; +#else + return type.IsEnum; +#endif + } + public static bool IsGenericType(this Type type) + { +#if COREFX + return type.GetTypeInfo().IsGenericType; +#else + return type.IsGenericType; +#endif + } + public static bool IsInterface(this Type type) + { +#if COREFX + return type.GetTypeInfo().IsInterface; +#else + return type.IsInterface; +#endif + } +#if COREFX + 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; + TypeCode result; + if (typeCodeLookup.TryGetValue(type, out result)) return result; + + if (type.IsEnum()) + { + type = Enum.GetUnderlyingType(type); + if (typeCodeLookup.TryGetValue(type, out result)) return result; + } + return TypeCode.Object; + } + 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) + { + return Type.GetTypeCode(type); + } +#endif + public static MethodInfo GetPublicInstanceMethod(this Type type, string name, Type[] types) + { +#if COREFX + var method = type.GetMethod(name, types); + return (method != null && method.IsPublic && !method.IsStatic) ? method : null; +#else + return type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public, null, types, null); +#endif + } + + + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/UdtTypeHandler.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/UdtTypeHandler.cs new file mode 100644 index 0000000..d740a1e --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/UdtTypeHandler.cs @@ -0,0 +1,43 @@ +using System; +using System.Data; + +namespace Dapper +{ + partial class SqlMapper + { +#if !COREFX + /// + /// A type handler for data-types that are supported by the underlying provider, but which need + /// a well-known UdtTypeName to be specified + /// + public class UdtTypeHandler : ITypeHandler + { + private readonly string udtTypeName; + /// + /// Creates a new instance of UdtTypeHandler with the specified UdtTypeName + /// + public UdtTypeHandler(string udtTypeName) + { + if (string.IsNullOrEmpty(udtTypeName)) throw new ArgumentException("Cannot be null or empty", udtTypeName); + this.udtTypeName = udtTypeName; + } + object ITypeHandler.Parse(Type destinationType, object value) + { + return value is DBNull ? null : value; + } + + 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; + } + } + } +#endif + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedDataReader.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedDataReader.cs new file mode 100644 index 0000000..93c4bf0 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedDataReader.cs @@ -0,0 +1,20 @@ +using System.Data; + +namespace Dapper +{ + /// + /// Describes a reader that controls the lifetime of both a command and a reader, + /// exposing the downstream command/reader as properties. + /// + public interface IWrappedDataReader : IDataReader + { + /// + /// Obtain the underlying reader + /// + IDataReader Reader { get; } + /// + /// Obtain the underlying command + /// + IDbCommand Command { get; } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedReader.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedReader.cs new file mode 100644 index 0000000..65002fa --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/WrappedReader.cs @@ -0,0 +1,186 @@ +using System; +using System.Data; + +namespace Dapper +{ + internal class WrappedReader : IDataReader, IWrappedDataReader + { + private IDataReader reader; + private IDbCommand cmd; + + public IDataReader Reader + { + get + { + var tmp = reader; + if (tmp == null) throw new ObjectDisposedException(GetType().Name); + return tmp; + } + } + IDbCommand IWrappedDataReader.Command + { + get + { + var tmp = cmd; + if (tmp == null) throw new ObjectDisposedException(GetType().Name); + return tmp; + } + } + public WrappedReader(IDbCommand cmd, IDataReader reader) + { + this.cmd = cmd; + this.reader = reader; + } + + void IDataReader.Close() + { + reader?.Close(); + } + + int IDataReader.Depth => Reader.Depth; + + DataTable IDataReader.GetSchemaTable() + { + return Reader.GetSchemaTable(); + } + + bool IDataReader.IsClosed => reader?.IsClosed ?? true; + + bool IDataReader.NextResult() + { + return Reader.NextResult(); + } + + bool IDataReader.Read() + { + return Reader.Read(); + } + + int IDataReader.RecordsAffected => Reader.RecordsAffected; + + void IDisposable.Dispose() + { + reader?.Close(); + reader?.Dispose(); + reader = null; + cmd?.Dispose(); + cmd = null; + } + + int IDataRecord.FieldCount => Reader.FieldCount; + + bool IDataRecord.GetBoolean(int i) + { + return Reader.GetBoolean(i); + } + + byte IDataRecord.GetByte(int i) + { + return Reader.GetByte(i); + } + + long IDataRecord.GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) + { + return Reader.GetBytes(i, fieldOffset, buffer, bufferoffset, length); + } + + char IDataRecord.GetChar(int i) + { + return Reader.GetChar(i); + } + + long IDataRecord.GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) + { + return Reader.GetChars(i, fieldoffset, buffer, bufferoffset, length); + } + + IDataReader IDataRecord.GetData(int i) + { + return Reader.GetData(i); + } + + string IDataRecord.GetDataTypeName(int i) + { + return Reader.GetDataTypeName(i); + } + + DateTime IDataRecord.GetDateTime(int i) + { + return Reader.GetDateTime(i); + } + + decimal IDataRecord.GetDecimal(int i) + { + return Reader.GetDecimal(i); + } + + double IDataRecord.GetDouble(int i) + { + return Reader.GetDouble(i); + } + + Type IDataRecord.GetFieldType(int i) + { + return Reader.GetFieldType(i); + } + + float IDataRecord.GetFloat(int i) + { + return Reader.GetFloat(i); + } + + Guid IDataRecord.GetGuid(int i) + { + return Reader.GetGuid(i); + } + + short IDataRecord.GetInt16(int i) + { + return Reader.GetInt16(i); + } + + int IDataRecord.GetInt32(int i) + { + return Reader.GetInt32(i); + } + + long IDataRecord.GetInt64(int i) + { + return Reader.GetInt64(i); + } + + string IDataRecord.GetName(int i) + { + return Reader.GetName(i); + } + + int IDataRecord.GetOrdinal(string name) + { + return Reader.GetOrdinal(name); + } + + string IDataRecord.GetString(int i) + { + return Reader.GetString(i); + } + + object IDataRecord.GetValue(int i) + { + return Reader.GetValue(i); + } + + int IDataRecord.GetValues(object[] values) + { + return Reader.GetValues(values); + } + + bool IDataRecord.IsDBNull(int i) + { + return Reader.IsDBNull(i); + } + + object IDataRecord.this[string name] => Reader[name]; + + object IDataRecord.this[int i] => Reader[i]; + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/XmlHandlers.cs b/Api/Ewide.Core/Ewide.Core.Data/Dapper/XmlHandlers.cs new file mode 100644 index 0000000..12c1723 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/XmlHandlers.cs @@ -0,0 +1,35 @@ +using System.Data; +using System.Xml; +using System.Xml.Linq; + +namespace Dapper +{ + internal abstract class XmlTypeHandler : SqlMapper.StringTypeHandler + { + public override void SetValue(IDbDataParameter parameter, T value) + { + base.SetValue(parameter, value); + parameter.DbType = DbType.Xml; + } + } + internal sealed class XmlDocumentHandler : XmlTypeHandler + { + protected override XmlDocument Parse(string xml) + { + var doc = new XmlDocument(); + doc.LoadXml(xml); + return doc; + } + protected override string Format(XmlDocument xml) => xml.OuterXml; + } + internal sealed class XDocumentHandler : XmlTypeHandler + { + protected override XDocument Parse(string xml) => XDocument.Parse(xml); + protected override string Format(XDocument xml) => xml.ToString(); + } + internal sealed class XElementHandler : XmlTypeHandler + { + protected override XElement Parse(string xml) => XElement.Parse(xml); + protected override string Format(XElement xml) => xml.ToString(); + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Dapper/project.json b/Api/Ewide.Core/Ewide.Core.Data/Dapper/project.json new file mode 100644 index 0000000..ee5a72f --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Dapper/project.json @@ -0,0 +1,79 @@ +{ + "packOptions": { + "summary": "A high performance Micro-ORM", + "tags": [ "orm", "sql", "micro-orm" ], + "owners": [ "marc.gravell", "nick.craver" ], + "releaseNotes": "http://stackexchange.github.io/dapper-dot-net/", + "projectUrl": "https://github.com/StackExchange/dapper-dot-net", + "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0", + "repository": { + "type": "git", + "url": "https://github.com/StackExchange/dapper-dot-net" + } + }, + "version": "1.50.2-*", + "authors": [ "Sam Saffron", "Marc Gravell", "Nick Craver" ], + "description": "A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc..", + "title": "Dapper dot net", + "copyright": "2016 Stack Exchange, Inc.", + "dependencies": { + }, + "buildOptions": { + "xmlDoc": true, + "warningsAsErrors": true + }, + "frameworks": { + "net40": { + "frameworkAssemblies": { + "System.Data": "4.0.0.0", + "System.Xml": "4.0.0.0", + "System.Xml.Linq": "4.0.0.0" + } + }, + "net45": { + "buildOptions": { + "define": [ "ASYNC" ] + }, + "frameworkAssemblies": { + "System.Data": "4.0.0.0", + "System.Xml": "4.0.0.0", + "System.Xml.Linq": "4.0.0.0" + } + }, + "net451": { + "buildOptions": { + "define": [ "ASYNC" ] + }, + "frameworkAssemblies": { + "System.Data": "4.0.0.0", + "System.Xml": "4.0.0.0", + "System.Xml.Linq": "4.0.0.0" + } + }, + "netstandard1.3": { + "buildOptions": { + "define": [ "ASYNC", "COREFX" ] + }, + "dependencies": { + "System.Collections": "4.0.11", + "System.Collections.Concurrent": "4.0.12", + "System.Collections.NonGeneric": "4.0.1", + "System.Data.SqlClient": "4.1.0", + "System.Dynamic.Runtime": "4.0.11", + "System.Linq": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Emit": "4.0.1", + "System.Reflection.Emit.Lightweight": "4.0.1", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Text.RegularExpressions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.XDocument": "4.0.11", + "System.Xml.XmlDocument": "4.0.1" + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensions.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensions.cs new file mode 100644 index 0000000..b0a9c2f --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensions.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Text; +using DapperExtensions.Sql; +using DapperExtensions.Mapper; + +namespace DapperExtensions +{ + public static class DapperExtensions + { + private readonly static object _lock = new object(); + + private static Func _instanceFactory; + private static IDapperImplementor _instance; + private static IDapperExtensionsConfiguration _configuration; + + /// + /// Gets or sets the default class mapper to use when generating class maps. If not specified, AutoClassMapper is used. + /// DapperExtensions.Configure(Type, IList, ISqlDialect) can be used instead to set all values at once + /// + public static Type DefaultMapper + { + get + { + return _configuration.DefaultMapper; + } + + set + { + Configure(value, _configuration.MappingAssemblies, _configuration.Dialect); + } + } + + /// + /// Gets or sets the type of sql to be generated. + /// DapperExtensions.Configure(Type, IList, ISqlDialect) can be used instead to set all values at once + /// + public static ISqlDialect SqlDialect + { + get + { + return _configuration.Dialect; + } + + set + { + Configure(_configuration.DefaultMapper, _configuration.MappingAssemblies, value); + } + } + + /// + /// Get or sets the Dapper Extensions Implementation Factory. + /// + public static Func InstanceFactory + { + get + { + if (_instanceFactory == null) + { + _instanceFactory = config => new DapperImplementor(new SqlGeneratorImpl(config)); + } + + return _instanceFactory; + } + set + { + _instanceFactory = value; + Configure(_configuration.DefaultMapper, _configuration.MappingAssemblies, _configuration.Dialect); + } + } + + /// + /// Gets the Dapper Extensions Implementation + /// + private static IDapperImplementor Instance + { + get + { + if (_instance == null) + { + lock (_lock) + { + if (_instance == null) + { + _instance = InstanceFactory(_configuration); + } + } + } + + return _instance; + } + } + + static DapperExtensions() + { + Configure(typeof(AutoClassMapper<>), new List(), new MySqlDialect()); // 设置默认数据库类型 + } + + /// + /// Add other assemblies that Dapper Extensions will search if a mapping is not found in the same assembly of the POCO. + /// + /// + public static void SetMappingAssemblies(IList assemblies) + { + Configure(_configuration.DefaultMapper, assemblies, _configuration.Dialect); + } + + /// + /// Configure DapperExtensions extension methods. + /// + /// + /// + /// + public static void Configure(IDapperExtensionsConfiguration configuration) + { + _instance = null; + _configuration = configuration; + } + + /// + /// Configure DapperExtensions extension methods. + /// + /// + /// + /// + public static void Configure(Type defaultMapper, IList mappingAssemblies, ISqlDialect sqlDialect) + { + Configure(new DapperExtensionsConfiguration(defaultMapper, mappingAssemblies, sqlDialect)); + } + + /// + /// Executes a query for the specified id, returning the data typed as per T + /// + public static T Get(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + var result = Instance.Get(connection, id, transaction, commandTimeout); + return (T)result; + } + + /// + /// Executes an insert query for the specified entity. + /// + public static void Insert(this IDbConnection connection, IEnumerable entities, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + Instance.Insert(connection, entities, transaction, commandTimeout); + } + + /// + /// Executes an insert query for the specified entity, returning the primary key. + /// If the entity has a single key, just the value is returned. + /// If the entity has a composite key, an IDictionary<string, object> is returned with the key values. + /// The key value for the entity will also be updated if the KeyType is a Guid or Identity. + /// + public static dynamic Insert(this IDbConnection connection, T entity, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + return Instance.Insert(connection, entity, transaction, commandTimeout); + } + + /// + /// Executes an update query for the specified entity. + /// + public static bool Update(this IDbConnection connection, T entity, IDbTransaction transaction = null, int? commandTimeout = null, bool ignoreAllKeyProperties = false) where T : class + { + return Instance.Update(connection, entity, transaction, commandTimeout, ignoreAllKeyProperties); + } + + /// + /// Executes a delete query for the specified entity. + /// + public static bool Delete(this IDbConnection connection, T entity, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + return Instance.Delete(connection, entity, transaction, commandTimeout); + } + + /// + /// Executes a delete query using the specified predicate. + /// + public static bool Delete(this IDbConnection connection, object predicate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + return Instance.Delete(connection, predicate, transaction, commandTimeout); + } + + /// + /// Executes a select query using the specified predicate, returning an IEnumerable data typed as per T. + /// + public static IEnumerable GetList(this IDbConnection connection, object predicate = null, IList sort = null, IDbTransaction transaction = null, int? commandTimeout = null, bool buffered = false) where T : class + { + return Instance.GetList(connection, predicate, sort, transaction, commandTimeout, buffered); + } + + /// + /// Executes a select query using the specified predicate, returning an IEnumerable data typed as per T. + /// Data returned is dependent upon the specified page and resultsPerPage. + /// + public static IEnumerable GetPage(this IDbConnection connection, object predicate, IList sort, int page, int resultsPerPage, IDbTransaction transaction = null, int? commandTimeout = null, bool buffered = false) where T : class + { + return Instance.GetPage(connection, predicate, sort, page, resultsPerPage, transaction, commandTimeout, buffered); + } + + /// + /// Executes a select query using the specified predicate, returning an IEnumerable data typed as per T. + /// Data returned is dependent upon the specified firstResult and maxResults. + /// + public static IEnumerable GetSet(this IDbConnection connection, object predicate, IList sort, int firstResult, int maxResults, IDbTransaction transaction = null, int? commandTimeout = null, bool buffered = false) where T : class + { + return Instance.GetSet(connection, predicate, sort, firstResult, maxResults, transaction, commandTimeout, buffered); + } + + /// + /// Executes a query using the specified predicate, returning an integer that represents the number of rows that match the query. + /// + public static int Count(this IDbConnection connection, object predicate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class + { + return Instance.Count(connection, predicate, transaction, commandTimeout); + } + + /// + /// Executes a select query for multiple objects, returning IMultipleResultReader for each predicate. + /// + public static IMultipleResultReader GetMultiple(this IDbConnection connection, GetMultiplePredicate predicate, IDbTransaction transaction = null, int? commandTimeout = null) + { + return Instance.GetMultiple(connection, predicate, transaction, commandTimeout); + } + + /// + /// Gets the appropriate mapper for the specified type T. + /// If the mapper for the type is not yet created, a new mapper is generated from the mapper type specifed by DefaultMapper. + /// + public static IClassMapper GetMap() where T : class + { + return Instance.SqlGenerator.Configuration.GetMap(); + } + + /// + /// Clears the ClassMappers for each type. + /// + public static void ClearCache() + { + Instance.SqlGenerator.Configuration.ClearCache(); + } + + /// + /// Generates a COMB Guid which solves the fragmented index issue. + /// See: http://davybrion.com/blog/2009/05/using-the-guidcomb-identifier-strategy + /// + public static Guid GetNextGuid() + { + return Instance.SqlGenerator.Configuration.GetNextGuid(); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensionsConfiguration.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensionsConfiguration.cs new file mode 100644 index 0000000..4be4c6b --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperExtensionsConfiguration.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using DapperExtensions.Mapper; +using DapperExtensions.Sql; + +namespace DapperExtensions +{ + public interface IDapperExtensionsConfiguration + { + Type DefaultMapper { get; } + IList MappingAssemblies { get; } + ISqlDialect Dialect { get; } + IClassMapper GetMap(Type entityType); + IClassMapper GetMap() where T : class; + void ClearCache(); + Guid GetNextGuid(); + } + + public class DapperExtensionsConfiguration : IDapperExtensionsConfiguration + { + private readonly ConcurrentDictionary _classMaps = new ConcurrentDictionary(); + + public DapperExtensionsConfiguration() + : this(typeof(AutoClassMapper<>), new List(), new SqlServerDialect()) + { + } + + + public DapperExtensionsConfiguration(Type defaultMapper, IList mappingAssemblies, ISqlDialect sqlDialect) + { + DefaultMapper = defaultMapper; + MappingAssemblies = mappingAssemblies ?? new List(); + Dialect = sqlDialect; + } + + public Type DefaultMapper { get; private set; } + public IList MappingAssemblies { get; private set; } + public ISqlDialect Dialect { get; private set; } + + public IClassMapper GetMap(Type entityType) + { + IClassMapper map; + if (!_classMaps.TryGetValue(entityType, out map)) + { + Type mapType = GetMapType(entityType); + if (mapType == null) + { + mapType = DefaultMapper.MakeGenericType(entityType); + } + + map = Activator.CreateInstance(mapType) as IClassMapper; + _classMaps[entityType] = map; + } + + return map; + } + + public IClassMapper GetMap() where T : class + { + return GetMap(typeof (T)); + } + + public void ClearCache() + { + _classMaps.Clear(); + } + + public Guid GetNextGuid() + { + byte[] b = Guid.NewGuid().ToByteArray(); + DateTime dateTime = new DateTime(1900, 1, 1); + DateTime now = DateTime.Now; + TimeSpan timeSpan = new TimeSpan(now.Ticks - dateTime.Ticks); + TimeSpan timeOfDay = now.TimeOfDay; + byte[] bytes1 = BitConverter.GetBytes(timeSpan.Days); + byte[] bytes2 = BitConverter.GetBytes((long)(timeOfDay.TotalMilliseconds / 3.333333)); + Array.Reverse(bytes1); + Array.Reverse(bytes2); + Array.Copy(bytes1, bytes1.Length - 2, b, b.Length - 6, 2); + Array.Copy(bytes2, bytes2.Length - 4, b, b.Length - 4, 4); + return new Guid(b); + } + + protected virtual Type GetMapType(Type entityType) + { + Func getType = a => + { + Type[] types = a.GetTypes(); + return (from type in types + let interfaceType = type.GetInterface(typeof(IClassMapper<>).FullName) + where + interfaceType != null && + interfaceType.GetGenericArguments()[0] == entityType + select type).SingleOrDefault(); + }; + + Type result = getType(entityType.Assembly); + if (result != null) + { + return result; + } + + foreach (var mappingAssembly in MappingAssemblies) + { + result = getType(mappingAssembly); + if (result != null) + { + return result; + } + } + + return getType(entityType.Assembly); + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperImplementor.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperImplementor.cs new file mode 100644 index 0000000..c20d0f8 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/DapperImplementor.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Dynamic; +using System.Linq; +using System.Reflection; +using System.Text; +using Dapper; +using DapperExtensions.Mapper; +using DapperExtensions.Sql; + +namespace DapperExtensions +{ + public interface IDapperImplementor + { + ISqlGenerator SqlGenerator { get; } + T Get(IDbConnection connection, dynamic id, IDbTransaction transaction, int? commandTimeout) where T : class; + void Insert(IDbConnection connection, IEnumerable entities, IDbTransaction transaction, int? commandTimeout) where T : class; + dynamic Insert(IDbConnection connection, T entity, IDbTransaction transaction, int? commandTimeout) where T : class; + bool Update(IDbConnection connection, T entity, IDbTransaction transaction, int? commandTimeout, bool ignoreAllKeyProperties) where T : class; + bool Delete(IDbConnection connection, T entity, IDbTransaction transaction, int? commandTimeout) where T : class; + bool Delete(IDbConnection connection, object predicate, IDbTransaction transaction, int? commandTimeout) where T : class; + IEnumerable GetList(IDbConnection connection, object predicate, IList sort, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class; + IEnumerable GetPage(IDbConnection connection, object predicate, IList sort, int page, int resultsPerPage, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class; + IEnumerable GetSet(IDbConnection connection, object predicate, IList sort, int firstResult, int maxResults, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class; + int Count(IDbConnection connection, object predicate, IDbTransaction transaction, int? commandTimeout) where T : class; + IMultipleResultReader GetMultiple(IDbConnection connection, GetMultiplePredicate predicate, IDbTransaction transaction, int? commandTimeout); + } + + public class DapperImplementor : IDapperImplementor + { + public DapperImplementor(ISqlGenerator sqlGenerator) + { + SqlGenerator = sqlGenerator; + } + + public ISqlGenerator SqlGenerator { get; private set; } + + public T Get(IDbConnection connection, dynamic id, IDbTransaction transaction, int? commandTimeout) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate predicate = GetIdPredicate(classMap, id); + T result = GetList(connection, classMap, predicate, null, transaction, commandTimeout, true).SingleOrDefault(); + return result; + } + + public void Insert(IDbConnection connection, IEnumerable entities, IDbTransaction transaction, int? commandTimeout) where T : class + { + IEnumerable properties = null; + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + var notKeyProperties = classMap.Properties.Where(p => p.KeyType != KeyType.NotAKey); + var triggerIdentityColumn = classMap.Properties.SingleOrDefault(p => p.KeyType == KeyType.TriggerIdentity); + + var parameters = new List(); + if (triggerIdentityColumn != null) + { + properties = typeof (T).GetProperties(BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.Name != triggerIdentityColumn.PropertyInfo.Name); + } + + foreach (var e in entities) + { + foreach (var column in notKeyProperties) + { + if (column.KeyType == KeyType.Guid && (Guid)column.PropertyInfo.GetValue(e, null) == Guid.Empty) + { + Guid comb = SqlGenerator.Configuration.GetNextGuid(); + column.PropertyInfo.SetValue(e, comb, null); + } + } + + if (triggerIdentityColumn != null) + { + var dynamicParameters = new DynamicParameters(); + foreach (var prop in properties) + { + dynamicParameters.Add(prop.Name, prop.GetValue(e, null)); + } + + // defaultValue need for identify type of parameter + var defaultValue = typeof(T).GetProperty(triggerIdentityColumn.PropertyInfo.Name).GetValue(e, null); + dynamicParameters.Add("IdOutParam", direction: ParameterDirection.Output, value: defaultValue); + + parameters.Add(dynamicParameters); + } + } + + string sql = SqlGenerator.Insert(classMap); + + if (triggerIdentityColumn == null) + { + connection.Execute(sql, entities, transaction, commandTimeout, CommandType.Text); + } + else + { + connection.Execute(sql, parameters, transaction, commandTimeout, CommandType.Text); + } + } + + public dynamic Insert(IDbConnection connection, T entity, IDbTransaction transaction, int? commandTimeout) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + List nonIdentityKeyProperties = classMap.Properties.Where(p => p.KeyType == KeyType.Guid || p.KeyType == KeyType.Assigned).ToList(); + var identityColumn = classMap.Properties.SingleOrDefault(p => p.KeyType == KeyType.Identity); + var triggerIdentityColumn = classMap.Properties.SingleOrDefault(p => p.KeyType == KeyType.TriggerIdentity); + foreach (var column in nonIdentityKeyProperties) + { + if (column.KeyType == KeyType.Guid && (Guid)column.PropertyInfo.GetValue(entity, null) == Guid.Empty) + { + Guid comb = SqlGenerator.Configuration.GetNextGuid(); + column.PropertyInfo.SetValue(entity, comb, null); + } + } + + IDictionary keyValues = new ExpandoObject(); + string sql = SqlGenerator.Insert(classMap); + if (identityColumn != null) + { + IEnumerable result; + if (SqlGenerator.SupportsMultipleStatements()) + { + sql += SqlGenerator.Configuration.Dialect.BatchSeperator + SqlGenerator.IdentitySql(classMap); + result = connection.Query(sql, entity, transaction, false, commandTimeout, CommandType.Text); + } + else + { + connection.Execute(sql, entity, transaction, commandTimeout, CommandType.Text); + sql = SqlGenerator.IdentitySql(classMap); + result = connection.Query(sql, entity, transaction, false, commandTimeout, CommandType.Text); + } + + // We are only interested in the first identity, but we are iterating over all resulting items (if any). + // This makes sure that ADO.NET drivers (like MySql) won't actively terminate the query. + bool hasResult = false; + int identityInt = 0; + foreach (var identityValue in result) + { + if (hasResult) + { + continue; + } + identityInt = Convert.ToInt32(identityValue); + hasResult = true; + } + if (!hasResult) + { + throw new InvalidOperationException("The source sequence is empty."); + } + + keyValues.Add(identityColumn.Name, identityInt); + identityColumn.PropertyInfo.SetValue(entity, identityInt, null); + } + else if (triggerIdentityColumn != null) + { + var dynamicParameters = new DynamicParameters(); + foreach (var prop in entity.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.Name != triggerIdentityColumn.PropertyInfo.Name)) + { + dynamicParameters.Add(prop.Name, prop.GetValue(entity, null)); + } + + // defaultValue need for identify type of parameter + var defaultValue = entity.GetType().GetProperty(triggerIdentityColumn.PropertyInfo.Name).GetValue(entity, null); + dynamicParameters.Add("IdOutParam", direction: ParameterDirection.Output, value: defaultValue); + + connection.Execute(sql, dynamicParameters, transaction, commandTimeout, CommandType.Text); + + var value = dynamicParameters.Get(SqlGenerator.Configuration.Dialect.ParameterPrefix + "IdOutParam"); + keyValues.Add(triggerIdentityColumn.Name, value); + triggerIdentityColumn.PropertyInfo.SetValue(entity, value, null); + } + else + { + connection.Execute(sql, entity, transaction, commandTimeout, CommandType.Text); + } + + foreach (var column in nonIdentityKeyProperties) + { + keyValues.Add(column.Name, column.PropertyInfo.GetValue(entity, null)); + } + + if (keyValues.Count == 1) + { + return keyValues.First().Value; + } + + return keyValues; + } + + public bool Update(IDbConnection connection, T entity, IDbTransaction transaction, int? commandTimeout, bool ignoreAllKeyProperties = false) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate predicate = GetKeyPredicate(classMap, entity); + Dictionary parameters = new Dictionary(); + string sql = SqlGenerator.Update(classMap, predicate, parameters, ignoreAllKeyProperties); + DynamicParameters dynamicParameters = new DynamicParameters(); + + var columns = ignoreAllKeyProperties + ? classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly) && p.KeyType == KeyType.NotAKey) + : classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly || p.KeyType == KeyType.Identity || p.KeyType == KeyType.Assigned)); + + foreach (var property in ReflectionHelper.GetObjectValues(entity).Where(property => columns.Any(c => c.Name == property.Key))) + { + dynamicParameters.Add(property.Key, property.Value); + } + + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + return connection.Execute(sql, dynamicParameters, transaction, commandTimeout, CommandType.Text) > 0; + } + + public bool Delete(IDbConnection connection, T entity, IDbTransaction transaction, int? commandTimeout) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate predicate = GetKeyPredicate(classMap, entity); + return Delete(connection, classMap, predicate, transaction, commandTimeout); + } + + public bool Delete(IDbConnection connection, object predicate, IDbTransaction transaction, int? commandTimeout) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate wherePredicate = GetPredicate(classMap, predicate); + return Delete(connection, classMap, wherePredicate, transaction, commandTimeout); + } + + public IEnumerable GetList(IDbConnection connection, object predicate, IList sort, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate wherePredicate = GetPredicate(classMap, predicate); + return GetList(connection, classMap, wherePredicate, sort, transaction, commandTimeout, buffered); + } + + public IEnumerable GetPage(IDbConnection connection, object predicate, IList sort, int page, int resultsPerPage, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate wherePredicate = GetPredicate(classMap, predicate); + return GetPage(connection, classMap, wherePredicate, sort, page, resultsPerPage, transaction, commandTimeout, buffered); + } + + public IEnumerable GetSet(IDbConnection connection, object predicate, IList sort, int firstResult, int maxResults, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate wherePredicate = GetPredicate(classMap, predicate); + return GetSet(connection, classMap, wherePredicate, sort, firstResult, maxResults, transaction, commandTimeout, buffered); + } + + public int Count(IDbConnection connection, object predicate, IDbTransaction transaction, int? commandTimeout) where T : class + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(); + IPredicate wherePredicate = GetPredicate(classMap, predicate); + Dictionary parameters = new Dictionary(); + string sql = SqlGenerator.Count(classMap, wherePredicate, parameters); + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + return (int)connection.Query(sql, dynamicParameters, transaction, false, commandTimeout, CommandType.Text).Single().Total; + } + + public IMultipleResultReader GetMultiple(IDbConnection connection, GetMultiplePredicate predicate, IDbTransaction transaction, int? commandTimeout) + { + if (SqlGenerator.SupportsMultipleStatements()) + { + return GetMultipleByBatch(connection, predicate, transaction, commandTimeout); + } + + return GetMultipleBySequence(connection, predicate, transaction, commandTimeout); + } + + protected IEnumerable GetList(IDbConnection connection, IClassMapper classMap, IPredicate predicate, IList sort, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + Dictionary parameters = new Dictionary(); + string sql = SqlGenerator.Select(classMap, predicate, sort, parameters); + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + return connection.Query(sql, dynamicParameters, transaction, buffered, commandTimeout, CommandType.Text); + } + + protected IEnumerable GetPage(IDbConnection connection, IClassMapper classMap, IPredicate predicate, IList sort, int page, int resultsPerPage, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + Dictionary parameters = new Dictionary(); + string sql = SqlGenerator.SelectPaged(classMap, predicate, sort, page, resultsPerPage, parameters); + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + return connection.Query(sql, dynamicParameters, transaction, buffered, commandTimeout, CommandType.Text); + } + + protected IEnumerable GetSet(IDbConnection connection, IClassMapper classMap, IPredicate predicate, IList sort, int firstResult, int maxResults, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + Dictionary parameters = new Dictionary(); + string sql = SqlGenerator.SelectSet(classMap, predicate, sort, firstResult, maxResults, parameters); + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + return connection.Query(sql, dynamicParameters, transaction, buffered, commandTimeout, CommandType.Text); + } + + protected bool Delete(IDbConnection connection, IClassMapper classMap, IPredicate predicate, IDbTransaction transaction, int? commandTimeout) where T : class + { + Dictionary parameters = new Dictionary(); + string sql = SqlGenerator.Delete(classMap, predicate, parameters); + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + return connection.Execute(sql, dynamicParameters, transaction, commandTimeout, CommandType.Text) > 0; + } + + protected IPredicate GetPredicate(IClassMapper classMap, object predicate) + { + IPredicate wherePredicate = predicate as IPredicate; + if (wherePredicate == null && predicate != null) + { + wherePredicate = GetEntityPredicate(classMap, predicate); + } + + return wherePredicate; + } + + protected IPredicate GetIdPredicate(IClassMapper classMap, object id) + { + bool isSimpleType = ReflectionHelper.IsSimpleType(id.GetType()); + var keys = classMap.Properties.Where(p => p.KeyType != KeyType.NotAKey); + IDictionary paramValues = null; + IList predicates = new List(); + if (!isSimpleType) + { + paramValues = ReflectionHelper.GetObjectValues(id); + } + + foreach (var key in keys) + { + object value = id; + if (!isSimpleType) + { + value = paramValues[key.Name]; + } + + Type predicateType = typeof(FieldPredicate<>).MakeGenericType(classMap.EntityType); + + IFieldPredicate fieldPredicate = Activator.CreateInstance(predicateType) as IFieldPredicate; + fieldPredicate.Not = false; + fieldPredicate.Operator = Operator.Eq; + fieldPredicate.PropertyName = key.Name; + fieldPredicate.Value = value; + predicates.Add(fieldPredicate); + } + + return predicates.Count == 1 + ? predicates[0] + : new PredicateGroup + { + Operator = GroupOperator.And, + Predicates = predicates + }; + } + + protected IPredicate GetKeyPredicate(IClassMapper classMap, T entity) where T : class + { + var whereFields = classMap.Properties.Where(p => p.KeyType != KeyType.NotAKey); + if (!whereFields.Any()) + { + throw new ArgumentException("At least one Key column must be defined."); + } + + IList predicates = (from field in whereFields + select new FieldPredicate + { + Not = false, + Operator = Operator.Eq, + PropertyName = field.Name, + Value = field.PropertyInfo.GetValue(entity, null) + }).Cast().ToList(); + + return predicates.Count == 1 + ? predicates[0] + : new PredicateGroup + { + Operator = GroupOperator.And, + Predicates = predicates + }; + } + + protected IPredicate GetEntityPredicate(IClassMapper classMap, object entity) + { + Type predicateType = typeof(FieldPredicate<>).MakeGenericType(classMap.EntityType); + IList predicates = new List(); + foreach (var kvp in ReflectionHelper.GetObjectValues(entity)) + { + IFieldPredicate fieldPredicate = Activator.CreateInstance(predicateType) as IFieldPredicate; + fieldPredicate.Not = false; + fieldPredicate.Operator = Operator.Eq; + fieldPredicate.PropertyName = kvp.Key; + fieldPredicate.Value = kvp.Value; + predicates.Add(fieldPredicate); + } + + return predicates.Count == 1 + ? predicates[0] + : new PredicateGroup + { + Operator = GroupOperator.And, + Predicates = predicates + }; + } + + protected GridReaderResultReader GetMultipleByBatch(IDbConnection connection, GetMultiplePredicate predicate, IDbTransaction transaction, int? commandTimeout) + { + Dictionary parameters = new Dictionary(); + StringBuilder sql = new StringBuilder(); + foreach (var item in predicate.Items) + { + IClassMapper classMap = SqlGenerator.Configuration.GetMap(item.Type); + IPredicate itemPredicate = item.Value as IPredicate; + if (itemPredicate == null && item.Value != null) + { + itemPredicate = GetPredicate(classMap, item.Value); + } + + sql.AppendLine(SqlGenerator.Select(classMap, itemPredicate, item.Sort, parameters) + SqlGenerator.Configuration.Dialect.BatchSeperator); + } + + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + SqlMapper.GridReader grid = connection.QueryMultiple(sql.ToString(), dynamicParameters, transaction, commandTimeout, CommandType.Text); + return new GridReaderResultReader(grid); + } + + protected SequenceReaderResultReader GetMultipleBySequence(IDbConnection connection, GetMultiplePredicate predicate, IDbTransaction transaction, int? commandTimeout) + { + IList items = new List(); + foreach (var item in predicate.Items) + { + Dictionary parameters = new Dictionary(); + IClassMapper classMap = SqlGenerator.Configuration.GetMap(item.Type); + IPredicate itemPredicate = item.Value as IPredicate; + if (itemPredicate == null && item.Value != null) + { + itemPredicate = GetPredicate(classMap, item.Value); + } + + string sql = SqlGenerator.Select(classMap, itemPredicate, item.Sort, parameters); + DynamicParameters dynamicParameters = new DynamicParameters(); + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, parameter.Value); + } + + SqlMapper.GridReader queryResult = connection.QueryMultiple(sql, dynamicParameters, transaction, commandTimeout, CommandType.Text); + items.Add(queryResult); + } + + return new SequenceReaderResultReader(items); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Database.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Database.cs new file mode 100644 index 0000000..1b2dfd3 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Database.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using DapperExtensions.Mapper; +using DapperExtensions.Sql; + +namespace DapperExtensions +{ + public interface IDatabase : IDisposable + { + bool HasActiveTransaction { get; } + IDbConnection Connection { get; } + void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted); + void Commit(); + void Rollback(); + void RunInTransaction(Action action); + T RunInTransaction(Func func); + T Get(dynamic id, IDbTransaction transaction, int? commandTimeout = null) where T : class; + T Get(dynamic id, int? commandTimeout = null) where T : class; + void Insert(IEnumerable entities, IDbTransaction transaction, int? commandTimeout = null) where T : class; + void Insert(IEnumerable entities, int? commandTimeout = null) where T : class; + dynamic Insert(T entity, IDbTransaction transaction, int? commandTimeout = null) where T : class; + dynamic Insert(T entity, int? commandTimeout = null) where T : class; + bool Update(T entity, IDbTransaction transaction, int? commandTimeout = null, bool ignoreAllKeyProperties = false) where T : class; + bool Update(T entity, int? commandTimeout = null, bool ignoreAllKeyProperties = false) where T : class; + bool Delete(T entity, IDbTransaction transaction, int? commandTimeout = null) where T : class; + bool Delete(T entity, int? commandTimeout = null) where T : class; + bool Delete(object predicate, IDbTransaction transaction, int? commandTimeout = null) where T : class; + bool Delete(object predicate, int? commandTimeout = null) where T : class; + IEnumerable GetList(object predicate, IList sort, IDbTransaction transaction, int? commandTimeout = null, bool buffered = true) where T : class; + IEnumerable GetList(object predicate = null, IList sort = null, int? commandTimeout = null, bool buffered = true) where T : class; + IEnumerable GetPage(object predicate, IList sort, int page, int resultsPerPage, IDbTransaction transaction, int? commandTimeout = null, bool buffered = true) where T : class; + IEnumerable GetPage(object predicate, IList sort, int page, int resultsPerPage, int? commandTimeout = null, bool buffered = true) where T : class; + IEnumerable GetSet(object predicate, IList sort, int firstResult, int maxResults, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class; + IEnumerable GetSet(object predicate, IList sort, int firstResult, int maxResults, int? commandTimeout, bool buffered) where T : class; + int Count(object predicate, IDbTransaction transaction, int? commandTimeout = null) where T : class; + int Count(object predicate, int? commandTimeout = null) where T : class; + IMultipleResultReader GetMultiple(GetMultiplePredicate predicate, IDbTransaction transaction, int? commandTimeout = null); + IMultipleResultReader GetMultiple(GetMultiplePredicate predicate, int? commandTimeout = null); + void ClearCache(); + Guid GetNextGuid(); + IClassMapper GetMap() where T : class; + } + + public class Database : IDatabase + { + private readonly IDapperImplementor _dapper; + + private IDbTransaction _transaction; + + public Database(IDbConnection connection, ISqlGenerator sqlGenerator) + { + _dapper = new DapperImplementor(sqlGenerator); + Connection = connection; + + if (Connection.State != ConnectionState.Open) + { + Connection.Open(); + } + } + + public bool HasActiveTransaction + { + get + { + return _transaction != null; + } + } + + public IDbConnection Connection { get; private set; } + + public void Dispose() + { + if (Connection.State != ConnectionState.Closed) + { + if (_transaction != null) + { + _transaction.Rollback(); + } + + Connection.Close(); + } + } + + public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) + { + _transaction = Connection.BeginTransaction(isolationLevel); + } + + public void Commit() + { + _transaction.Commit(); + _transaction = null; + } + + public void Rollback() + { + _transaction.Rollback(); + _transaction = null; + } + + public void RunInTransaction(Action action) + { + BeginTransaction(); + try + { + action(); + Commit(); + } + catch (Exception ex) + { + if (HasActiveTransaction) + { + Rollback(); + } + + throw ex; + } + } + + public T RunInTransaction(Func func) + { + BeginTransaction(); + try + { + T result = func(); + Commit(); + return result; + } + catch (Exception ex) + { + if (HasActiveTransaction) + { + Rollback(); + } + + throw ex; + } + } + + public T Get(dynamic id, IDbTransaction transaction, int? commandTimeout) where T : class + { + return (T)_dapper.Get(Connection, id, transaction, commandTimeout); + } + + public T Get(dynamic id, int? commandTimeout) where T : class + { + return (T)_dapper.Get(Connection, id, _transaction, commandTimeout); + } + + public void Insert(IEnumerable entities, IDbTransaction transaction, int? commandTimeout) where T : class + { + _dapper.Insert(Connection, entities, transaction, commandTimeout); + } + + public void Insert(IEnumerable entities, int? commandTimeout) where T : class + { + _dapper.Insert(Connection, entities, _transaction, commandTimeout); + } + + public dynamic Insert(T entity, IDbTransaction transaction, int? commandTimeout) where T : class + { + return _dapper.Insert(Connection, entity, transaction, commandTimeout); + } + + public dynamic Insert(T entity, int? commandTimeout) where T : class + { + return _dapper.Insert(Connection, entity, _transaction, commandTimeout); + } + + public bool Update(T entity, IDbTransaction transaction, int? commandTimeout, bool ignoreAllKeyProperties) where T : class + { + return _dapper.Update(Connection, entity, transaction, commandTimeout, ignoreAllKeyProperties); + } + + public bool Update(T entity, int? commandTimeout, bool ignoreAllKeyProperties) where T : class + { + return _dapper.Update(Connection, entity, _transaction, commandTimeout, ignoreAllKeyProperties); + } + + public bool Delete(T entity, IDbTransaction transaction, int? commandTimeout) where T : class + { + return _dapper.Delete(Connection, entity, transaction, commandTimeout); + } + + public bool Delete(T entity, int? commandTimeout) where T : class + { + return _dapper.Delete(Connection, entity, _transaction, commandTimeout); + } + + public bool Delete(object predicate, IDbTransaction transaction, int? commandTimeout) where T : class + { + return _dapper.Delete(Connection, predicate, transaction, commandTimeout); + } + + public bool Delete(object predicate, int? commandTimeout) where T : class + { + return _dapper.Delete(Connection, predicate, _transaction, commandTimeout); + } + + public IEnumerable GetList(object predicate, IList sort, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + return _dapper.GetList(Connection, predicate, sort, transaction, commandTimeout, buffered); + } + + public IEnumerable GetList(object predicate, IList sort, int? commandTimeout, bool buffered) where T : class + { + return _dapper.GetList(Connection, predicate, sort, _transaction, commandTimeout, buffered); + } + + public IEnumerable GetPage(object predicate, IList sort, int page, int resultsPerPage, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + return _dapper.GetPage(Connection, predicate, sort, page, resultsPerPage, transaction, commandTimeout, buffered); + } + + public IEnumerable GetPage(object predicate, IList sort, int page, int resultsPerPage, int? commandTimeout, bool buffered) where T : class + { + return _dapper.GetPage(Connection, predicate, sort, page, resultsPerPage, _transaction, commandTimeout, buffered); + } + + public IEnumerable GetSet(object predicate, IList sort, int firstResult, int maxResults, IDbTransaction transaction, int? commandTimeout, bool buffered) where T : class + { + return _dapper.GetSet(Connection, predicate, sort, firstResult, maxResults, transaction, commandTimeout, buffered); + } + + public IEnumerable GetSet(object predicate, IList sort, int firstResult, int maxResults, int? commandTimeout, bool buffered) where T : class + { + return _dapper.GetSet(Connection, predicate, sort, firstResult, maxResults, _transaction, commandTimeout, buffered); + } + + public int Count(object predicate, IDbTransaction transaction, int? commandTimeout) where T : class + { + return _dapper.Count(Connection, predicate, transaction, commandTimeout); + } + + public int Count(object predicate, int? commandTimeout) where T : class + { + return _dapper.Count(Connection, predicate, _transaction, commandTimeout); + } + + public IMultipleResultReader GetMultiple(GetMultiplePredicate predicate, IDbTransaction transaction, int? commandTimeout) + { + return _dapper.GetMultiple(Connection, predicate, transaction, commandTimeout); + } + + public IMultipleResultReader GetMultiple(GetMultiplePredicate predicate, int? commandTimeout) + { + return _dapper.GetMultiple(Connection, predicate, _transaction, commandTimeout); + } + + public void ClearCache() + { + _dapper.SqlGenerator.Configuration.ClearCache(); + } + + public Guid GetNextGuid() + { + return _dapper.SqlGenerator.Configuration.GetNextGuid(); + } + + public IClassMapper GetMap() where T : class + { + return _dapper.SqlGenerator.Configuration.GetMap(); + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultiplePredicate.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultiplePredicate.cs new file mode 100644 index 0000000..e71defa --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultiplePredicate.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DapperExtensions +{ + public class GetMultiplePredicate + { + private readonly List _items; + + public GetMultiplePredicate() + { + _items = new List(); + } + + public IEnumerable Items + { + get { return _items.AsReadOnly(); } + } + + public void Add(IPredicate predicate, IList sort = null) where T : class + { + _items.Add(new GetMultiplePredicateItem + { + Value = predicate, + Type = typeof(T), + Sort = sort + }); + } + + public void Add(object id) where T : class + { + _items.Add(new GetMultiplePredicateItem + { + Value = id, + Type = typeof (T) + }); + } + + public class GetMultiplePredicateItem + { + public object Value { get; set; } + public Type Type { get; set; } + public IList Sort { get; set; } + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultipleResult.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultipleResult.cs new file mode 100644 index 0000000..a17369c --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/GetMultipleResult.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Dapper; + +namespace DapperExtensions +{ + public interface IMultipleResultReader + { + IEnumerable Read(); + } + + public class GridReaderResultReader : IMultipleResultReader + { + private readonly SqlMapper.GridReader _reader; + + public GridReaderResultReader(SqlMapper.GridReader reader) + { + _reader = reader; + } + + public IEnumerable Read() + { + return _reader.Read(); + } + } + + public class SequenceReaderResultReader : IMultipleResultReader + { + private readonly Queue _items; + + public SequenceReaderResultReader(IEnumerable items) + { + _items = new Queue(items); + } + + public IEnumerable Read() + { + SqlMapper.GridReader reader = _items.Dequeue(); + return reader.Read(); + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/AutoClassMapper.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/AutoClassMapper.cs new file mode 100644 index 0000000..54c353f --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/AutoClassMapper.cs @@ -0,0 +1,20 @@ +using System.Text; +using System.Linq; +using System.Collections.Generic; +using System; + +namespace DapperExtensions.Mapper +{ + /// + /// Automatically maps an entity to a table using a combination of reflection and naming conventions for keys. + /// + public class AutoClassMapper : ClassMapper where T : class + { + public AutoClassMapper() + { + Type type = typeof(T); + Table(type.Name); + AutoMap(); + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/ClassMapper.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/ClassMapper.cs new file mode 100644 index 0000000..a11c5e5 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/ClassMapper.cs @@ -0,0 +1,171 @@ +using System.Numerics; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Renci.SshNet.Common; + +namespace DapperExtensions.Mapper +{ + public interface IClassMapper + { + string SchemaName { get; } + string TableName { get; } + IList Properties { get; } + Type EntityType { get; } + } + + public interface IClassMapper : IClassMapper where T : class + { + } + + /// + /// Maps an entity to a table through a collection of property maps. + /// + public class ClassMapper : IClassMapper where T : class + { + /// + /// Gets or sets the schema to use when referring to the corresponding table name in the database. + /// + public string SchemaName { get; protected set; } + + /// + /// Gets or sets the table to use in the database. + /// + public string TableName { get; protected set; } + + /// + /// A collection of properties that will map to columns in the database table. + /// + public IList Properties { get; private set; } + + public Type EntityType + { + get { return typeof(T); } + } + + public ClassMapper() + { + PropertyTypeKeyTypeMapping = new Dictionary + { + { typeof(byte), KeyType.Identity }, { typeof(byte?), KeyType.Identity }, + { typeof(sbyte), KeyType.Identity }, { typeof(sbyte?), KeyType.Identity }, + { typeof(short), KeyType.Identity }, { typeof(short?), KeyType.Identity }, + { typeof(ushort), KeyType.Identity }, { typeof(ushort?), KeyType.Identity }, + { typeof(int), KeyType.Identity }, { typeof(int?), KeyType.Identity }, + { typeof(uint), KeyType.Identity}, { typeof(uint?), KeyType.Identity }, + { typeof(long), KeyType.Identity }, { typeof(long?), KeyType.Identity }, + { typeof(ulong), KeyType.Identity }, { typeof(ulong?), KeyType.Identity }, + { typeof(BigInteger), KeyType.Identity }, { typeof(BigInteger?), KeyType.Identity }, + { typeof(Guid), KeyType.Guid }, { typeof(Guid?), KeyType.Guid }, + }; + + Properties = new List(); + Table(typeof(T).Name); + } + + protected Dictionary PropertyTypeKeyTypeMapping { get; private set; } + + public virtual void Schema(string schemaName) + { + SchemaName = schemaName; + } + + public virtual void Table(string tableName) + { + TableName = tableName; + } + + protected virtual void AutoMap() + { + AutoMap(null); + } + + protected virtual void AutoMap(Func canMap) + { + Type type = typeof(T); + bool hasDefinedKey = Properties.Any(p => p.KeyType != KeyType.NotAKey); + PropertyMap keyMap = null; + foreach (var propertyInfo in type.GetProperties()) + { + if (Properties.Any(p => p.Name.Equals(propertyInfo.Name, StringComparison.InvariantCultureIgnoreCase))) + { + continue; + } + + if ((canMap != null && !canMap(type, propertyInfo))) + { + continue; + } + + PropertyMap map = Map(propertyInfo); + if (!hasDefinedKey) + { + if (string.Equals(map.PropertyInfo.Name, "id", StringComparison.InvariantCultureIgnoreCase)) + { + keyMap = map; + } + + if (keyMap == null && map.PropertyInfo.Name.EndsWith("id", true, CultureInfo.InvariantCulture)) + { + keyMap = map; + } + } + } + + if (keyMap != null) + { + keyMap.Key(PropertyTypeKeyTypeMapping.ContainsKey(keyMap.PropertyInfo.PropertyType) + ? PropertyTypeKeyTypeMapping[keyMap.PropertyInfo.PropertyType] + : KeyType.Assigned); + } + } + + /// + /// Fluently, maps an entity property to a column + /// + protected PropertyMap Map(Expression> expression) + { + PropertyInfo propertyInfo = ReflectionHelper.GetProperty(expression) as PropertyInfo; + return Map(propertyInfo); + } + + /// + /// Fluently, maps an entity property to a column + /// + protected PropertyMap Map(PropertyInfo propertyInfo) + { + PropertyMap result = new PropertyMap(propertyInfo); + this.GuardForDuplicatePropertyMap(result); + Properties.Add(result); + return result; + } + + /// + /// Removes a propertymap entry + /// + /// + protected void UnMap(Expression> expression) + { + var propertyInfo = ReflectionHelper.GetProperty(expression) as PropertyInfo; + var mapping = this.Properties.Where(w => w.Name == propertyInfo.Name).SingleOrDefault(); + + if (mapping == null) + { + throw new ApplicationException("Unable to UnMap because mapping does not exist."); + } + + this.Properties.Remove(mapping); + } + + private void GuardForDuplicatePropertyMap(PropertyMap result) + { + if (Properties.Any(p => p.Name.Equals(result.Name))) + { + throw new ArgumentException(string.Format("Duplicate mapping for property {0} detected.",result.Name)); + } + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PluralizedAutoClassMapper.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PluralizedAutoClassMapper.cs new file mode 100644 index 0000000..de511df --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PluralizedAutoClassMapper.cs @@ -0,0 +1,67 @@ +using System.Text; +using System.Linq; +using System.Collections.Generic; +using System; +using System.Text.RegularExpressions; + +namespace DapperExtensions.Mapper +{ + /// + /// Automatically maps an entity to a table using a combination of reflection and naming conventions for keys. + /// Identical to AutoClassMapper, but attempts to pluralize table names automatically. + /// Example: Person entity maps to People table + /// + public class PluralizedAutoClassMapper : AutoClassMapper where T : class + { + public override void Table(string tableName) + { + base.Table(Formatting.Pluralize(tableName)); + } + + // Adapted from: http://mattgrande.wordpress.com/2009/10/28/pluralization-helper-for-c/ + public static class Formatting + { + private static readonly IList Unpluralizables = new List { "equipment", "information", "rice", "money", "species", "series", "fish", "sheep", "deer" }; + private static readonly IDictionary Pluralizations = new Dictionary + { + // Start with the rarest cases, and move to the most common + { "person", "people" }, + { "ox", "oxen" }, + { "child", "children" }, + { "foot", "feet" }, + { "tooth", "teeth" }, + { "goose", "geese" }, + // And now the more standard rules. + { "(.*)fe?$", "$1ves" }, // ie, wolf, wife + { "(.*)man$", "$1men" }, + { "(.+[aeiou]y)$", "$1s" }, + { "(.+[^aeiou])y$", "$1ies" }, + { "(.+z)$", "$1zes" }, + { "([m|l])ouse$", "$1ice" }, + { "(.+)(e|i)x$", @"$1ices"}, // ie, Matrix, Index + { "(octop|vir)us$", "$1i"}, + { "(.+(s|x|sh|ch))$", @"$1es"}, + { "(.+)", @"$1s" } + }; + + public static string Pluralize(string singular) + { + if (Unpluralizables.Contains(singular)) + return singular; + + var plural = string.Empty; + + foreach (var pluralization in Pluralizations) + { + if (Regex.IsMatch(singular, pluralization.Key)) + { + plural = Regex.Replace(singular, pluralization.Key, pluralization.Value); + break; + } + } + + return plural; + } + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PropertyMap.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PropertyMap.cs new file mode 100644 index 0000000..35d0e49 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Mapper/PropertyMap.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace DapperExtensions.Mapper +{ + /// + /// Maps an entity property to its corresponding column in the database. + /// + public interface IPropertyMap + { + string Name { get; } + string ColumnName { get; } + bool Ignored { get; } + bool IsReadOnly { get; } + KeyType KeyType { get; } + PropertyInfo PropertyInfo { get; } + } + + /// + /// Maps an entity property to its corresponding column in the database. + /// + public class PropertyMap : IPropertyMap + { + public PropertyMap(PropertyInfo propertyInfo) + { + PropertyInfo = propertyInfo; + ColumnName = PropertyInfo.Name; + } + + /// + /// Gets the name of the property by using the specified propertyInfo. + /// + public string Name + { + get { return PropertyInfo.Name; } + } + + /// + /// Gets the column name for the current property. + /// + public string ColumnName { get; private set; } + + /// + /// Gets the key type for the current property. + /// + public KeyType KeyType { get; private set; } + + /// + /// Gets the ignore status of the current property. If ignored, the current property will not be included in queries. + /// + public bool Ignored { get; private set; } + + /// + /// Gets the read-only status of the current property. If read-only, the current property will not be included in INSERT and UPDATE queries. + /// + public bool IsReadOnly { get; private set; } + + /// + /// Gets the property info for the current property. + /// + public PropertyInfo PropertyInfo { get; private set; } + + /// + /// Fluently sets the column name for the property. + /// + /// The column name as it exists in the database. + public PropertyMap Column(string columnName) + { + ColumnName = columnName; + return this; + } + + /// + /// Fluently sets the key type of the property. + /// + /// The column name as it exists in the database. + public PropertyMap Key(KeyType keyType) + { + if (Ignored) + { + throw new ArgumentException(string.Format("'{0}' is ignored and cannot be made a key field. ", Name)); + } + + if (IsReadOnly) + { + throw new ArgumentException(string.Format("'{0}' is readonly and cannot be made a key field. ", Name)); + } + + KeyType = keyType; + return this; + } + + /// + /// Fluently sets the ignore status of the property. + /// + public PropertyMap Ignore() + { + if (KeyType != KeyType.NotAKey) + { + throw new ArgumentException(string.Format("'{0}' is a key field and cannot be ignored.", Name)); + } + + Ignored = true; + return this; + } + + /// + /// Fluently sets the read-only status of the property. + /// + public PropertyMap ReadOnly() + { + if (KeyType != KeyType.NotAKey) + { + throw new ArgumentException(string.Format("'{0}' is a key field and cannot be marked readonly.", Name)); + } + + IsReadOnly = true; + return this; + } + } + + /// + /// Used by ClassMapper to determine which entity property represents the key. + /// + public enum KeyType + { + /// + /// The property is not a key and is not automatically managed. + /// + NotAKey, + + /// + /// The property is an integery-based identity generated from the database. + /// + Identity, + + /// + /// The property is an identity generated by the database trigger. + /// + TriggerIdentity, + + /// + /// The property is a Guid identity which is automatically managed. + /// + Guid, + + /// + /// The property is a key that is not automatically managed. + /// + Assigned + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Predicates.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Predicates.cs new file mode 100644 index 0000000..5db3d97 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Predicates.cs @@ -0,0 +1,393 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using DapperExtensions.Mapper; +using DapperExtensions.Sql; + +namespace DapperExtensions +{ + public static class Predicates + { + /// + /// Factory method that creates a new IFieldPredicate predicate: [FieldName] [Operator] [Value]. + /// Example: WHERE FirstName = 'Foo' + /// + /// The type of the entity. + /// An expression that returns the left operand [FieldName]. + /// The comparison operator. + /// The value for the predicate. + /// Effectively inverts the comparison operator. Example: WHERE FirstName <> 'Foo'. + /// An instance of IFieldPredicate. + public static IFieldPredicate Field(Expression> expression, Operator op, object value, bool not = false) where T : class + { + PropertyInfo propertyInfo = ReflectionHelper.GetProperty(expression) as PropertyInfo; + return new FieldPredicate + { + PropertyName = propertyInfo.Name, + Operator = op, + Value = value, + Not = not + }; + } + + /// + /// Factory method that creates a new IPropertyPredicate predicate: [FieldName1] [Operator] [FieldName2] + /// Example: WHERE FirstName = LastName + /// + /// The type of the entity for the left operand. + /// The type of the entity for the right operand. + /// An expression that returns the left operand [FieldName1]. + /// The comparison operator. + /// An expression that returns the right operand [FieldName2]. + /// Effectively inverts the comparison operator. Example: WHERE FirstName <> LastName + /// An instance of IPropertyPredicate. + public static IPropertyPredicate Property(Expression> expression, Operator op, Expression> expression2, bool not = false) + where T : class + where T2 : class + { + PropertyInfo propertyInfo = ReflectionHelper.GetProperty(expression) as PropertyInfo; + PropertyInfo propertyInfo2 = ReflectionHelper.GetProperty(expression2) as PropertyInfo; + return new PropertyPredicate + { + PropertyName = propertyInfo.Name, + PropertyName2 = propertyInfo2.Name, + Operator = op, + Not = not + }; + } + + /// + /// Factory method that creates a new IPredicateGroup predicate. + /// Predicate groups can be joined together with other predicate groups. + /// + /// The grouping operator to use when joining the predicates (AND / OR). + /// A list of predicates to group. + /// An instance of IPredicateGroup. + public static IPredicateGroup Group(GroupOperator op, params IPredicate[] predicate) + { + return new PredicateGroup + { + Operator = op, + Predicates = predicate + }; + } + + /// + /// Factory method that creates a new IExistsPredicate predicate. + /// + public static IExistsPredicate Exists(IPredicate predicate, bool not = false) + where TSub : class + { + return new ExistsPredicate + { + Not = not, + Predicate = predicate + }; + } + + /// + /// Factory method that creates a new IBetweenPredicate predicate. + /// + public static IBetweenPredicate Between(Expression> expression, BetweenValues values, bool not = false) + where T : class + { + PropertyInfo propertyInfo = ReflectionHelper.GetProperty(expression) as PropertyInfo; + return new BetweenPredicate + { + Not = not, + PropertyName = propertyInfo.Name, + Value = values + }; + } + + /// + /// Factory method that creates a new Sort which controls how the results will be sorted. + /// + public static ISort Sort(Expression> expression, bool ascending = true) + { + PropertyInfo propertyInfo = ReflectionHelper.GetProperty(expression) as PropertyInfo; + return new Sort + { + PropertyName = propertyInfo.Name, + Ascending = ascending + }; + } + } + + public interface IPredicate + { + string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters); + } + + public interface IBasePredicate : IPredicate + { + string PropertyName { get; set; } + } + + public abstract class BasePredicate : IBasePredicate + { + public abstract string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters); + public string PropertyName { get; set; } + + protected virtual string GetColumnName(Type entityType, ISqlGenerator sqlGenerator, string propertyName) + { + IClassMapper map = sqlGenerator.Configuration.GetMap(entityType); + if (map == null) + { + throw new NullReferenceException(string.Format("Map was not found for {0}", entityType)); + } + + IPropertyMap propertyMap = map.Properties.SingleOrDefault(p => p.Name == propertyName); + if (propertyMap == null) + { + throw new NullReferenceException(string.Format("{0} was not found for {1}", propertyName, entityType)); + } + + return sqlGenerator.GetColumnName(map, propertyMap, false); + } + } + + public interface IComparePredicate : IBasePredicate + { + Operator Operator { get; set; } + bool Not { get; set; } + } + + public abstract class ComparePredicate : BasePredicate + { + public Operator Operator { get; set; } + public bool Not { get; set; } + + public virtual string GetOperatorString() + { + switch (Operator) + { + case Operator.Gt: + return Not ? "<=" : ">"; + case Operator.Ge: + return Not ? "<" : ">="; + case Operator.Lt: + return Not ? ">=" : "<"; + case Operator.Le: + return Not ? ">" : "<="; + case Operator.Like: + return Not ? "NOT LIKE" : "LIKE"; + default: + return Not ? "<>" : "="; + } + } + } + + public interface IFieldPredicate : IComparePredicate + { + object Value { get; set; } + } + + public class FieldPredicate : ComparePredicate, IFieldPredicate + where T : class + { + public object Value { get; set; } + + public override string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters) + { + string columnName = GetColumnName(typeof(T), sqlGenerator, PropertyName); + if (Value == null) + { + return string.Format("({0} IS {1}NULL)", columnName, Not ? "NOT " : string.Empty); + } + + if (Value is IEnumerable && !(Value is string)) + { + if (Operator != Operator.Eq) + { + throw new ArgumentException("Operator must be set to Eq for Enumerable types"); + } + + List @params = new List(); + foreach (var value in (IEnumerable)Value) + { + string valueParameterName = parameters.SetParameterName(this.PropertyName, value, sqlGenerator.Configuration.Dialect.ParameterPrefix); + @params.Add(valueParameterName); + } + + string paramStrings = @params.Aggregate(new StringBuilder(), (sb, s) => sb.Append((sb.Length != 0 ? ", " : string.Empty) + s), sb => sb.ToString()); + return string.Format("({0} {1}IN ({2}))", columnName, Not ? "NOT " : string.Empty, paramStrings); + } + + string parameterName = parameters.SetParameterName(this.PropertyName, this.Value, sqlGenerator.Configuration.Dialect.ParameterPrefix); + return string.Format("({0} {1} {2})", columnName, GetOperatorString(), parameterName); + } + } + + public interface IPropertyPredicate : IComparePredicate + { + string PropertyName2 { get; set; } + } + + public class PropertyPredicate : ComparePredicate, IPropertyPredicate + where T : class + where T2 : class + { + public string PropertyName2 { get; set; } + + public override string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters) + { + string columnName = GetColumnName(typeof(T), sqlGenerator, PropertyName); + string columnName2 = GetColumnName(typeof(T2), sqlGenerator, PropertyName2); + return string.Format("({0} {1} {2})", columnName, GetOperatorString(), columnName2); + } + } + + public struct BetweenValues + { + public object Value1 { get; set; } + public object Value2 { get; set; } + } + + public interface IBetweenPredicate : IPredicate + { + string PropertyName { get; set; } + BetweenValues Value { get; set; } + bool Not { get; set; } + + } + + public class BetweenPredicate : BasePredicate, IBetweenPredicate + where T : class + { + public override string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters) + { + string columnName = GetColumnName(typeof(T), sqlGenerator, PropertyName); + string propertyName1 = parameters.SetParameterName(this.PropertyName, this.Value.Value1, sqlGenerator.Configuration.Dialect.ParameterPrefix); + string propertyName2 = parameters.SetParameterName(this.PropertyName, this.Value.Value2, sqlGenerator.Configuration.Dialect.ParameterPrefix); + return string.Format("({0} {1}BETWEEN {2} AND {3})", columnName, Not ? "NOT " : string.Empty, propertyName1, propertyName2); + } + + public BetweenValues Value { get; set; } + + public bool Not { get; set; } + } + + /// + /// Comparison operator for predicates. + /// + public enum Operator + { + /// + /// Equal to + /// + Eq, + + /// + /// Greater than + /// + Gt, + + /// + /// Greater than or equal to + /// + Ge, + + /// + /// Less than + /// + Lt, + + /// + /// Less than or equal to + /// + Le, + + /// + /// Like (You can use % in the value to do wilcard searching) + /// + Like + } + + public interface IPredicateGroup : IPredicate + { + GroupOperator Operator { get; set; } + IList Predicates { get; set; } + } + + /// + /// Groups IPredicates together using the specified group operator. + /// + public class PredicateGroup : IPredicateGroup + { + public GroupOperator Operator { get; set; } + public IList Predicates { get; set; } + public string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters) + { + string seperator = Operator == GroupOperator.And ? " AND " : " OR "; + return "(" + Predicates.Aggregate(new StringBuilder(), + (sb, p) => (sb.Length == 0 ? sb : sb.Append(seperator)).Append(p.GetSql(sqlGenerator, parameters)), + sb => + { + var s = sb.ToString(); + if (s.Length == 0) return sqlGenerator.Configuration.Dialect.EmptyExpression; + return s; + } + ) + ")"; + } + } + + public interface IExistsPredicate : IPredicate + { + IPredicate Predicate { get; set; } + bool Not { get; set; } + } + + public class ExistsPredicate : IExistsPredicate + where TSub : class + { + public IPredicate Predicate { get; set; } + public bool Not { get; set; } + + public string GetSql(ISqlGenerator sqlGenerator, IDictionary parameters) + { + IClassMapper mapSub = GetClassMapper(typeof(TSub), sqlGenerator.Configuration); + string sql = string.Format("({0}EXISTS (SELECT 1 FROM {1} WHERE {2}))", + Not ? "NOT " : string.Empty, + sqlGenerator.GetTableName(mapSub), + Predicate.GetSql(sqlGenerator, parameters)); + return sql; + } + + protected virtual IClassMapper GetClassMapper(Type type, IDapperExtensionsConfiguration configuration) + { + IClassMapper map = configuration.GetMap(type); + if (map == null) + { + throw new NullReferenceException(string.Format("Map was not found for {0}", type)); + } + + return map; + } + } + + public interface ISort + { + string PropertyName { get; set; } + bool Ascending { get; set; } + } + + public class Sort : ISort + { + public string PropertyName { get; set; } + public bool Ascending { get; set; } + } + + /// + /// Operator to use when joining predicates in a PredicateGroup. + /// + public enum GroupOperator + { + And, + Or + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/ReflectionHelper.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/ReflectionHelper.cs new file mode 100644 index 0000000..1be6d8e --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/ReflectionHelper.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace DapperExtensions +{ + public static class ReflectionHelper + { + private static List _simpleTypes = new List + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + typeof(bool), + typeof(string), + typeof(char), + typeof(Guid), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(byte[]) + }; + + public static MemberInfo GetProperty(LambdaExpression lambda) + { + Expression expr = lambda; + for (; ; ) + { + switch (expr.NodeType) + { + case ExpressionType.Lambda: + expr = ((LambdaExpression)expr).Body; + break; + case ExpressionType.Convert: + expr = ((UnaryExpression)expr).Operand; + break; + case ExpressionType.MemberAccess: + MemberExpression memberExpression = (MemberExpression)expr; + MemberInfo mi = memberExpression.Member; + return mi; + default: + return null; + } + } + } + + public static IDictionary GetObjectValues(object obj) + { + IDictionary result = new Dictionary(); + if (obj == null) + { + return result; + } + + + foreach (var propertyInfo in obj.GetType().GetProperties()) + { + string name = propertyInfo.Name; + object value = propertyInfo.GetValue(obj, null); + result[name] = value; + } + + return result; + } + + public static string AppendStrings(this IEnumerable list, string seperator = ", ") + { + return list.Aggregate( + new StringBuilder(), + (sb, s) => (sb.Length == 0 ? sb : sb.Append(seperator)).Append(s), + sb => sb.ToString()); + } + + public static bool IsSimpleType(Type type) + { + Type actualType = type; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + actualType = type.GetGenericArguments()[0]; + } + + return _simpleTypes.Contains(actualType); + } + + public static string GetParameterName(this IDictionary parameters, string parameterName, char parameterPrefix) + { + return string.Format("{0}{1}_{2}", parameterPrefix, parameterName, parameters.Count); + } + + public static string SetParameterName(this IDictionary parameters, string parameterName, object value, char parameterPrefix) + { + string name = parameters.GetParameterName(parameterName, parameterPrefix); + parameters.Add(name, value); + return name; + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/DB2Dialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/DB2Dialect.cs new file mode 100644 index 0000000..b1fc379 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/DB2Dialect.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DapperExtensions.Sql +{ + public class DB2Dialect : SqlDialectBase + { + public override string GetIdentitySql(string tableName) + { + return "SELECT CAST(IDENTITY_VAL_LOCAL() AS BIGINT) AS \"ID\" FROM SYSIBM.SYSDUMMY1"; + } + + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + var startValue = ((page - 1) * resultsPerPage) + 1; + var endValue = (page * resultsPerPage); + return GetSetSql(sql, startValue, endValue, parameters); + } + + public override string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters) + { + if (string.IsNullOrEmpty(sql)) + { + throw new ArgumentNullException("SQL"); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + int selectIndex = GetSelectEnd(sql) + 1; + string orderByClause = GetOrderByClause(sql); + if (orderByClause == null) + { + orderByClause = "ORDER BY CURRENT_TIMESTAMP"; + } + + + string projectedColumns = GetColumnNames(sql).Aggregate(new StringBuilder(), (sb, s) => (sb.Length == 0 ? sb : sb.Append(", ")).Append(GetColumnName("_TEMP", s, null)), sb => sb.ToString()); + string newSql = sql + .Replace(" " + orderByClause, string.Empty) + .Insert(selectIndex, string.Format("ROW_NUMBER() OVER(ORDER BY {0}) AS {1}, ", orderByClause.Substring(9), GetColumnName(null, "_ROW_NUMBER", null))); + + string result = string.Format("SELECT {0} FROM ({1}) AS \"_TEMP\" WHERE {2} BETWEEN @_pageStartRow AND @_pageEndRow", + projectedColumns.Trim(), newSql, GetColumnName("_TEMP", "_ROW_NUMBER", null)); + + parameters.Add("@_pageStartRow", firstResult); + parameters.Add("@_pageEndRow", maxResults); + return result; + } + + protected string GetOrderByClause(string sql) + { + int orderByIndex = sql.LastIndexOf(" ORDER BY ", StringComparison.InvariantCultureIgnoreCase); + if (orderByIndex == -1) + { + return null; + } + + string result = sql.Substring(orderByIndex).Trim(); + + int whereIndex = result.IndexOf(" WHERE ", StringComparison.InvariantCultureIgnoreCase); + if (whereIndex == -1) + { + return result; + } + + return result.Substring(0, whereIndex).Trim(); + } + + protected int GetFromStart(string sql) + { + int selectCount = 0; + string[] words = sql.Split(' '); + int fromIndex = 0; + foreach (var word in words) + { + if (word.Equals("SELECT", StringComparison.InvariantCultureIgnoreCase)) + { + selectCount++; + } + + if (word.Equals("FROM", StringComparison.InvariantCultureIgnoreCase)) + { + selectCount--; + if (selectCount == 0) + { + break; + } + } + + fromIndex += word.Length + 1; + } + + return fromIndex; + } + + protected virtual int GetSelectEnd(string sql) + { + if (sql.StartsWith("SELECT DISTINCT", StringComparison.InvariantCultureIgnoreCase)) + { + return 15; + } + + if (sql.StartsWith("SELECT", StringComparison.InvariantCultureIgnoreCase)) + { + return 6; + } + + throw new ArgumentException("SQL must be a SELECT statement.", "sql"); + } + + protected virtual IList GetColumnNames(string sql) + { + int start = GetSelectEnd(sql); + int stop = GetFromStart(sql); + string[] columnSql = sql.Substring(start, stop - start).Split(','); + List result = new List(); + foreach (string c in columnSql) + { + int index = c.IndexOf(" AS ", StringComparison.InvariantCultureIgnoreCase); + if (index > 0) + { + result.Add(c.Substring(index + 4).Trim()); + continue; + } + + string[] colParts = c.Split('.'); + result.Add(colParts[colParts.Length - 1].Trim()); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/MySqlDialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/MySqlDialect.cs new file mode 100644 index 0000000..92959ba --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/MySqlDialect.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DapperExtensions.Sql +{ + public class MySqlDialect : SqlDialectBase + { + public override char OpenQuote + { + get { return '`'; } + } + + public override char CloseQuote + { + get { return '`'; } + } + + public override string GetIdentitySql(string tableName) + { + return "SELECT CONVERT(LAST_INSERT_ID(), SIGNED INTEGER) AS ID"; + } + + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + int startValue = page * resultsPerPage; + return GetSetSql(sql, startValue, resultsPerPage, parameters); + } + + public override string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters) + { + string result = string.Format("{0} LIMIT @firstResult, @maxResults", sql); + parameters.Add("@firstResult", firstResult); + parameters.Add("@maxResults", maxResults); + return result; + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/OracleDialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/OracleDialect.cs new file mode 100644 index 0000000..a681984 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/OracleDialect.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Text; + +namespace DapperExtensions.Sql +{ + public class OracleDialect : SqlDialectBase + { + public OracleDialect() { } + + public override string GetIdentitySql(string tableName) + { + throw new System.NotImplementedException("Oracle does not support get last inserted identity."); + } + + public override bool SupportsMultipleStatements + { + get { return false; } + } + + //from Simple.Data.Oracle implementation https://github.com/flq/Simple.Data.Oracle/blob/master/Simple.Data.Oracle/OraclePager.cs + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + var toSkip = page * resultsPerPage; + var topLimit = (page + 1) * resultsPerPage; + + var sb = new StringBuilder(); + sb.AppendLine("SELECT * FROM ("); + sb.AppendLine("SELECT \"_ss_dapper_1_\".*, ROWNUM RNUM FROM ("); + sb.Append(sql); + sb.AppendLine(") \"_ss_dapper_1_\""); + sb.AppendLine("WHERE ROWNUM <= :topLimit) \"_ss_dapper_2_\" "); + sb.AppendLine("WHERE \"_ss_dapper_2_\".RNUM > :toSkip"); + + parameters.Add(":topLimit", topLimit); + parameters.Add(":toSkip", toSkip); + + return sb.ToString(); + } + + public override string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters) + { + var sb = new StringBuilder(); + sb.AppendLine("SELECT * FROM ("); + sb.AppendLine("SELECT \"_ss_dapper_1_\".*, ROWNUM RNUM FROM ("); + sb.Append(sql); + sb.AppendLine(") \"_ss_dapper_1_\""); + sb.AppendLine("WHERE ROWNUM <= :topLimit) \"_ss_dapper_2_\" "); + sb.AppendLine("WHERE \"_ss_dapper_2_\".RNUM > :toSkip"); + + parameters.Add(":topLimit", maxResults + firstResult); + parameters.Add(":toSkip", firstResult); + + return sb.ToString(); + } + + public override string QuoteString(string value) + { + if (value != null && value[0]=='`') + { + return string.Format("{0}{1}{2}", OpenQuote, value.Substring(1, value.Length - 2), CloseQuote); + } + return value.ToUpper(); + } + + public override char ParameterPrefix + { + get { return ':'; } + } + + public override char OpenQuote + { + get { return '"'; } + } + + public override char CloseQuote + { + get { return '"'; } + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/PostgreSqlDialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/PostgreSqlDialect.cs new file mode 100644 index 0000000..1dd4eeb --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/PostgreSqlDialect.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DapperExtensions.Sql +{ + public class PostgreSqlDialect : SqlDialectBase + { + public override string GetIdentitySql(string tableName) + { + return "SELECT LASTVAL() AS Id"; + } + + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + int startValue = page * resultsPerPage; + return GetSetSql(sql, startValue, resultsPerPage, parameters); + } + + public override string GetSetSql(string sql, int pageNumber, int maxResults, IDictionary parameters) + { + string result = string.Format("{0} LIMIT @maxResults OFFSET @pageStartRowNbr", sql); + parameters.Add("@maxResults", maxResults); + parameters.Add("@pageStartRowNbr", pageNumber * maxResults); + return result; + } + + public override string GetColumnName(string prefix, string columnName, string alias) + { + return base.GetColumnName(null, columnName, alias).ToLower(); + } + + public override string GetTableName(string schemaName, string tableName, string alias) + { + return base.GetTableName(schemaName, tableName, alias).ToLower(); + } + } + +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlCeDialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlCeDialect.cs new file mode 100644 index 0000000..66ed83b --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlCeDialect.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DapperExtensions.Sql +{ + public class SqlCeDialect : SqlDialectBase + { + public override char OpenQuote + { + get { return '['; } + } + + public override char CloseQuote + { + get { return ']'; } + } + + public override bool SupportsMultipleStatements + { + get { return false; } + } + + public override string GetTableName(string schemaName, string tableName, string alias) + { + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentNullException("TableName"); + } + + StringBuilder result = new StringBuilder(); + result.Append(OpenQuote); + if (!string.IsNullOrWhiteSpace(schemaName)) + { + result.AppendFormat("{0}_", schemaName); + } + + result.AppendFormat("{0}{1}", tableName, CloseQuote); + + + if (!string.IsNullOrWhiteSpace(alias)) + { + result.AppendFormat(" AS {0}{1}{2}", OpenQuote, alias, CloseQuote); + } + + return result.ToString(); + } + + public override string GetIdentitySql(string tableName) + { + return "SELECT CAST(@@IDENTITY AS BIGINT) AS [Id]"; + } + + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + int startValue = (page * resultsPerPage); + return GetSetSql(sql, startValue, resultsPerPage, parameters); + } + + public override string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters) + { + string result = string.Format("{0} OFFSET @firstResult ROWS FETCH NEXT @maxResults ROWS ONLY", sql); + parameters.Add("@firstResult", firstResult); + parameters.Add("@maxResults", maxResults); + return result; + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlDialectBase.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlDialectBase.cs new file mode 100644 index 0000000..c7ea9d5 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlDialectBase.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DapperExtensions.Sql +{ + public interface ISqlDialect + { + char OpenQuote { get; } + char CloseQuote { get; } + string BatchSeperator { get; } + bool SupportsMultipleStatements { get; } + char ParameterPrefix { get; } + string EmptyExpression { get; } + string GetTableName(string schemaName, string tableName, string alias); + string GetColumnName(string prefix, string columnName, string alias); + string GetIdentitySql(string tableName); + string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters); + string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters); + bool IsQuoted(string value); + string QuoteString(string value); + } + + public abstract class SqlDialectBase : ISqlDialect + { + public virtual char OpenQuote + { + get { return '"'; } + } + + public virtual char CloseQuote + { + get { return '"'; } + } + + public virtual string BatchSeperator + { + get { return ";" + Environment.NewLine; } + } + + public virtual bool SupportsMultipleStatements + { + get { return true; } + } + + public virtual char ParameterPrefix + { + get + { + return '@'; + } + } + + public string EmptyExpression + { + get + { + return "1=1"; + } + } + + public virtual string GetTableName(string schemaName, string tableName, string alias) + { + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentNullException("TableName", "tableName cannot be null or empty."); + } + + StringBuilder result = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(schemaName)) + { + result.AppendFormat(QuoteString(schemaName) + "."); + } + + result.AppendFormat(QuoteString(tableName)); + + if (!string.IsNullOrWhiteSpace(alias)) + { + result.AppendFormat(" AS {0}", QuoteString(alias)); + } + return result.ToString(); + } + + public virtual string GetColumnName(string prefix, string columnName, string alias) + { + if (string.IsNullOrWhiteSpace(columnName)) + { + throw new ArgumentNullException("ColumnName", "columnName cannot be null or empty."); + } + + StringBuilder result = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(prefix)) + { + result.AppendFormat(QuoteString(prefix) + "."); + } + + result.AppendFormat(QuoteString(columnName)); + + if (!string.IsNullOrWhiteSpace(alias)) + { + result.AppendFormat(" AS {0}", QuoteString(alias)); + } + + return result.ToString(); + } + + public abstract string GetIdentitySql(string tableName); + public abstract string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters); + public abstract string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters); + + public virtual bool IsQuoted(string value) + { + if (value.Trim()[0] == OpenQuote) + { + return value.Trim().Last() == CloseQuote; + } + + return false; + } + + public virtual string QuoteString(string value) + { + if (IsQuoted(value) || value == "*") + { + return value; + } + return string.Format("{0}{1}{2}", OpenQuote, value.Trim(), CloseQuote); + } + + public virtual string UnQuoteString(string value) + { + return IsQuoted(value) ? value.Substring(1, value.Length - 2) : value; + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlGenerator.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlGenerator.cs new file mode 100644 index 0000000..2488111 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlGenerator.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DapperExtensions.Mapper; + +namespace DapperExtensions.Sql +{ + public interface ISqlGenerator + { + IDapperExtensionsConfiguration Configuration { get; } + + string Select(IClassMapper classMap, IPredicate predicate, IList sort, IDictionary parameters); + string SelectPaged(IClassMapper classMap, IPredicate predicate, IList sort, int page, int resultsPerPage, IDictionary parameters); + string SelectSet(IClassMapper classMap, IPredicate predicate, IList sort, int firstResult, int maxResults, IDictionary parameters); + string Count(IClassMapper classMap, IPredicate predicate, IDictionary parameters); + + string Insert(IClassMapper classMap); + string Update(IClassMapper classMap, IPredicate predicate, IDictionary parameters, bool ignoreAllKeyProperties); + string Delete(IClassMapper classMap, IPredicate predicate, IDictionary parameters); + + string IdentitySql(IClassMapper classMap); + string GetTableName(IClassMapper map); + string GetColumnName(IClassMapper map, IPropertyMap property, bool includeAlias); + string GetColumnName(IClassMapper map, string propertyName, bool includeAlias); + bool SupportsMultipleStatements(); + } + + public class SqlGeneratorImpl : ISqlGenerator + { + public SqlGeneratorImpl(IDapperExtensionsConfiguration configuration) + { + Configuration = configuration; + } + + public IDapperExtensionsConfiguration Configuration { get; private set; } + + public virtual string Select(IClassMapper classMap, IPredicate predicate, IList sort, IDictionary parameters) + { + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + StringBuilder sql = new StringBuilder(string.Format("SELECT {0} FROM {1}", + BuildSelectColumns(classMap), + GetTableName(classMap))); + if (predicate != null) + { + sql.Append(" WHERE ") + .Append(predicate.GetSql(this, parameters)); + } + + if (sort != null && sort.Any()) + { + sql.Append(" ORDER BY ") + .Append(sort.Select(s => GetColumnName(classMap, s.PropertyName, false) + (s.Ascending ? " ASC" : " DESC")).AppendStrings()); + } + + return sql.ToString(); + } + + public virtual string SelectPaged(IClassMapper classMap, IPredicate predicate, IList sort, int page, int resultsPerPage, IDictionary parameters) + { + if (sort == null || !sort.Any()) + { + throw new ArgumentNullException("Sort", "Sort cannot be null or empty."); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + StringBuilder innerSql = new StringBuilder(string.Format("SELECT {0} FROM {1}", + BuildSelectColumns(classMap), + GetTableName(classMap))); + if (predicate != null) + { + innerSql.Append(" WHERE ") + .Append(predicate.GetSql(this, parameters)); + } + + string orderBy = sort.Select(s => GetColumnName(classMap, s.PropertyName, false) + (s.Ascending ? " ASC" : " DESC")).AppendStrings(); + innerSql.Append(" ORDER BY " + orderBy); + + string sql = Configuration.Dialect.GetPagingSql(innerSql.ToString(), page, resultsPerPage, parameters); + return sql; + } + + public virtual string SelectSet(IClassMapper classMap, IPredicate predicate, IList sort, int firstResult, int maxResults, IDictionary parameters) + { + if (sort == null || !sort.Any()) + { + throw new ArgumentNullException("Sort", "Sort cannot be null or empty."); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + StringBuilder innerSql = new StringBuilder(string.Format("SELECT {0} FROM {1}", + BuildSelectColumns(classMap), + GetTableName(classMap))); + if (predicate != null) + { + innerSql.Append(" WHERE ") + .Append(predicate.GetSql(this, parameters)); + } + + string orderBy = sort.Select(s => GetColumnName(classMap, s.PropertyName, false) + (s.Ascending ? " ASC" : " DESC")).AppendStrings(); + innerSql.Append(" ORDER BY " + orderBy); + + string sql = Configuration.Dialect.GetSetSql(innerSql.ToString(), firstResult, maxResults, parameters); + return sql; + } + + + public virtual string Count(IClassMapper classMap, IPredicate predicate, IDictionary parameters) + { + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + StringBuilder sql = new StringBuilder(string.Format("SELECT COUNT(*) AS {0}Total{1} FROM {2}", + Configuration.Dialect.OpenQuote, + Configuration.Dialect.CloseQuote, + GetTableName(classMap))); + if (predicate != null) + { + sql.Append(" WHERE ") + .Append(predicate.GetSql(this, parameters)); + } + + return sql.ToString(); + } + + public virtual string Insert(IClassMapper classMap) + { + var columns = classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly || p.KeyType == KeyType.Identity || p.KeyType == KeyType.TriggerIdentity)); + if (!columns.Any()) + { + throw new ArgumentException("No columns were mapped."); + } + + var columnNames = columns.Select(p => GetColumnName(classMap, p, false)); + var parameters = columns.Select(p => Configuration.Dialect.ParameterPrefix + p.Name); + + string sql = string.Format("INSERT INTO {0} ({1}) VALUES ({2})", + GetTableName(classMap), + columnNames.AppendStrings(), + parameters.AppendStrings()); + + var triggerIdentityColumn = classMap.Properties.Where(p => p.KeyType == KeyType.TriggerIdentity).ToList(); + + if (triggerIdentityColumn.Count > 0) + { + if (triggerIdentityColumn.Count > 1) + throw new ArgumentException("TriggerIdentity generator cannot be used with multi-column keys"); + + sql += string.Format(" RETURNING {0} INTO {1}IdOutParam", triggerIdentityColumn.Select(p => GetColumnName(classMap, p, false)).First(), Configuration.Dialect.ParameterPrefix); + } + + return sql; + } + + public virtual string Update(IClassMapper classMap, IPredicate predicate, IDictionary parameters, bool ignoreAllKeyProperties) + { + if (predicate == null) + { + throw new ArgumentNullException("Predicate"); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + var columns = ignoreAllKeyProperties + ? classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly) && p.KeyType == KeyType.NotAKey) + : classMap.Properties.Where(p => !(p.Ignored || p.IsReadOnly || p.KeyType == KeyType.Identity || p.KeyType == KeyType.Assigned)); + + if (!columns.Any()) + { + throw new ArgumentException("No columns were mapped."); + } + + var setSql = + columns.Select( + p => + string.Format( + "{0} = {1}{2}", GetColumnName(classMap, p, false), Configuration.Dialect.ParameterPrefix, p.Name)); + + return string.Format("UPDATE {0} SET {1} WHERE {2}", + GetTableName(classMap), + setSql.AppendStrings(), + predicate.GetSql(this, parameters)); + } + + public virtual string Delete(IClassMapper classMap, IPredicate predicate, IDictionary parameters) + { + if (predicate == null) + { + throw new ArgumentNullException("Predicate"); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + StringBuilder sql = new StringBuilder(string.Format("DELETE FROM {0}", GetTableName(classMap))); + sql.Append(" WHERE ").Append(predicate.GetSql(this, parameters)); + return sql.ToString(); + } + + public virtual string IdentitySql(IClassMapper classMap) + { + return Configuration.Dialect.GetIdentitySql(GetTableName(classMap)); + } + + public virtual string GetTableName(IClassMapper map) + { + return Configuration.Dialect.GetTableName(map.SchemaName, map.TableName, null); + } + + public virtual string GetColumnName(IClassMapper map, IPropertyMap property, bool includeAlias) + { + string alias = null; + if (property.ColumnName != property.Name && includeAlias) + { + alias = property.Name; + } + + return Configuration.Dialect.GetColumnName(GetTableName(map), property.ColumnName, alias); + } + + public virtual string GetColumnName(IClassMapper map, string propertyName, bool includeAlias) + { + IPropertyMap propertyMap = map.Properties.SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase)); + if (propertyMap == null) + { + throw new ArgumentException(string.Format("Could not find '{0}' in Mapping.", propertyName)); + } + + return GetColumnName(map, propertyMap, includeAlias); + } + + public virtual bool SupportsMultipleStatements() + { + return Configuration.Dialect.SupportsMultipleStatements; + } + + public virtual string BuildSelectColumns(IClassMapper classMap) + { + var columns = classMap.Properties + .Where(p => !p.Ignored) + .Select(p => GetColumnName(classMap, p, true)); + return columns.AppendStrings(); + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlServerDialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlServerDialect.cs new file mode 100644 index 0000000..56a9d22 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqlServerDialect.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DapperExtensions.Sql +{ + public class SqlServerDialect : SqlDialectBase + { + public override char OpenQuote + { + get { return '['; } + } + + public override char CloseQuote + { + get { return ']'; } + } + + public override string GetIdentitySql(string tableName) + { + return string.Format("SELECT CAST(SCOPE_IDENTITY() AS BIGINT) AS [Id]"); + } + + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + int startValue = (page * resultsPerPage) + 1; + return GetSetSql(sql, startValue, resultsPerPage, parameters); + } + + public override string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters) + { + if (string.IsNullOrEmpty(sql)) + { + throw new ArgumentNullException("SQL"); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + int selectIndex = GetSelectEnd(sql) + 1; + string orderByClause = GetOrderByClause(sql); + if (orderByClause == null) + { + orderByClause = "ORDER BY CURRENT_TIMESTAMP"; + } + + + string projectedColumns = GetColumnNames(sql).Aggregate(new StringBuilder(), (sb, s) => (sb.Length == 0 ? sb : sb.Append(", ")).Append(GetColumnName("_proj", s, null)), sb => sb.ToString()); + string newSql = sql + .Replace(" " + orderByClause, string.Empty) + .Insert(selectIndex, string.Format("ROW_NUMBER() OVER(ORDER BY {0}) AS {1}, ", orderByClause.Substring(9), GetColumnName(null, "_row_number", null))); + + string result = string.Format("SELECT TOP({0}) {1} FROM ({2}) [_proj] WHERE {3} >= @_pageStartRow ORDER BY {3}", + maxResults, projectedColumns.Trim(), newSql, GetColumnName("_proj", "_row_number", null)); + + parameters.Add("@_pageStartRow", firstResult); + return result; + } + + protected string GetOrderByClause(string sql) + { + int orderByIndex = sql.LastIndexOf(" ORDER BY ", StringComparison.InvariantCultureIgnoreCase); + if (orderByIndex == -1) + { + return null; + } + + string result = sql.Substring(orderByIndex).Trim(); + + int whereIndex = result.IndexOf(" WHERE ", StringComparison.InvariantCultureIgnoreCase); + if (whereIndex == -1) + { + return result; + } + + return result.Substring(0, whereIndex).Trim(); + } + + protected int GetFromStart(string sql) + { + int selectCount = 0; + string[] words = sql.Split(' '); + int fromIndex = 0; + foreach (var word in words) + { + if (word.Equals("SELECT", StringComparison.InvariantCultureIgnoreCase)) + { + selectCount++; + } + + if (word.Equals("FROM", StringComparison.InvariantCultureIgnoreCase)) + { + selectCount--; + if (selectCount == 0) + { + break; + } + } + + fromIndex += word.Length + 1; + } + + return fromIndex; + } + + protected virtual int GetSelectEnd(string sql) + { + if (sql.StartsWith("SELECT DISTINCT", StringComparison.InvariantCultureIgnoreCase)) + { + return 15; + } + + if (sql.StartsWith("SELECT", StringComparison.InvariantCultureIgnoreCase)) + { + return 6; + } + + throw new ArgumentException("SQL must be a SELECT statement.", "sql"); + } + + protected virtual IList GetColumnNames(string sql) + { + int start = GetSelectEnd(sql); + int stop = GetFromStart(sql); + string[] columnSql = sql.Substring(start, stop - start).Split(','); + List result = new List(); + foreach (string c in columnSql) + { + int index = c.IndexOf(" AS ", StringComparison.InvariantCultureIgnoreCase); + if (index > 0) + { + result.Add(c.Substring(index + 4).Trim()); + continue; + } + + string[] colParts = c.Split('.'); + result.Add(colParts[colParts.Length - 1].Trim()); + } + + return result; + } + } +} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqliteDialect.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqliteDialect.cs new file mode 100644 index 0000000..ed664d9 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperExtensions/Sql/SqliteDialect.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DapperExtensions.Sql +{ + public class SqliteDialect : SqlDialectBase + { + public override string GetIdentitySql(string tableName) + { + return "SELECT LAST_INSERT_ROWID() AS [Id]"; + } + + public override string GetPagingSql(string sql, int page, int resultsPerPage, IDictionary parameters) + { + int startValue = page * resultsPerPage; + return GetSetSql(sql, startValue, resultsPerPage, parameters); + } + + public override string GetSetSql(string sql, int firstResult, int maxResults, IDictionary parameters) + { + if (string.IsNullOrEmpty(sql)) + { + throw new ArgumentNullException("SQL"); + } + + if (parameters == null) + { + throw new ArgumentNullException("Parameters"); + } + + var result = string.Format("{0} LIMIT @Offset, @Count", sql); + parameters.Add("@Offset", firstResult); + parameters.Add("@Count", maxResults); + return result; + } + + public override string GetColumnName(string prefix, string columnName, string alias) + { + if (string.IsNullOrWhiteSpace(columnName)) + { + throw new ArgumentNullException(columnName, "columnName cannot be null or empty."); + } + var result = new StringBuilder(); + result.AppendFormat(columnName); + if (!string.IsNullOrWhiteSpace(alias)) + { + result.AppendFormat(" AS {0}", QuoteString(alias)); + } + return result.ToString(); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/DapperHelper.cs b/Api/Ewide.Core/Ewide.Core.Data/DapperHelper.cs new file mode 100644 index 0000000..948ddfc --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/DapperHelper.cs @@ -0,0 +1,82 @@ +using MySql.Data.MySqlClient; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ewide.Core.Data +{ + public class DapperHelper : IDisposable + { + private readonly static string _connString = System.Configuration.ConfigurationManager.ConnectionStrings["MySqlConnection"].ToString(); + + public IDbConnection Sql = null; + + public DapperHelper() + { + Sql = new MySqlConnection(_connString); + if (Sql.State == ConnectionState.Closed) + { + Sql.Open(); + } + } + + void IDisposable.Dispose() + { + if (Sql.State == ConnectionState.Open) + { + Sql.Close(); + } + Sql.Dispose(); + } + } + + public class DapperTransactionHelper : IDisposable + { + private readonly static string _connString = System.Configuration.ConfigurationManager.ConnectionStrings["MySqlConnection"].ToString(); + + public IDbConnection Sql = null; + + private IDbTransaction _trans = null; + + public DapperTransactionHelper() + { + Sql = new MySqlConnection(_connString); + if (Sql.State == ConnectionState.Closed) + { + Sql.Open(); + } + _trans = Sql.BeginTransaction(); + } + + private void Close() + { + if (Sql.State == ConnectionState.Open) + { + Sql.Close(); + } + } + + void IDisposable.Dispose() + { + _trans.Dispose(); + this.Close(); + Sql.Dispose(); + } + + public void Complete() + { + _trans.Commit(); + this.Close(); + } + + public void RollBack() + { + _trans.Rollback(); + this.Close(); + } + + } +} diff --git a/Api/Ewide.Core/Ewide.Core.Data/Ewide.Core.Data.csproj b/Api/Ewide.Core/Ewide.Core.Data/Ewide.Core.Data.csproj new file mode 100644 index 0000000..3a6ab8e --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Ewide.Core.Data.csproj @@ -0,0 +1,167 @@ + + + + + Debug + AnyCPU + {B5B46BAD-81E3-4DF0-83EF-75148236F7CE} + Library + Properties + Ewide.Core.Data + Ewide.Core.Data + v4.5.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\BouncyCastle.1.8.5\lib\BouncyCastle.Crypto.dll + + + ..\packages\Google.Protobuf.3.11.4\lib\net45\Google.Protobuf.dll + + + ..\packages\K4os.Compression.LZ4.1.1.11\lib\net45\K4os.Compression.LZ4.dll + + + ..\packages\K4os.Compression.LZ4.Streams.1.1.11\lib\net45\K4os.Compression.LZ4.Streams.dll + + + ..\packages\K4os.Hash.xxHash.1.0.6\lib\net45\K4os.Hash.xxHash.dll + + + ..\packages\MySql.Data.8.0.23\lib\net452\MySql.Data.dll + + + ..\packages\SSH.NET.2020.0.0\lib\net40\Renci.SshNet.dll + + + + ..\packages\System.Buffers.4.5.1\lib\netstandard1.1\System.Buffers.dll + + + + + + + + + ..\packages\System.Memory.4.5.3\lib\netstandard1.1\System.Memory.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll + + + + + + + + + + ..\packages\MySql.Data.8.0.23\lib\net452\Ubiety.Dns.Core.dll + + + ..\packages\MySql.Data.8.0.23\lib\net452\Zstandard.Net.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {31c3ca3d-14a1-453a-866d-76d4c74a9bdc} + Ewide.Core.Model + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/Properties/AssemblyInfo.cs b/Api/Ewide.Core/Ewide.Core.Data/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1b0f42d --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("Ewide.Core.Data")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ewide.Core.Data")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("b5b46bad-81e3-4df0-83ef-75148236f7ce")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Api/Ewide.Core/Ewide.Core.Data/app.config b/Api/Ewide.Core/Ewide.Core.Data/app.config new file mode 100644 index 0000000..bdaf331 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Data/packages.config b/Api/Ewide.Core/Ewide.Core.Data/packages.config new file mode 100644 index 0000000..b71dc9d --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Data/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Model/Ewide.Core.Model.csproj b/Api/Ewide.Core/Ewide.Core.Model/Ewide.Core.Model.csproj index 2fdd6b7..dab9953 100644 --- a/Api/Ewide.Core/Ewide.Core.Model/Ewide.Core.Model.csproj +++ b/Api/Ewide.Core/Ewide.Core.Model/Ewide.Core.Model.csproj @@ -9,9 +9,12 @@ Properties Ewide.Core.Model Ewide.Core.Model - v4.5 + v4.5.2 512 true + + + true @@ -32,7 +35,17 @@ + + + + + + + + + + @@ -43,5 +56,8 @@ + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Model/app.config b/Api/Ewide.Core/Ewide.Core.Model/app.config new file mode 100644 index 0000000..eee0eef --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Model/app.config @@ -0,0 +1,37 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Service/Ewide.Core.Service.csproj b/Api/Ewide.Core/Ewide.Core.Service/Ewide.Core.Service.csproj index 7564118..dc47421 100644 --- a/Api/Ewide.Core/Ewide.Core.Service/Ewide.Core.Service.csproj +++ b/Api/Ewide.Core/Ewide.Core.Service/Ewide.Core.Service.csproj @@ -9,9 +9,10 @@ Properties Ewide.Core.Service Ewide.Core.Service - v4.5 + v4.5.2 512 true + true @@ -53,5 +54,8 @@ Ewide.Core.Utility + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Service/app.config b/Api/Ewide.Core/Ewide.Core.Service/app.config new file mode 100644 index 0000000..89c0ff0 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.Service/app.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.Utility/Ewide.Core.Utility.csproj b/Api/Ewide.Core/Ewide.Core.Utility/Ewide.Core.Utility.csproj index 2fe1559..efff9f9 100644 --- a/Api/Ewide.Core/Ewide.Core.Utility/Ewide.Core.Utility.csproj +++ b/Api/Ewide.Core/Ewide.Core.Utility/Ewide.Core.Utility.csproj @@ -9,9 +9,10 @@ Properties Ewide.Core.Utility Ewide.Core.Utility - v4.5 + v4.5.2 512 true + true diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/App_Start/WebApiConfig.cs b/Api/Ewide.Core/Ewide.Core.WebApi/App_Start/WebApiConfig.cs index 1f72b83..545e8f8 100644 --- a/Api/Ewide.Core/Ewide.Core.WebApi/App_Start/WebApiConfig.cs +++ b/Api/Ewide.Core/Ewide.Core.WebApi/App_Start/WebApiConfig.cs @@ -16,9 +16,14 @@ namespace Ewide.Core.WebApi // Web API 路由 config.MapHttpAttributeRoutes(); + #region 处理接口报错 + // 404 config.Services.Replace(typeof(IHttpControllerSelector), new HttpNotFoundDefaultHttpControllerSelector(config)); + // 500 config.Services.Replace(typeof(IHttpActionInvoker), new HttpWebApiControllerActionInvoker(config)); + // 405 config.Services.Replace(typeof(IHttpActionSelector), new HttpNotFoundControllerActionSelector(config)); + #endregion config.Routes.MapHttpRoute( name: "DefaultApi", diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Controllers/LoginController.cs b/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Controllers/LoginController.cs deleted file mode 100644 index 665f974..0000000 --- a/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Controllers/LoginController.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; - -namespace Ewide.Core.WebApi.Areas.Gate.Controllers -{ - public class LoginController : BaseController - { - public IHttpActionResult Index() - { - return DisplayJSON("123"); - } - - [HttpPost] - public IHttpActionResult Test() - { - throw new Exception("dsds"); - return DisplayJSON("123"); - } - - public IHttpActionResult Aaa() - { - return DisplayJSON("aaa"); - } - } -} diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/GateAreaRegistration.cs b/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/GateAreaRegistration.cs deleted file mode 100644 index 60830d9..0000000 --- a/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/GateAreaRegistration.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Web.Mvc; - -namespace Ewide.Core.WebApi.Areas.Gate -{ - public class GateAreaRegistration : AreaRegistration - { - public override string AreaName - { - get - { - return "Gate"; - } - } - - public override void RegisterArea(AreaRegistrationContext context) - { - context.MapRoute( - "Gate_default", - "Gate/{controller}/{action}/{id}", - new { action = "Index", id = UrlParameter.Optional } - ); - } - } -} \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Views/web.config b/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Views/web.config deleted file mode 100644 index 65f2f0a..0000000 --- a/Api/Ewide.Core/Ewide.Core.WebApi/Areas/Gate/Views/web.config +++ /dev/null @@ -1,36 +0,0 @@ - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/BaseController.cs b/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/BaseController.cs index a71d7ab..72f7d3a 100644 --- a/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/BaseController.cs +++ b/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/BaseController.cs @@ -30,13 +30,7 @@ namespace Ewide.Core.WebApi /// private IHttpActionResult _DisplayJSON(object obj) { - var _result = JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings - { - ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), - DateFormatString = "yyyy-MM-dd HH:mm:ss" - }); - - var result = JsonConvert.DeserializeObject(_result); + var result = BaseDisplayJSON.DisplayJSON(obj); return Ok(result); } diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/ValidateArgumentsFilter.cs b/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/ValidateArgumentsFilter.cs new file mode 100644 index 0000000..6a58298 --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/Code/ValidateArgumentsFilter.cs @@ -0,0 +1,63 @@ +using Ewide.Core.Common; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; + +namespace Ewide.Core.WebApi +{ + public class ValidateArgumentsFilter : ActionFilterAttribute + { + + public override void OnActionExecuting(HttpActionContext actionContext) + { + if (!actionContext.ModelState.IsValid) + { + var message = new List(); + + var argsType = actionContext.ActionArguments.FirstOrDefault().Value.GetType(); + var properties = argsType.GetProperties(); + + foreach (var elem in actionContext.ModelState) + { + var key = elem.Key.Split(".".ToCharArray()).Last(); + key = key.First().ToString().ToLower() + key.Substring(1); + + var p = properties.FirstOrDefault(m => m.Name.Equals(key, StringComparison.CurrentCultureIgnoreCase)); + + var attribute = p.GetCustomAttributes(typeof(DisplayNameAttribute), true); + var displayName = String.Empty; + if(attribute.Count() > 0) + { + displayName = ((DisplayNameAttribute)attribute.FirstOrDefault()).DisplayName; + } + + var error = elem.Value.Errors.FirstOrDefault(); + var msg = error.ErrorMessage; + if (error.Exception != null) + { + msg = error.Exception.Message; + } + + message.Add(new + { + Key = key, + Name = displayName, + Message = msg + }); + } + + + actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.OK, + BaseDisplayJSON.DisplayJSON(BaseDisplayJSON.Display(HttpStatusCode.BadRequest, message)) + ); + } + base.OnActionExecuting(actionContext); + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/GateController.cs b/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/GateController.cs new file mode 100644 index 0000000..1a15d7e --- /dev/null +++ b/Api/Ewide.Core/Ewide.Core.WebApi/Controllers/GateController.cs @@ -0,0 +1,37 @@ +using Dapper; +using Ewide.Core.DTO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; + +namespace Ewide.Core.WebApi.Controllers +{ + [ValidateArgumentsFilter] + public class GateController : BaseController + { + public IHttpActionResult Login(TestArgs A) + { + using (var db = new Data.DapperHelper()) + { + var sw = new Stopwatch(); + sw.Start(); + db.Sql.Query("SELECT * FROM nbhudb_zzfwzxx").ToList(); + sw.Stop(); + var t1 = sw.Elapsed.Milliseconds; + sw.Restart(); + db.Sql.Query("SELECT * FROM nbhudb_zzfwzxx limit 3000,10"); + sw.Stop(); + var t2 = sw.Elapsed.Milliseconds; + return DisplayJSON(new + { + t1, + t2 + }); + } + } + } +} diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Ewide.Core.WebApi.csproj b/Api/Ewide.Core/Ewide.Core.WebApi/Ewide.Core.WebApi.csproj index 4b81b04..d54640c 100644 --- a/Api/Ewide.Core/Ewide.Core.WebApi/Ewide.Core.WebApi.csproj +++ b/Api/Ewide.Core/Ewide.Core.WebApi/Ewide.Core.WebApi.csproj @@ -14,7 +14,7 @@ Properties Ewide.Core.WebApi Ewide.Core.WebApi - v4.5 + v4.5.2 false true @@ -25,6 +25,7 @@ + true @@ -48,15 +49,15 @@ + + - - - + @@ -108,6 +109,7 @@ True ..\packages\Microsoft.AspNet.WebPages.3.2.4\lib\net45\System.Web.WebPages.Razor.dll + True ..\packages\WebGrease.1.6.0\lib\WebGrease.dll @@ -130,8 +132,6 @@ - - @@ -163,6 +163,8 @@ + + Global.asax @@ -217,8 +219,6 @@ - - @@ -250,14 +250,21 @@ - + + {9003B29C-AC1D-444E-8FE2-201F76D848A5} + Ewide.Core.DTO + {c7e2ac14-ac20-4552-a5b8-08b650ac8416} Ewide.Core.Common + + {b5b46bad-81e3-4df0-83ef-75148236f7ce} + Ewide.Core.Data + {34ae80c2-5c37-4b6c-aac3-f52c06928721} Ewide.Core.Service diff --git a/Api/Ewide.Core/Ewide.Core.WebApi/Web.config b/Api/Ewide.Core/Ewide.Core.WebApi/Web.config index 9b3544f..265ce45 100644 --- a/Api/Ewide.Core/Ewide.Core.WebApi/Web.config +++ b/Api/Ewide.Core/Ewide.Core.WebApi/Web.config @@ -4,65 +4,83 @@ https://go.microsoft.com/fwlink/?LinkId=301879 --> + + + - - - - + + + + + - - + + - - - - + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + - - + + - + \ No newline at end of file diff --git a/Api/Ewide.Core/Ewide.Core.sln b/Api/Ewide.Core/Ewide.Core.sln index 34dc651..629506c 100644 --- a/Api/Ewide.Core/Ewide.Core.sln +++ b/Api/Ewide.Core/Ewide.Core.sln @@ -17,10 +17,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ewide.Core.Utility", "Ewide EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ewide.Core.Service", "Ewide.Core.Service\Ewide.Core.Service.csproj", "{34AE80C2-5C37-4B6C-AAC3-F52C06928721}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ewide.Core.Arguments", "Ewide.Core.Arguments\Ewide.Core.Arguments.csproj", "{9003B29C-AC1D-444E-8FE2-201F76D848A5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ewide.Core.DTO", "Ewide.Core.Arguments\Ewide.Core.DTO.csproj", "{9003B29C-AC1D-444E-8FE2-201F76D848A5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ewide.Core.Common", "Ewide.Core.Common\Ewide.Core.Common.csproj", "{C7E2AC14-AC20-4552-A5B8-08B650AC8416}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ewide.Core.Data", "Ewide.Core.Data\Ewide.Core.Data.csproj", "{B5B46BAD-81E3-4DF0-83EF-75148236F7CE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {C7E2AC14-AC20-4552-A5B8-08B650AC8416}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7E2AC14-AC20-4552-A5B8-08B650AC8416}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7E2AC14-AC20-4552-A5B8-08B650AC8416}.Release|Any CPU.Build.0 = Release|Any CPU + {B5B46BAD-81E3-4DF0-83EF-75148236F7CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5B46BAD-81E3-4DF0-83EF-75148236F7CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5B46BAD-81E3-4DF0-83EF-75148236F7CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5B46BAD-81E3-4DF0-83EF-75148236F7CE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,8 +65,9 @@ Global {31C3CA3D-14A1-453A-866D-76D4C74A9BDC} = {30FD9EAF-7D70-4244-83AD-EB702BDC95ED} {D5C48D01-5AB1-44C9-8709-D4A336D19E9F} = {2D369B73-E5F6-412B-AD86-C5D1384D815B} {34AE80C2-5C37-4B6C-AAC3-F52C06928721} = {62222202-F9F0-4E0A-9C7F-CB69CBE60611} - {9003B29C-AC1D-444E-8FE2-201F76D848A5} = {62222202-F9F0-4E0A-9C7F-CB69CBE60611} + {9003B29C-AC1D-444E-8FE2-201F76D848A5} = {30FD9EAF-7D70-4244-83AD-EB702BDC95ED} {C7E2AC14-AC20-4552-A5B8-08B650AC8416} = {2D369B73-E5F6-412B-AD86-C5D1384D815B} + {B5B46BAD-81E3-4DF0-83EF-75148236F7CE} = {30FD9EAF-7D70-4244-83AD-EB702BDC95ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {435CD121-36FA-41DC-95FE-0B93517A1190}