diff --git a/sample/SqlServerEventStore/SqlServerEventStore/Program.cs b/sample/SqlServerEventStore/SqlServerEventStore/Program.cs index 5f4c2d0..8d176fc 100644 --- a/sample/SqlServerEventStore/SqlServerEventStore/Program.cs +++ b/sample/SqlServerEventStore/SqlServerEventStore/Program.cs @@ -70,8 +70,7 @@ void DoJsonSerializedEvents() public class BinarySampleRuntime : SimpleCqrs.SimpleCqrsRuntime { protected override IEventStore GetEventStore(SimpleCqrs.IServiceLocator serviceLocator) { - var configuration = new SqlServerConfiguration("Server=(local)\\sqlexpress;Database=test_event_store;Trusted_Connection=True;", - "dbo", "binary_event_store"); + var configuration = new SqlServerConfiguration(@"Server=(local)\sqlexpress;Database=test_event_store;Trusted_Connection=True;"); return new SqlServerEventStore(configuration, new BinaryDomainEventSerializer()); } } @@ -80,8 +79,7 @@ public class JsonSampleRuntime : SimpleCqrs.SimpleCqrsRuntime..\..\..\binaries\servicelocators\unity\Microsoft.Practices.Unity.dll - ..\..\..\src\EventStores\SimpleCqrs.EventStore.SqlServer\bin\Debug\ServiceStack.Text.dll + ..\..\..\lib\ServiceStack.Text.dll ..\..\..\binaries\SimpleCqrs.dll - ..\..\..\src\EventStores\SimpleCqrs.EventStore.SqlServer\bin\Debug\SimpleCqrs.EventStore.SqlServer.dll + ..\..\..\binaries\eventstores\sqlserver\SimpleCqrs.EventStore.SqlServer.dll ..\..\..\binaries\servicelocators\unity\SimpleCqrs.Unity.dll diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/IDomainEventSerializer.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/IDomainEventSerializer.cs new file mode 100644 index 0000000..9c6b5f9 --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/IDomainEventSerializer.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using SimpleCqrs.Eventing; + +namespace SimpleCqrs.EventStore.MySql +{ + public interface IDomainEventSerializer + { + string Serialize(DomainEvent domainEvent); + DomainEvent Deserialize(Type targetType, string serializedDomainEvent); + } +} diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlEventStore.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlEventStore.cs new file mode 100644 index 0000000..7c9551d --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlEventStore.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +// using System.Data.SqlClient; +using MySql.Data.MySqlClient; +using System.Linq; +using System.Text; +using ServiceStack.Text; +using SimpleCqrs.Eventing; +using Dapper; + +namespace SimpleCqrs.EventStore.MySql +{ + public class MySqlEventStore : IEventStore + { + private readonly IDomainEventSerializer serializer; + private readonly MySqlServerConfiguration configuration; + + public MySqlEventStore(MySqlServerConfiguration configuration, IDomainEventSerializer serializer) + { + this.serializer = serializer; + this.configuration = configuration; + Init(); + } + + public void Init() + { + using (var connection = new MySqlConnection(configuration.ConnectionString)) + { + connection.Open(); + var sql = string.Format(MySqlStatements.CreateTheEventStoreTable, "EventStore"); + using (var command = new MySqlCommand(sql, connection)) + command.ExecuteNonQuery(); + connection.Close(); + } + } + + public IEnumerable GetEvents(Guid aggregateRootId, int startSequence) + { + var events = new List(); + using (var connection = new MySqlConnection(configuration.ConnectionString)) + { + connection.Open(); + var sql = string.Format(MySqlStatements.GetEventsByAggregateRootAndSequence, "", "EventStore", aggregateRootId, + startSequence); + using (var command = new MySqlCommand(sql, connection)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) + { + var type = reader["EventType"].ToString(); + var data = reader["data"].ToString(); + + try + { + events.Add(serializer.Deserialize(Type.GetType(type), data)); + } catch(ArgumentNullException ex) + { + throw new Exception(string.Format("Cannot find type '{0}', yet the type is in the event store. Are you sure you haven't changed a class name or something arising from mental dullness?", type.Split(',')[0]), ex.InnerException); + } + } + connection.Close(); + } + return events; + } + + public void Insert(IEnumerable domainEvents) + { + var queries = domainEvents.Select( domainEvent => + new { + EventType = TypeToStringHelperMethods.GetString(domainEvent.GetType()), + AggregateRootId = domainEvent.AggregateRootId, + EventDate = domainEvent.EventDate, + Sequence = domainEvent.Sequence, + Data = (serializer.Serialize(domainEvent) ?? string.Empty) + .Replace("'", "''") + }); + + + if (!queries.Any()) return; + + using (var connection = new MySqlConnection(configuration.ConnectionString)) + { + connection.Open(); + connection.Execute(string.Format(MySqlStatements.InsertEvents, "EventStore"), queries); + connection.Close(); + } + } + + public IEnumerable GetEventsByEventTypes(IEnumerable domainEventTypes) + { + var events = new List(); + + var eventParameters = domainEventTypes.Select(TypeToStringHelperMethods.GetString).Join("','"); + + using (var connection = new MySqlConnection(configuration.ConnectionString)) + { + connection.Open(); + var sql = string.Format(MySqlStatements.GetEventsByType, "EventStore", eventParameters); + using (var command = new MySqlCommand(sql, connection)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) + { + var type = reader["EventType"].ToString(); + var data = reader["data"].ToString(); + + var domainEvent = serializer.Deserialize(Type.GetType(type), data); + events.Add(domainEvent); + } + connection.Close(); + } + return events; + } + + public IEnumerable GetEventsByEventTypes(IEnumerable domainEventTypes, Guid aggregateRootId) + { + throw new NotImplementedException(); + } + + public IEnumerable GetEventsByEventTypes(IEnumerable domainEventTypes, DateTime startDate, DateTime endDate) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlServerConfiguration.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlServerConfiguration.cs new file mode 100644 index 0000000..1bf4b6e --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlServerConfiguration.cs @@ -0,0 +1,17 @@ +namespace SimpleCqrs.EventStore.MySql +{ + public class MySqlServerConfiguration + { + private readonly string connectionString; + + public MySqlServerConfiguration(string connectionString) + { + this.connectionString = connectionString; + } + + public string ConnectionString + { + get { return connectionString; } + } + } +} \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlStatements.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlStatements.cs new file mode 100644 index 0000000..ec4bfd8 --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/MySqlStatements.cs @@ -0,0 +1,21 @@ +namespace SimpleCqrs.EventStore.MySql +{ + public class MySqlStatements + { + internal const string GetEventsByType = "SELECT EVENTTYPE, DATA FROM {0} WHERE EVENTTYPE IN (@EventType)"; + internal const string InsertEvents = @"INSERT INTO {0} (EventType, AggregateRootId, EventDate, Sequence, Data) VALUES ( @EventType, @AggregateRootId, @EventDate, @Sequence, @Data)"; + internal const string GetEventsByAggregateRootAndSequence = @"SELECT eventtype, data FROM {1} WHERE aggregaterootid = @AggregateRootId AND sequence >= @Sequence"; + internal const string CreateTheEventStoreTable = @"CREATE TABLE IF NOT EXISTS {0} +( + EventId INT AUTO_INCREMENT NOT NULL, + EventType VARCHAR(255), + AggregateRootId VARBINARY(36) NOT NULL, + EventDate DATETIME NOT NULL, + Sequence int NOT NULL, + Data BLOB, + CONSTRAINT PK_{0} PRIMARY KEY (EventId) +);"; + + + } +} \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/Properties/AssemblyInfo.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..b6bf0ad --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SimpleCqrs.EventStore.MySql")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Hewlett-Packard")] +[assembly: AssemblyProduct("SimpleCqrs.EventStore.MySql")] +[assembly: AssemblyCopyright("Copyright © Hewlett-Packard 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3d0353d4-22d9-4336-a0e9-2d7d380e2faa")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/Serializers/JsonDomainEventSerializer.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/Serializers/JsonDomainEventSerializer.cs new file mode 100644 index 0000000..4e5d8e4 --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/Serializers/JsonDomainEventSerializer.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using ServiceStack.Text; +using SimpleCqrs.Eventing; + +namespace SimpleCqrs.EventStore.MySql.Serializers +{ + public class JsonDomainEventSerializer : IDomainEventSerializer + { + + public string Serialize(DomainEvent domainEvent) + { + return JsonSerializer.SerializeToString(domainEvent, domainEvent.GetType()); + } + + public DomainEvent Deserialize(Type targetType, string serializedDomainEvent) + { + return (DomainEvent)JsonSerializer.DeserializeFromString(serializedDomainEvent, targetType); + } + } + + public class BinaryDomainEventSerializer : IDomainEventSerializer + { + public string Serialize(DomainEvent domainEvent) { + var formatter = new BinaryFormatter(); + using (var stream = new MemoryStream()) { + formatter.Serialize(stream, domainEvent); + stream.Flush(); + stream.Position = 0; + return Convert.ToBase64String(stream.ToArray()); + } + } + + public DomainEvent Deserialize(Type targetType, string serializedDomainEvent) { + var formatter = new BinaryFormatter(); + using (var stream = new MemoryStream(Convert.FromBase64String(serializedDomainEvent))) { + return (DomainEvent)formatter.Deserialize(stream); + } + } + } + +} \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/SimpleCqrs.EventStore.MySql.csproj b/src/EventStores/SimpleCqrs.EventStore.MySql/SimpleCqrs.EventStore.MySql.csproj new file mode 100644 index 0000000..0c1ea0d --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/SimpleCqrs.EventStore.MySql.csproj @@ -0,0 +1,75 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {D55AC0E2-94D7-4805-A108-C51A59F9E2D1} + Library + Properties + SimpleCqrs.EventStore.MySql + SimpleCqrs.EventStore.MySql + v4.0 + 512 + + + true + full + false + ..\..\..\binaries\eventstores\mysql\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\MySql.Data.6.5.4\lib\net40\MySql.Data.dll + + + ..\..\..\lib\ServiceStack.Text.dll + + + + + + + + + + + + + + + + + + + + + + {E04C12B5-A6D3-4D64-9F38-896BEE68162E} + SimpleCqrs + + + + + + + + \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/SqlMapper.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/SqlMapper.cs new file mode 100644 index 0000000..6e1617e --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/SqlMapper.cs @@ -0,0 +1,2245 @@ +/* + License: http://www.apache.org/licenses/LICENSE-2.0 + Home page: http://code.google.com/p/dapper-dot-net/ + + Note: to build on C# 3.0 + .NET 3.5, include the CSHARP30 compiler symbol (and yes, + I know the difference between language and runtime versions; this is a compromise). + */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using System.Threading; +using System.Text.RegularExpressions; + +namespace Dapper +{ + /// + /// Dapper, a light weight object mapper for ADO.NET + /// + public static 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); + } + static Link> bindByNameCache; + static Action GetBindByName(Type commandType) + { + if (commandType == null) return null; // GIGO + Action action; + if (Link>.TryGet(bindByNameCache, commandType, out action)) + { + return action; + } + var prop = commandType.GetProperty("BindByName", BindingFlags.Public | BindingFlags.Instance); + action = null; + ParameterInfo[] indexers; + MethodInfo setter; + if (prop != null && prop.CanWrite && prop.PropertyType == typeof(bool) + && ((indexers = prop.GetIndexParameters()) == null || indexers.Length == 0) + && (setter = prop.GetSetMethod()) != null + ) + { + var method = new DynamicMethod(commandType.Name + "_BindByName", null, new Type[] { typeof(IDbCommand), typeof(bool) }); + var il = method.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Castclass, commandType); + il.Emit(OpCodes.Ldarg_1); + il.EmitCall(OpCodes.Callvirt, setter, null); + il.Emit(OpCodes.Ret); + action = (Action)method.CreateDelegate(typeof(Action)); + } + // cache it + Link>.TryAdd(ref bindByNameCache, commandType, ref action); + return action; + } + /// + /// 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. + /// + 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; private set; } + public TValue Value { get; private set; } + public Link Tail { get; private set; } + } + class CacheInfo + { + public Func 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); } + } + + /// + /// Called if the query cache is purged via PurgeQueryCache + /// + public static event EventHandler QueryCachePurged; + private static void OnQueryCachePurged() + { + var handler = QueryCachePurged; + if (handler != null) handler(null, EventArgs.Empty); + } +#if CSHARP30 + private static readonly Dictionary _queryCache = new Dictionary(); + // note: conflicts between readers and writers are so short-lived that it isn't worth the overhead of + // ReaderWriterLockSlim etc; a simple lock is faster + private static void SetQueryCache(Identity key, CacheInfo value) + { + lock (_queryCache) { _queryCache[key] = value; } + } + private static bool TryGetQueryCache(Identity key, out CacheInfo value) + { + lock (_queryCache) { return _queryCache.TryGetValue(key, out value); } + } + public static void PurgeQueryCache() + { + lock (_queryCache) + { + _queryCache.Clear(); + } + OnQueryCachePurged(); + } +#else + 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(); + OnQueryCachePurged(); + } + + /// + /// 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); + + } +#endif + + + static readonly Dictionary typeMap; + + static SqlMapper() + { + typeMap = new Dictionary(); + typeMap[typeof(byte)] = DbType.Byte; + typeMap[typeof(sbyte)] = DbType.SByte; + typeMap[typeof(short)] = DbType.Int16; + typeMap[typeof(ushort)] = DbType.UInt16; + typeMap[typeof(int)] = DbType.Int32; + typeMap[typeof(uint)] = DbType.UInt32; + typeMap[typeof(long)] = DbType.Int64; + typeMap[typeof(ulong)] = DbType.UInt64; + typeMap[typeof(float)] = DbType.Single; + typeMap[typeof(double)] = DbType.Double; + typeMap[typeof(decimal)] = DbType.Decimal; + typeMap[typeof(bool)] = DbType.Boolean; + typeMap[typeof(string)] = DbType.String; + typeMap[typeof(char)] = DbType.StringFixedLength; + typeMap[typeof(Guid)] = DbType.Guid; + typeMap[typeof(DateTime)] = DbType.DateTime; + typeMap[typeof(DateTimeOffset)] = DbType.DateTimeOffset; + typeMap[typeof(byte[])] = DbType.Binary; + typeMap[typeof(byte?)] = DbType.Byte; + typeMap[typeof(sbyte?)] = DbType.SByte; + typeMap[typeof(short?)] = DbType.Int16; + typeMap[typeof(ushort?)] = DbType.UInt16; + typeMap[typeof(int?)] = DbType.Int32; + typeMap[typeof(uint?)] = DbType.UInt32; + typeMap[typeof(long?)] = DbType.Int64; + typeMap[typeof(ulong?)] = DbType.UInt64; + typeMap[typeof(float?)] = DbType.Single; + typeMap[typeof(double?)] = DbType.Double; + typeMap[typeof(decimal?)] = DbType.Decimal; + typeMap[typeof(bool?)] = DbType.Boolean; + typeMap[typeof(char?)] = DbType.StringFixedLength; + typeMap[typeof(Guid?)] = DbType.Guid; + typeMap[typeof(DateTime?)] = DbType.DateTime; + typeMap[typeof(DateTimeOffset?)] = DbType.DateTimeOffset; + } + + private const string LinqBinary = "System.Data.Linq.Binary"; + private static DbType LookupDbType(Type type, string name) + { + DbType dbType; + var nullUnderlyingType = Nullable.GetUnderlyingType(type); + if (nullUnderlyingType != null) type = nullUnderlyingType; + if (type.IsEnum) + { + type = Enum.GetUnderlyingType(type); + } + if (typeMap.TryGetValue(type, out dbType)) + { + return dbType; + } + if (type.FullName == LinqBinary) + { + return DbType.Binary; + } + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + // use xml to denote its a list, hacky but will work on any DB + return DbType.Xml; + } + + + throw new NotSupportedException(string.Format("The member {0} of type {1} cannot be used as a parameter value", name, type)); + } + + /// + /// Identity of a cached query in Dapper, used for extensability + /// + 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 == null ? 0 : sql.GetHashCode()); + hashCode = hashCode * 23 + (type == null ? 0 : type.GetHashCode()); + if (otherTypes != null) + { + foreach (var t in otherTypes) + { + hashCode = hashCode * 23 + (t == null ? 0 : t.GetHashCode()); + } + } + hashCode = hashCode * 23 + (connectionString == null ? 0 : connectionString.GetHashCode()); + hashCode = hashCode * 23 + (parametersType == null ? 0 : parametersType.GetHashCode()); + } + } + + /// + /// + /// + /// + /// + 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; + private 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 && + connectionString == other.connectionString && + parametersType == other.parametersType; + } + } + +#if CSHARP30 + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute(this IDbConnection cnn, string sql, object param) + { + return Execute(cnn, sql, param, null, null, null); + } + /// + /// 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 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, string sql, object param) + { + return Query(cnn, sql, param, null, true, null, null); + } + +#endif + /// + /// Execute parameterized SQL + /// + /// Number of rows affected + public static int Execute( +#if CSHARP30 + this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType +#else +this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null +#endif +) + { + IEnumerable multiExec = (object)param as IEnumerable; + Identity identity; + CacheInfo info = null; + if (multiExec != null && !(multiExec is string)) + { + bool isFirst = true; + int total = 0; + using (var cmd = SetupCommand(cnn, transaction, sql, null, null, commandTimeout, commandType)) + { + + string masterSql = null; + foreach (var obj in multiExec) + { + if (isFirst) + { + masterSql = cmd.CommandText; + isFirst = false; + identity = new Identity(sql, cmd.CommandType, cnn, null, obj.GetType(), null); + info = GetCacheInfo(identity); + } + 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(); + } + } + return total; + } + + // nice and simple + if ((object)param != null) + { + identity = new Identity(sql, commandType, cnn, null, (object) param == null ? null : ((object) param).GetType(), null); + info = GetCacheInfo(identity); + } + return ExecuteCommand(cnn, transaction, sql, (object)param == null ? null : info.ParamReader, (object)param, commandTimeout, commandType); + } +#if !CSHARP30 + /// + /// Return a list of dynamic objects, reader is closed after the call + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, dynamic 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); + } +#endif + + /// + /// 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( +#if CSHARP30 + this IDbConnection cnn, string sql, object param, IDbTransaction transaction, bool buffered, int? commandTimeout, CommandType? commandType +#else +this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null +#endif +) + { + var data = QueryInternal(cnn, sql, param as object, transaction, commandTimeout, commandType); + return buffered ? data.ToList() : data; + } + + /// + /// Execute a command that returns multiple result sets, and access each in turn + /// + public static GridReader QueryMultiple( +#if CSHARP30 + this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType +#else +this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null +#endif +) + { + Identity identity = new Identity(sql, commandType, cnn, typeof(GridReader), (object)param == null ? null : ((object)param).GetType(), null); + CacheInfo info = GetCacheInfo(identity); + + IDbCommand cmd = null; + IDataReader reader = null; + try + { + cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType); + reader = cmd.ExecuteReader(); + return new GridReader(cmd, reader, identity); + } + catch + { + if (reader != null) reader.Dispose(); + if (cmd != null) cmd.Dispose(); + throw; + } + } + + /// + /// Return a typed list of objects, reader is closed after the call + /// + private static IEnumerable QueryInternal(this IDbConnection cnn, string sql, object param, IDbTransaction transaction, int? commandTimeout, CommandType? commandType) + { + var identity = new Identity(sql, commandType, cnn, typeof(T), param == null ? null : param.GetType(), null); + var info = GetCacheInfo(identity); + + using (var cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType)) + { + using (var reader = cmd.ExecuteReader()) + { + Func> cacheDeserializer = () => + { + info.Deserializer = GetDeserializer(typeof(T), reader, 0, -1, false); + SetQueryCache(identity, info); + return info.Deserializer; + }; + + if (info.Deserializer == null) + { + cacheDeserializer(); + } + + var deserializer = info.Deserializer; + + while (reader.Read()) + { + object next; + try + { + next = deserializer(reader); + } + catch (DataException) + { + // give it another shot, in case the underlying schema changed + deserializer = cacheDeserializer(); + next = deserializer(reader); + } + yield return (T)next; + } + + } + } + } + + /// + /// 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 IEnumerable Query( +#if CSHARP30 + this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType +#else +this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null +#endif +) + { + return MultiMap(cnn, sql, map, param as object, 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( +#if CSHARP30 + this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType +#else +this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null +#endif +) + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } + + /// + /// Perform a multi mapping query with 4 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable Query( +#if CSHARP30 + this IDbConnection cnn, string sql, Func map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType +#else +this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null +#endif +) + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } +#if !CSHARP30 + /// + /// Perform a multi mapping query with 5 input parameters + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IEnumerable Query(this IDbConnection cnn, string sql, Func map, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, string splitOn = "Id", int? commandTimeout = null, CommandType? commandType = null) + { + return MultiMap(cnn, sql, map, param as object, transaction, buffered, splitOn, commandTimeout, commandType); + } +#endif + class DontMap { } + static IEnumerable MultiMap( + this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, bool buffered, string splitOn, int? commandTimeout, CommandType? commandType) + { + var results = MultiMapImpl(cnn, sql, map, param, transaction, splitOn, commandTimeout, commandType, null, null); + return buffered ? results.ToList() : results; + } + + + static IEnumerable MultiMapImpl(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity) + { + identity = identity ?? new Identity(sql, commandType, cnn, typeof(TFirst), (object)param == null ? null : ((object)param).GetType(), new[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth) }); + CacheInfo cinfo = GetCacheInfo(identity); + + IDbCommand ownedCommand = null; + IDataReader ownedReader = null; + + try + { + if (reader == null) + { + ownedCommand = SetupCommand(cnn, transaction, sql, cinfo.ParamReader, (object)param, commandTimeout, commandType); + ownedReader = ownedCommand.ExecuteReader(); + reader = ownedReader; + } + Func deserializer = null; + Func[] otherDeserializers = null; + + Action cacheDeserializers = () => + { + var deserializers = GenerateDeserializers(new Type[] { typeof(TFirst), typeof(TSecond), typeof(TThird), typeof(TFourth), typeof(TFifth)}, splitOn, reader); + deserializer = cinfo.Deserializer = deserializers[0]; + otherDeserializers = cinfo.OtherDeserializers = deserializers.Skip(1).ToArray(); + SetQueryCache(identity, cinfo); + }; + + if ((deserializer = cinfo.Deserializer) == null || (otherDeserializers = cinfo.OtherDeserializers) == null) + { + cacheDeserializers(); + } + + Func mapIt = GenerateMapper(deserializer, otherDeserializers, map); + + if (mapIt != null) + { + while (reader.Read()) + { + TReturn next; + try + { + next = mapIt(reader); + } + catch (DataException) + { + cacheDeserializers(); + mapIt = GenerateMapper(deserializer, otherDeserializers, map); + next = mapIt(reader); + } + yield return next; + } + } + } + finally + { + try + { + if (ownedReader != null) + { + ownedReader.Dispose(); + } + } + finally + { + if (ownedCommand != null) + { + ownedCommand.Dispose(); + } + } + } + } + + 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)); +#if !CSHARP30 + 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)); +#endif + default: + throw new NotSupportedException(); + } + } + + private static Func[] GenerateDeserializers(Type[] types, string splitOn, IDataReader reader) + { + int current = 0; + var splits = splitOn.Split(',').ToArray(); + var splitIndex = 0; + + Func nextSplit = type => + { + var currentSplit = splits[splitIndex]; + if (splits.Length > splitIndex + 1) + { + splitIndex++; + } + + bool skipFirst = false; + int startingPos = current + 1; + // if our current type has the split, skip the first time you see it. + if (type != typeof(Object)) + { + var props = GetSettableProps(type); + var fields = GetSettableFields(type); + + foreach (var name in props.Select(p => p.Name).Concat(fields.Select(f => f.Name))) + { + if (string.Equals(name, currentSplit, StringComparison.OrdinalIgnoreCase)) + { + skipFirst = true; + startingPos = current; + break; + } + } + + } + + int pos; + for (pos = startingPos; pos < reader.FieldCount; pos++) + { + // some people like ID some id ... assuming case insensitive splits for now + if (splitOn == "*") + { + break; + } + if (string.Equals(reader.GetName(pos), currentSplit, StringComparison.OrdinalIgnoreCase)) + { + if (skipFirst) + { + skipFirst = false; + } + else + { + break; + } + } + } + current = pos; + return pos; + }; + + var deserializers = new List>(); + int split = 0; + bool first = true; + foreach (var type in types) + { + if (type != typeof(DontMap)) + { + int next = nextSplit(type); + deserializers.Add(GetDeserializer(type, reader, split, next - split, /* returnNullIfFirstMissing: */ !first)); + first = false; + split = next; + } + } + + return deserializers.ToArray(); + } + + private static CacheInfo GetCacheInfo(Identity identity) + { + CacheInfo info; + if (!TryGetQueryCache(identity, out info)) + { + info = new CacheInfo(); + if (identity.parametersType != null) + { + if (typeof(IDynamicParameters).IsAssignableFrom(identity.parametersType)) + { + info.ParamReader = (cmd, obj) => { (obj as IDynamicParameters).AddParameters(cmd,identity); }; + } + else + { + info.ParamReader = CreateParamInfoGenerator(identity); + } + } + SetQueryCache(identity, info); + } + return info; + } + + private static Func GetDeserializer(Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing) + { +#if !CSHARP30 + // dynamic is passed in as Object ... by c# design + if (type == typeof(object) + || type == typeof(FastExpando)) + { + return GetDynamicDeserializer(reader, startBound, length, returnNullIfFirstMissing); + } +#endif + + if (!(typeMap.ContainsKey(type) || type.FullName == LinqBinary)) + { + return GetTypeDeserializer(type, reader, startBound, length, returnNullIfFirstMissing); + } + return GetStructDeserializer(type, startBound); + + } +#if !CSHARP30 + private class FastExpando : System.Dynamic.DynamicObject, IDictionary + { + IDictionary data; + + public static FastExpando Attach(IDictionary data) + { + return new FastExpando { data = data }; + } + + public override bool TrySetMember(System.Dynamic.SetMemberBinder binder, object value) + { + data[binder.Name] = value; + return true; + } + + public override bool TryGetMember(System.Dynamic.GetMemberBinder binder, out object result) + { + return data.TryGetValue(binder.Name, out result); + } + + public override IEnumerable GetDynamicMemberNames() + { + return data.Keys; + } + + #region IDictionary Members + + void IDictionary.Add(string key, object value) + { + throw new NotImplementedException(); + } + + bool IDictionary.ContainsKey(string key) + { + return data.ContainsKey(key); + } + + ICollection IDictionary.Keys + { + get { return data.Keys; } + } + + bool IDictionary.Remove(string key) + { + throw new NotImplementedException(); + } + + bool IDictionary.TryGetValue(string key, out object value) + { + return data.TryGetValue(key, out value); + } + + ICollection IDictionary.Values + { + get { return data.Values; } + } + + object IDictionary.this[string key] + { + get + { + return data[key]; + } + set + { + if (!data.ContainsKey(key)) + { + throw new NotImplementedException(); + } + data[key] = value; + } + } + + #endregion + + #region ICollection> Members + + void ICollection>.Add(KeyValuePair item) + { + throw new NotImplementedException(); + } + + void ICollection>.Clear() + { + throw new NotImplementedException(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return data.Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + data.CopyTo(array, arrayIndex); + } + + int ICollection>.Count + { + get { return data.Count; } + } + + bool ICollection>.IsReadOnly + { + get { return true; } + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + #endregion + + #region IEnumerable> Members + + IEnumerator> IEnumerable>.GetEnumerator() + { + return data.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return data.GetEnumerator(); + } + + #endregion + } + + + private static Func GetDynamicDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) + { + var fieldCount = reader.FieldCount; + if (length == -1) + { + length = fieldCount - startBound; + } + + if (fieldCount <= startBound) + { + throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); + } + + return + r => + { + IDictionary row = new Dictionary(length); + for (var i = startBound; i < startBound + length; i++) + { + var tmp = r.GetValue(i); + tmp = tmp == DBNull.Value ? null : tmp; + row[r.GetName(i)] = tmp; + if (returnNullIfFirstMissing && i == startBound && tmp == null) + { + return null; + } + } + //we know this is an object so it will not box + return FastExpando.Attach(row); + }; + } +#endif + /// + /// Internal use only + /// + /// + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", false)] + public static char ReadChar(object value) + { + if (value == null || value is DBNull) throw new ArgumentNullException("value"); + string s = value as string; + if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); + return s[0]; + } + + /// + /// Internal use only + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", 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", "value"); + return s[0]; + } + /// + /// Internal use only + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is for internal usage only", true)] + 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 + + var list = value as IEnumerable; + var count = 0; + + if (list != null) + { + if (FeatureSupport.Get(command.Connection).Arrays) + { + var arrayParm = command.CreateParameter(); + arrayParm.Value = list; + arrayParm.ParameterName = namePrefix; + command.Parameters.Add(arrayParm); + } + else + { + bool isString = value is IEnumerable; + bool isDbString = value is IEnumerable; + foreach (var item in list) + { + count++; + var listParam = command.CreateParameter(); + listParam.ParameterName = namePrefix + count; + listParam.Value = item ?? DBNull.Value; + if (isString) + { + listParam.Size = 4000; + if (item != null && ((string) item).Length > 4000) + { + listParam.Size = -1; + } + } + if (isDbString && item as DbString != null) + { + var str = item as DbString; + str.AddParameter(command, listParam.ParameterName); + } + else + { + command.Parameters.Add(listParam); + } + } + + if (count == 0) + { + command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), "(SELECT NULL WHERE 1 = 0)"); + } + else + { + command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), match => + { + var grp = match.Value; + var sb = new StringBuilder("(").Append(grp).Append(1); + for (int i = 2; i <= count; i++) + { + sb.Append(',').Append(grp).Append(i); + } + return sb.Append(')').ToString(); + }); + } + } + } + + } + + private static IEnumerable FilterParameters(IEnumerable parameters, string sql) + { + return parameters.Where(p => Regex.IsMatch(sql, "[@:]" + p.Name + "([^a-zA-Z0-9_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline)); + } + + /// + /// Internal use only + /// + public static Action CreateParamInfoGenerator(Identity identity) + { + Type type = identity.parametersType; + bool filterParams = identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text; + var dm = new DynamicMethod(string.Format("ParamInfo{0}", Guid.NewGuid()), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); + + var il = dm.GetILGenerator(); + + il.DeclareLocal(type); // 0 + bool haveInt32Arg1 = false; + il.Emit(OpCodes.Ldarg_1); // stack is now [untyped-param] + il.Emit(OpCodes.Unbox_Any, 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("Parameters").GetGetMethod(), null); // stack is now [parameters] + + IEnumerable props = type.GetProperties().OrderBy(p => p.Name); + if (filterParams) + { + props = FilterParameters(props, identity.sql); + } + foreach (var prop in props) + { + if (filterParams) + { + if (identity.sql.IndexOf("@" + prop.Name, StringComparison.InvariantCultureIgnoreCase) < 0 + && identity.sql.IndexOf(":" + prop.Name, StringComparison.InvariantCultureIgnoreCase) < 0) + { // can't see the parameter in the text (even in a comment, etc) - burn it with fire + continue; + } + } + if (prop.PropertyType == typeof(DbString)) + { + il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [typed-param] + il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [dbstring] + il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [dbstring] [command] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [dbstring] [command] [name] + il.EmitCall(OpCodes.Callvirt, typeof(DbString).GetMethod("AddParameter"), null); // stack is now [parameters] + continue; + } + DbType dbType = LookupDbType(prop.PropertyType, prop.Name); + if (dbType == DbType.Xml) + { + // 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(OpCodes.Callvirt, 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("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] + il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetMethod("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("ParameterName").GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter] + + il.Emit(OpCodes.Dup);// stack is now [parameters] [parameters] [parameter] [parameter] + EmitInt32(il, (int)dbType);// stack is now [parameters] [parameters] [parameter] [parameter] [db-type] + + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("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("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(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [parameters] [parameter] [parameter] [typed-value] + bool checkForNull = true; + if (prop.PropertyType.IsValueType) + { + il.Emit(OpCodes.Box, prop.PropertyType); // stack is [parameters] [parameters] [parameter] [parameter] [boxed-value] + if (Nullable.GetUnderlyingType(prop.PropertyType) == null) + { // struct but not Nullable; boxed value cannot be null + checkForNull = false; + } + } + if (checkForNull) + { + if (dbType == DbType.String && !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 ? 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("Value")); // relative stack [DBNull] + if (dbType == DbType.String) + { + 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("Length").GetGetMethod(), null); // [string] [length] + EmitInt32(il, 4000); // [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, 4000); // [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] + } + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("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("Size").GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter] + + il.MarkLabel(endOfSize); + } + + il.EmitCall(OpCodes.Callvirt, typeof(IList).GetMethod("Add"), null); // stack is now [parameters] + il.Emit(OpCodes.Pop); // IList.Add returns the new index (int); we don't care + } + // stack is currently [command] + il.Emit(OpCodes.Pop); // stack is now empty + il.Emit(OpCodes.Ret); + return (Action)dm.CreateDelegate(typeof(Action)); + } + + private static IDbCommand SetupCommand(IDbConnection cnn, IDbTransaction transaction, string sql, Action paramReader, object obj, int? commandTimeout, CommandType? commandType) + { + var cmd = cnn.CreateCommand(); + var bindByName = GetBindByName(cmd.GetType()); + if (bindByName != null) bindByName(cmd, true); + if (transaction != null) + cmd.Transaction = transaction; + cmd.CommandText = sql; + if (commandTimeout.HasValue) + cmd.CommandTimeout = commandTimeout.Value; + if (commandType.HasValue) + cmd.CommandType = commandType.Value; + if (paramReader != null) + { + paramReader(cmd, obj); + } + return cmd; + } + + + private static int ExecuteCommand(IDbConnection cnn, IDbTransaction transaction, string sql, Action paramReader, object obj, int? commandTimeout, CommandType? commandType) + { + using (var cmd = SetupCommand(cnn, transaction, sql, paramReader, obj, commandTimeout, commandType)) + { + return cmd.ExecuteNonQuery(); + } + } + + private static Func GetStructDeserializer(Type type, 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 => SqlMapper.ReadChar(r.GetValue(index)); + } + if (type == typeof(char?)) + { + return r => SqlMapper.ReadNullableChar(r.GetValue(index)); + } + if (type.FullName == LinqBinary) + { + return r => Activator.CreateInstance(type, r.GetValue(index)); + } +#pragma warning restore 618 + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null : val; + }; + } + + static readonly MethodInfo + enumParse = typeof(Enum).GetMethod("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(); + + class PropInfo + { + public string Name { get; set; } + public MethodInfo Setter { get; set; } + public Type Type { get; set; } + } + + static List GetSettableProps(Type t) + { + return t + .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(p => new PropInfo + { + Name = p.Name, + Setter = p.DeclaringType == t ? + p.GetSetMethod(true) : + p.DeclaringType.GetProperty(p.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).GetSetMethod(true), + Type = p.PropertyType + }) + .Where(info => info.Setter != null) + .ToList(); + } + + static List GetSettableFields(Type t) + { + return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); + } + + /// + /// Internal use only + /// + /// + /// + /// + /// + /// + /// + public static Func GetTypeDeserializer( +#if CSHARP30 + Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing +#else + Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false +#endif +) + { + var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true); + + var il = dm.GetILGenerator(); + il.DeclareLocal(typeof(int)); + il.DeclareLocal(type); + bool haveEnumLocal = false; + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Stloc_0); + var properties = GetSettableProps(type); + var fields = GetSettableFields(type); + if (length == -1) + { + length = reader.FieldCount - startBound; + } + + if (reader.FieldCount <= startBound) + { + throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn"); + } + + var names = new List(); + + for (int i = startBound; i < startBound + length; i++) + { + names.Add(reader.GetName(i)); + } + + var setters = ( + from n in names + let prop = properties.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.Ordinal)) // property case sensitive first + ?? properties.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase)) // property case insensitive second + let field = prop != null ? null : (fields.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.Ordinal)) // field case sensitive third + ?? fields.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase))) // field case insensitive fourth + select new { Name = n, Property = prop, Field = field } + ).ToList(); + + int index = startBound; + + if (type.IsValueType) + { + il.Emit(OpCodes.Ldloca_S, (byte)1); + il.Emit(OpCodes.Initobj, type); + } + else + { + var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); + if (ctor == null) + { + throw new InvalidOperationException("A parameterless default constructor is required to allow for dapper materialization"); + } + il.Emit(OpCodes.Newobj, ctor); + il.Emit(OpCodes.Stloc_1); + } + il.BeginExceptionBlock(); + if(type.IsValueType) + { + il.Emit(OpCodes.Ldloca_S, (byte)1);// [target] + } else + { + il.Emit(OpCodes.Ldloc_1);// [target] + } + + // stack is now [target] + + bool first = true; + var allDone = il.DefineLabel(); + foreach (var item in setters) + { + if (item.Property != null || item.Field != 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] + + Type memberType = item.Property != null ? item.Property.Type : item.Field.FieldType; + + if (memberType == typeof(char) || memberType == typeof(char?)) + { + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod( + memberType == typeof(char) ? "ReadChar" : "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) + { + if (!haveEnumLocal) + { + il.DeclareLocal(typeof(string)); + haveEnumLocal = true; + } + + Label isNotString = il.DefineLabel(); + il.Emit(OpCodes.Dup); // stack is now [target][target][value][value] + il.Emit(OpCodes.Isinst, typeof(string)); // stack is now [target][target][value-as-object][string or null] + il.Emit(OpCodes.Dup);// stack is now [target][target][value-as-object][string or null][string or null] + il.Emit(OpCodes.Stloc_2); // stack is now [target][target][value-as-object][string or null] + il.Emit(OpCodes.Brfalse_S, isNotString); // stack is now [target][target][value-as-object] + + il.Emit(OpCodes.Pop); // 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("GetTypeFromHandle"), null);// stack is now [target][target][enum-type] + il.Emit(OpCodes.Ldloc_2); // 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.MarkLabel(isNotString); + + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + + if (nullUnderlyingType != null) + { + il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][enum-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 + { + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + } + } + + // Store the value in the property/field + if (item.Property != null) + { + if (type.IsValueType) + { + il.Emit(OpCodes.Call, item.Property.Setter); // stack is now [target] + } + else + { + il.Emit(OpCodes.Callvirt, item.Property.Setter); // stack is now [target] + } + } + 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] + + 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 + { + il.Emit(OpCodes.Stloc_1); // stack is empty + } + 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 + il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("ThrowDataException"), null); + il.EndExceptionBlock(); + + il.Emit(OpCodes.Ldloc_1); // stack is [rval] + if(type.IsValueType) + { + il.Emit(OpCodes.Box, type); + } + il.Emit(OpCodes.Ret); + + return (Func)dm.CreateDelegate(typeof(Func)); + } + + /// + /// Throws a data exception, only used internally + /// + /// + /// + /// + public static void ThrowDataException(Exception ex, int index, IDataReader reader) + { + string name = "(n/a)", value = "(n/a)"; + if (reader != null && index >= 0 && index < reader.FieldCount) + { + name = reader.GetName(index); + object val = reader.GetValue(index); + if (val == null || val is DBNull) + { + value = ""; + } + else + { + value = Convert.ToString(val) + " - " + Type.GetTypeCode(val.GetType()); + } + } + throw new DataException(string.Format("Error parsing column {0} ({1}={2})", index, name, value), ex); + } + 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; + } + } + + /// + /// The grid reader provides interfaces for reading multiple result sets from a Dapper query + /// + public class GridReader : IDisposable + { + private IDataReader reader; + private IDbCommand command; + private Identity identity; + + internal GridReader(IDbCommand command, IDataReader reader, Identity identity) + { + this.command = command; + this.reader = reader; + this.identity = identity; + } + +#if !CSHARP30 + + /// + /// Read the next grid of results, returned as a dynamic object + /// + public IEnumerable Read() + { + return Read(); + } + +#endif + + /// + /// Read the next grid of results + /// + public IEnumerable Read() + { + if (reader == null) throw new ObjectDisposedException(GetType().Name); + if (consumed) throw new InvalidOperationException("Each grid can only be iterated once"); + var typedIdentity = identity.ForGrid(typeof(T), gridIndex); + CacheInfo cache = GetCacheInfo(typedIdentity); + var deserializer = cache.Deserializer; + + Func> deserializerGenerator = () => + { + deserializer = GetDeserializer(typeof(T), reader, 0, -1, false); + cache.Deserializer = deserializer; + return deserializer; + }; + + if (deserializer == null) + { + deserializer = deserializerGenerator(); + } + consumed = true; + return ReadDeferred(gridIndex, deserializer, typedIdentity, deserializerGenerator); + } + + private IEnumerable MultiReadInternal(object func, string splitOn) + { + + var identity = this.identity.ForGrid(typeof(TReturn), new Type[] { + typeof(TFirst), + typeof(TSecond), + typeof(TThird), + typeof(TFourth), + typeof(TFifth) + }, gridIndex); + try + { + foreach (var r in SqlMapper.MultiMapImpl(null, null, func, null, null, splitOn, null, null, reader, identity)) + { + yield return r; + } + } + finally + { + NextResult(); + } + } + + /// + /// Read multiple objects from a single recordset on the grid + /// + /// + /// + /// + /// + /// + /// +#if CSHARP30 + public IEnumerable Read(Func func, string splitOn) +#else + public IEnumerable Read(Func func, string splitOn = "id") +#endif + { + return MultiReadInternal(func, splitOn); + } + + /// + /// Read multiple objects from a single recordset on the grid + /// + /// + /// + /// + /// + /// + /// + /// +#if CSHARP30 + public IEnumerable Read(Func func, string splitOn) +#else + public IEnumerable Read(Func func, string splitOn = "id") +#endif + { + return MultiReadInternal(func, splitOn); + } + + /// + /// Read multiple objects from a single record set on the grid + /// + /// + /// + /// + /// + /// + /// + /// + /// +#if CSHARP30 + public IEnumerable Read(Func func, string splitOn) +#else + public IEnumerable Read(Func func, string splitOn = "id") +#endif + { + return MultiReadInternal(func, splitOn); + } + +#if !CSHARP30 + /// + /// Read multiple objects from a single record set on the grid + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable Read(Func func, string splitOn = "id") + + { + return MultiReadInternal(func, splitOn); + } +#endif + + private IEnumerable ReadDeferred(int index, Func deserializer, Identity typedIdentity, Func> deserializerGenerator) + { + try + { + while (index == gridIndex && reader.Read()) + { + object next; + try + { + next = deserializer(reader); + } + catch (DataException) + { + deserializer = deserializerGenerator(); + next = deserializer(reader); + } + yield return (T)next; + } + } + finally // finally so that First etc progresses things even when multiple rows + { + if (index == gridIndex) + { + NextResult(); + } + } + } + private int gridIndex; + private bool consumed; + private void NextResult() + { + if (reader.NextResult()) + { + gridIndex++; + consumed = false; + } + else + { + Dispose(); + } + + } + /// + /// Dispose the grid, closing and disposing both the underlying reader and command. + /// + public void Dispose() + { + if (reader != null) + { + reader.Dispose(); + reader = null; + } + if (command != null) + { + command.Dispose(); + command = null; + } + } + } + } + + /// + /// A bag of parameters that can be passed to the Dapper Query and Execute methods + /// + public class DynamicParameters : SqlMapper.IDynamicParameters + { + static Dictionary> paramReaderCache = new Dictionary>(); + + Dictionary parameters = new Dictionary(); + List templates; + + 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; } + } + + /// + /// construct a dynamic parameter bag + /// + public DynamicParameters() { } + + /// + /// construct a dynamic parameter bag + /// + /// can be an anonymous type or a DynamicParameters bag + public DynamicParameters(object template) + { + 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( +#if CSHARP30 + object param +#else + dynamic param +#endif +) + { + var obj = param as object; + 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) + { +#if CSHARP30 + Add(kvp.Key, kvp.Value, null, null, null); +#else + Add(kvp.Key, kvp.Value); +#endif + } + } + } + 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( +#if CSHARP30 + string name, object value, DbType? dbType, ParameterDirection? direction, int? size +#else +string name, object value = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null +#endif +) + { + parameters[Clean(name)] = new ParamInfo() { Name = name, Value = value, ParameterDirection = direction ?? ParameterDirection.Input, DbType = dbType, Size = size }; + } + + 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) + { + 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); + paramReaderCache[newIdent] = appender; + } + } + + appender(command, template); + } + } + + foreach (var param in parameters.Values) + { + string name = Clean(param.Name); + bool add = !command.Parameters.Contains(name); + IDbDataParameter p; + if(add) + { + p = command.CreateParameter(); + p.ParameterName = name; + } else + { + p = (IDbDataParameter)command.Parameters[name]; + } + var val = param.Value; + p.Value = val ?? DBNull.Value; + p.Direction = param.ParameterDirection; + var s = val as string; + if (s != null) + { + if (s.Length <= 4000) + { + p.Size = 4000; + } + } + if (param.Size != null) + { + p.Size = param.Size.Value; + } + if (param.DbType != null) + { + p.DbType = param.DbType.Value; + } + if (add) + { + command.Parameters.Add(p); + } + param.AttachedParam = p; + } + } + + /// + /// All the names of the param in the bag, use Get to yank them out + /// + public IEnumerable ParameterNames + { + get + { + return 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 val = parameters[Clean(name)].AttachedParam.Value; + if (val == DBNull.Value) + { + if (default(T) != null) + { + throw new ApplicationException("Attempting to cast a DBNull to a non nullable type!"); + } + return default(T); + } + return (T)val; + } + } + + /// + /// 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 + { + /// + /// Create a new DbString + /// + public DbString() { Length = -1; } + /// + /// 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; + param.Value = (object)Value ?? DBNull.Value; + if (Length == -1 && Value != null && Value.Length <= 4000) + { + param.Size = 4000; + } + else + { + param.Size = Length; + } + param.DbType = IsAnsi ? (IsFixedLength ? DbType.AnsiStringFixedLength : DbType.AnsiString) : (IsFixedLength ? DbType.StringFixedLength : DbType.String); + command.Parameters.Add(param); + } + } + + /// + /// Handles variances in features per DBMS + /// + public class FeatureSupport + { + /// + /// Dictionary of supported features index by connection type name + /// + private static readonly Dictionary FeatureList = new Dictionary() { + {"sqlserverconnection", new FeatureSupport { Arrays = false}}, + {"npgsqlconnection", new FeatureSupport {Arrays = true}} + }; + + /// + /// Gets the featureset based on the passed connection + /// + public static FeatureSupport Get(IDbConnection connection) + { + string name = connection.GetType().Name.ToLower(); + FeatureSupport features; + return FeatureList.TryGetValue(name, out features) ? features : FeatureList.Values.First(); + } + + /// + /// True if the db supports array columns e.g. Postgresql + /// + public bool Arrays { get; set; } + } + +} diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/SqlServerEventStore.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/SqlServerEventStore.cs new file mode 100644 index 0000000..32eb495 --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/SqlServerEventStore.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using System.Text; +using ServiceStack.Text; +using SimpleCqrs.Eventing; + +namespace SimpleCqrs.EventStore.MySql +{ + public class SqlServerEventStore : IEventStore + { + private readonly IDomainEventSerializer serializer; + private readonly MySqlServerConfiguration configuration; + + public SqlServerEventStore(MySqlServerConfiguration configuration, IDomainEventSerializer serializer) + { + this.serializer = serializer; + this.configuration = configuration; + Init(); + } + + public void Init() + { + using (var connection = new SqlConnection(configuration.ConnectionString)) + { + connection.Open(); + var sql = string.Format(MySqlStatements.CreateTheEventStoreTable, "EventStore"); + using (var command = new SqlCommand(sql, connection)) + command.ExecuteNonQuery(); + connection.Close(); + } + } + + public IEnumerable GetEvents(Guid aggregateRootId, int startSequence) + { + var events = new List(); + using (var connection = new SqlConnection(configuration.ConnectionString)) + { + connection.Open(); + var sql = string.Format(MySqlStatements.GetEventsByAggregateRootAndSequence, "", "EventStore", aggregateRootId, + startSequence); + using (var command = new SqlCommand(sql, connection)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) + { + var type = reader["EventType"].ToString(); + var data = reader["data"].ToString(); + + try + { + events.Add(serializer.Deserialize(Type.GetType(type), data)); + } catch(ArgumentNullException ex) + { + throw new Exception(string.Format("Cannot find type '{0}', yet the type is in the event store. Are you sure you haven't changed a class name or something arising from mental dullness?", type.Split(',')[0]), ex.InnerException); + } + } + connection.Close(); + } + return events; + } + + public void Insert(IEnumerable domainEvents) + { + var sql = new StringBuilder(); + foreach (var domainEvent in domainEvents) + sql.AppendFormat(MySqlStatements.InsertEvents, "EventStore", TypeToStringHelperMethods.GetString(domainEvent.GetType()), domainEvent.AggregateRootId, domainEvent.EventDate, domainEvent.Sequence, + (serializer.Serialize(domainEvent) ?? string.Empty) + .Replace("'", "''")); + + if (sql.Length <= 0) return; + + using (var connection = new SqlConnection(configuration.ConnectionString)) + { + connection.Open(); + using (var command = new SqlCommand(sql.ToString(), connection)) + command.ExecuteNonQuery(); + connection.Close(); + } + } + + public IEnumerable GetEventsByEventTypes(IEnumerable domainEventTypes) + { + var events = new List(); + + var eventParameters = domainEventTypes.Select(TypeToStringHelperMethods.GetString).Join("','"); + + using (var connection = new SqlConnection(configuration.ConnectionString)) + { + connection.Open(); + var sql = string.Format(MySqlStatements.GetEventsByType, "EventStore", eventParameters); + using (var command = new SqlCommand(sql, connection)) + using (var reader = command.ExecuteReader()) + while (reader.Read()) + { + var type = reader["EventType"].ToString(); + var data = reader["data"].ToString(); + + var domainEvent = serializer.Deserialize(Type.GetType(type), data); + events.Add(domainEvent); + } + connection.Close(); + } + return events; + } + + public IEnumerable GetEventsByEventTypes(IEnumerable domainEventTypes, Guid aggregateRootId) + { + throw new NotImplementedException(); + } + + public IEnumerable GetEventsByEventTypes(IEnumerable domainEventTypes, DateTime startDate, DateTime endDate) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/TypeToStringHelperMethods.cs b/src/EventStores/SimpleCqrs.EventStore.MySql/TypeToStringHelperMethods.cs new file mode 100644 index 0000000..e1fd6c4 --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/TypeToStringHelperMethods.cs @@ -0,0 +1,14 @@ +using System; + +namespace SimpleCqrs.EventStore.MySql +{ + public static class TypeToStringHelperMethods + { + public static string GetString(Type type) + { + var typeArray = type.AssemblyQualifiedName.Split(" ".ToCharArray()); + var returnValue = typeArray[0] + " " + typeArray[1].Replace(",", ""); + return returnValue; + } + } +} \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.MySql/packages.config b/src/EventStores/SimpleCqrs.EventStore.MySql/packages.config new file mode 100644 index 0000000..4cfb792 --- /dev/null +++ b/src/EventStores/SimpleCqrs.EventStore.MySql/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlServerEventStore.cs b/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlServerEventStore.cs index 4955553..d819bfd 100644 --- a/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlServerEventStore.cs +++ b/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlServerEventStore.cs @@ -64,7 +64,7 @@ public void Insert(IEnumerable domainEvents) { var sql = new StringBuilder(); foreach (var domainEvent in domainEvents) - sql.AppendFormat(SqlStatements.InsertEvents, "EventStore", TypeToStringHelperMethods.GetString(domainEvent.GetType()), domainEvent.AggregateRootId, domainEvent.EventDate, domainEvent.Sequence, + sql.AppendFormat(SqlStatements.InsertEvents, "EventStore", TypeToStringHelperMethods.GetString(domainEvent.GetType()), domainEvent.AggregateRootId, domainEvent.EventDate.ToString("s"), domainEvent.Sequence, (serializer.Serialize(domainEvent) ?? string.Empty) .Replace("'", "''")); diff --git a/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlStatements.cs b/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlStatements.cs index d1ab2ba..b5b2049 100644 --- a/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlStatements.cs +++ b/src/EventStores/SimpleCqrs.EventStore.SqlServer/SqlStatements.cs @@ -3,7 +3,7 @@ public class SqlStatements { internal const string GetEventsByType = "select eventtype, data from {0} where eventtype in ('{1}')"; - internal const string InsertEvents = "insert into {0} values ('{1}', '{2}', '{3}', {4}, '{5}')"; + internal const string InsertEvents = "insert into {0} values ('{1}', '{2}', Convert(datetime,'{3}'), {4}, '{5}')"; internal const string GetEventsByAggregateRootAndSequence = "select eventtype, data from {1} where aggregaterootid = '{2}' and sequence >= {3}"; internal const string CreateTheEventStoreTable = @"IF not EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[{0}]') AND type in (N'U')) begin diff --git a/src/Packages/Dapper.1.8/Dapper.1.8.nupkg b/src/Packages/Dapper.1.8/Dapper.1.8.nupkg new file mode 100644 index 0000000..d656e3c Binary files /dev/null and b/src/Packages/Dapper.1.8/Dapper.1.8.nupkg differ diff --git a/src/Packages/Dapper.1.8/lib/net35/Dapper.dll b/src/Packages/Dapper.1.8/lib/net35/Dapper.dll new file mode 100644 index 0000000..5952983 Binary files /dev/null and b/src/Packages/Dapper.1.8/lib/net35/Dapper.dll differ diff --git a/src/Packages/Dapper.1.8/lib/net35/Dapper.pdb b/src/Packages/Dapper.1.8/lib/net35/Dapper.pdb new file mode 100644 index 0000000..37c65f9 Binary files /dev/null and b/src/Packages/Dapper.1.8/lib/net35/Dapper.pdb differ diff --git a/src/Packages/Dapper.1.8/lib/net40/Dapper.dll b/src/Packages/Dapper.1.8/lib/net40/Dapper.dll new file mode 100644 index 0000000..4ad8e9e Binary files /dev/null and b/src/Packages/Dapper.1.8/lib/net40/Dapper.dll differ diff --git a/src/Packages/Dapper.1.8/lib/net40/Dapper.pdb b/src/Packages/Dapper.1.8/lib/net40/Dapper.pdb new file mode 100644 index 0000000..00c2ad9 Binary files /dev/null and b/src/Packages/Dapper.1.8/lib/net40/Dapper.pdb differ diff --git a/src/Packages/MySql.Data.6.5.4/MySql.Data.6.5.4.nupkg b/src/Packages/MySql.Data.6.5.4/MySql.Data.6.5.4.nupkg new file mode 100644 index 0000000..d41e397 Binary files /dev/null and b/src/Packages/MySql.Data.6.5.4/MySql.Data.6.5.4.nupkg differ diff --git a/src/Packages/MySql.Data.6.5.4/lib/net20-cf/MySql.Data.cf.dll b/src/Packages/MySql.Data.6.5.4/lib/net20-cf/MySql.Data.cf.dll new file mode 100644 index 0000000..caab890 Binary files /dev/null and b/src/Packages/MySql.Data.6.5.4/lib/net20-cf/MySql.Data.cf.dll differ diff --git a/src/Packages/MySql.Data.6.5.4/lib/net20/MySql.Data.dll b/src/Packages/MySql.Data.6.5.4/lib/net20/MySql.Data.dll new file mode 100644 index 0000000..088ef1c Binary files /dev/null and b/src/Packages/MySql.Data.6.5.4/lib/net20/MySql.Data.dll differ diff --git a/src/Packages/MySql.Data.6.5.4/lib/net40/MySql.Data.dll b/src/Packages/MySql.Data.6.5.4/lib/net40/MySql.Data.dll new file mode 100644 index 0000000..0662a83 Binary files /dev/null and b/src/Packages/MySql.Data.6.5.4/lib/net40/MySql.Data.dll differ diff --git a/src/Packages/repositories.config b/src/Packages/repositories.config index e39cf99..d2030ed 100644 --- a/src/Packages/repositories.config +++ b/src/Packages/repositories.config @@ -1,4 +1,5 @@ - - - + + + + \ No newline at end of file diff --git a/src/SimpleCQRS.sln b/src/SimpleCQRS.sln index 04ded4b..7e63f61 100644 --- a/src/SimpleCQRS.sln +++ b/src/SimpleCQRS.sln @@ -40,6 +40,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleCqrs.EventStore.SqlSe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleCqrs.StructureMap", "ServiceLocators\SimpleCqrs.StructureMap\SimpleCqrs.StructureMap.csproj", "{9C1063E4-1A0B-4402-8D78-A3F8E23D3D98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleCqrs.EventStore.MySql", "EventStores\SimpleCqrs.EventStore.MySql\SimpleCqrs.EventStore.MySql.csproj", "{D55AC0E2-94D7-4805-A108-C51A59F9E2D1}" +EndProject Global GlobalSection(TestCaseManagementSettings) = postSolution CategoryFile = Simple CQRS.vsmdi @@ -93,6 +95,10 @@ Global {9C1063E4-1A0B-4402-8D78-A3F8E23D3D98}.Debug|Any CPU.Build.0 = Debug|Any CPU {9C1063E4-1A0B-4402-8D78-A3F8E23D3D98}.Release|Any CPU.ActiveCfg = Release|Any CPU {9C1063E4-1A0B-4402-8D78-A3F8E23D3D98}.Release|Any CPU.Build.0 = Release|Any CPU + {D55AC0E2-94D7-4805-A108-C51A59F9E2D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D55AC0E2-94D7-4805-A108-C51A59F9E2D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D55AC0E2-94D7-4805-A108-C51A59F9E2D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D55AC0E2-94D7-4805-A108-C51A59F9E2D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -103,6 +109,7 @@ Global {201A4DF5-2217-4202-80E8-E784737366BC} = {77CC485D-D260-45FC-A161-3DDF3A571C41} {65B52205-E45D-4D6B-8F48-AFBC4406BEE8} = {77CC485D-D260-45FC-A161-3DDF3A571C41} {BEEBFC8E-91EC-4E24-8223-395C2302C1B3} = {77CC485D-D260-45FC-A161-3DDF3A571C41} + {D55AC0E2-94D7-4805-A108-C51A59F9E2D1} = {77CC485D-D260-45FC-A161-3DDF3A571C41} {38E51E8F-7008-47EC-AC9F-862A1F377532} = {D3DFE07B-B78C-4AF2-A886-002A09D9FAA0} {00634E6C-A282-404A-B08B-338FD4CA41A9} = {0C8AE82D-CACF-44CD-BE4F-222041AC4699} {9C1063E4-1A0B-4402-8D78-A3F8E23D3D98} = {0C8AE82D-CACF-44CD-BE4F-222041AC4699}