Commit 0444094f authored by Marc Gravell's avatar Marc Gravell

Manual rollback while I figure out some DapperTable questions

parent 1e1c7157
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
Note: to build on C# 3.0 + .NET 3.5, include the CSHARP30 compiler symbol (and yes, 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). I know the difference between language and runtime versions; this is a compromise).
*/ */
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
...@@ -19,7 +18,6 @@ ...@@ -19,7 +18,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Diagnostics; using System.Diagnostics;
namespace Dapper namespace Dapper
{ {
/// <summary> /// <summary>
...@@ -259,7 +257,7 @@ public static void PurgeQueryCache() ...@@ -259,7 +257,7 @@ public static void PurgeQueryCache()
static readonly System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo> _queryCache = new System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo>(); static readonly System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo> _queryCache = new System.Collections.Concurrent.ConcurrentDictionary<Identity, CacheInfo>();
private static void SetQueryCache(Identity key, CacheInfo value) private static void SetQueryCache(Identity key, CacheInfo value)
{ {
if(Interlocked.Increment(ref collect)==COLLECT_PER_ITEMS) if (Interlocked.Increment(ref collect) == COLLECT_PER_ITEMS)
{ {
CollectCacheGarbage(); CollectCacheGarbage();
} }
...@@ -279,7 +277,7 @@ private static void CollectCacheGarbage() ...@@ -279,7 +277,7 @@ private static void CollectCacheGarbage()
} }
} }
} }
finally finally
{ {
Interlocked.Exchange(ref collect, 0); Interlocked.Exchange(ref collect, 0);
...@@ -290,7 +288,7 @@ private static void CollectCacheGarbage() ...@@ -290,7 +288,7 @@ private static void CollectCacheGarbage()
private static int collect; private static int collect;
private static bool TryGetQueryCache(Identity key, out CacheInfo value) private static bool TryGetQueryCache(Identity key, out CacheInfo value)
{ {
if(_queryCache.TryGetValue(key, out value)) if (_queryCache.TryGetValue(key, out value))
{ {
value.RecordHit(); value.RecordHit();
return true; return true;
...@@ -343,16 +341,17 @@ public static int GetCachedSQLCount() ...@@ -343,16 +341,17 @@ public static int GetCachedSQLCount()
/// Deep diagnostics only: find any hash collisions in the cache /// Deep diagnostics only: find any hash collisions in the cache
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public static IEnumerable<Tuple<int,int>> GetHashCollissions() public static IEnumerable<Tuple<int, int>> GetHashCollissions()
{ {
var counts = new Dictionary<int, int>(); var counts = new Dictionary<int, int>();
foreach(var key in _queryCache.Keys) foreach (var key in _queryCache.Keys)
{ {
int count; int count;
if(!counts.TryGetValue(key.hashCode, out count)) if (!counts.TryGetValue(key.hashCode, out count))
{ {
counts.Add(key.hashCode, 1); counts.Add(key.hashCode, 1);
} else }
else
{ {
counts[key.hashCode] = count + 1; counts[key.hashCode] = count + 1;
} }
...@@ -490,7 +489,7 @@ private Identity(string sql, CommandType? commandType, string connectionString, ...@@ -490,7 +489,7 @@ private Identity(string sql, CommandType? commandType, string connectionString,
hashCode = hashCode * 23 + (parametersType == null ? 0 : parametersType.GetHashCode()); hashCode = hashCode * 23 + (parametersType == null ? 0 : parametersType.GetHashCode());
} }
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
...@@ -508,7 +507,7 @@ public override bool Equals(object obj) ...@@ -508,7 +507,7 @@ public override bool Equals(object obj)
/// The command type /// The command type
/// </summary> /// </summary>
public readonly CommandType? commandType; public readonly CommandType? commandType;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
...@@ -672,13 +671,13 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec ...@@ -672,13 +671,13 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec
Identity identity; Identity identity;
CacheInfo info = null; CacheInfo info = null;
if (multiExec != null && !(multiExec is string)) if (multiExec != null && !(multiExec is string))
{ {
bool isFirst = true; bool isFirst = true;
int total = 0; int total = 0;
using (var cmd = SetupCommand(cnn, transaction, sql, null, null, commandTimeout, commandType)) using (var cmd = SetupCommand(cnn, transaction, sql, null, null, commandTimeout, commandType))
{ {
string masterSql = null; string masterSql = null;
foreach (var obj in multiExec) foreach (var obj in multiExec)
{ {
if (isFirst) if (isFirst)
...@@ -703,7 +702,7 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec ...@@ -703,7 +702,7 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec
// nice and simple // nice and simple
if ((object)param != null) if ((object)param != null)
{ {
identity = new Identity(sql, commandType, cnn, null, (object) param == null ? null : ((object) param).GetType(), null); identity = new Identity(sql, commandType, cnn, null, (object)param == null ? null : ((object)param).GetType(), null);
info = GetCacheInfo(identity); info = GetCacheInfo(identity);
} }
return ExecuteCommand(cnn, transaction, sql, (object)param == null ? null : info.ParamReader, (object)param, commandTimeout, commandType); return ExecuteCommand(cnn, transaction, sql, (object)param == null ? null : info.ParamReader, (object)param, commandTimeout, commandType);
...@@ -714,7 +713,7 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec ...@@ -714,7 +713,7 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec
/// </summary> /// </summary>
public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null) public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dynamic param = null, IDbTransaction transaction = null, bool buffered = true, int? commandTimeout = null, CommandType? commandType = null)
{ {
return Query<DapperRow>(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType); return Query<FastExpando>(cnn, sql, param as object, transaction, buffered, commandTimeout, commandType);
} }
#else #else
/// <summary> /// <summary>
...@@ -752,7 +751,7 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dyn ...@@ -752,7 +751,7 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dyn
return Query<IDictionary<string, object>>(cnn, sql, param, transaction, buffered, commandTimeout, commandType); return Query<IDictionary<string, object>>(cnn, sql, param, transaction, buffered, commandTimeout, commandType);
} }
#endif #endif
/// <summary> /// <summary>
/// Executes a query, returning the data typed as per T /// Executes a query, returning the data typed as per T
/// </summary> /// </summary>
...@@ -794,7 +793,7 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dyn ...@@ -794,7 +793,7 @@ public static IEnumerable<dynamic> Query(this IDbConnection cnn, string sql, dyn
if (wasClosed) cnn.Open(); if (wasClosed) cnn.Open();
cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType); cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, (object)param, commandTimeout, commandType);
reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default);
var result = new GridReader(cmd, reader, identity); var result = new GridReader(cmd, reader, identity);
wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader 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 // with the CloseConnection flag, so the reader will deal with the connection; we
...@@ -831,13 +830,13 @@ private static IEnumerable<T> QueryInternal<T>(this IDbConnection cnn, string sq ...@@ -831,13 +830,13 @@ private static IEnumerable<T> QueryInternal<T>(this IDbConnection cnn, string sq
try try
{ {
cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType); cmd = SetupCommand(cnn, transaction, sql, info.ParamReader, param, commandTimeout, commandType);
if (wasClosed) cnn.Open(); if (wasClosed) cnn.Open();
reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default); reader = cmd.ExecuteReader(wasClosed ? CommandBehavior.CloseConnection : CommandBehavior.Default);
wasClosed = false; // *if* the connection was closed and we got this far, then we now have a reader 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 // 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 // still need something in the "finally" to ensure that broken SQL still results
// in the connection closing itself // in the connection closing itself
var tuple = info.Deserializer; var tuple = info.Deserializer;
int hash = GetColumnHash(reader); int hash = GetColumnHash(reader);
if (tuple.Func == null || tuple.Hash != hash) if (tuple.Func == null || tuple.Hash != hash)
...@@ -986,7 +985,7 @@ partial class DontMap { } ...@@ -986,7 +985,7 @@ partial class DontMap { }
return buffered ? results.ToList() : results; return buffered ? results.ToList() : results;
} }
static IEnumerable<TReturn> MultiMapImpl<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(this IDbConnection cnn, string sql, object map, object param, IDbTransaction transaction, string splitOn, int? commandTimeout, CommandType? commandType, IDataReader reader, Identity identity) static IEnumerable<TReturn> MultiMapImpl<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(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) }); 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) });
...@@ -1046,7 +1045,7 @@ partial class DontMap { } ...@@ -1046,7 +1045,7 @@ partial class DontMap { }
private static Func<IDataReader, TReturn> GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(Func<IDataReader, object> deserializer, Func<IDataReader, object>[] otherDeserializers, object map) private static Func<IDataReader, TReturn> GenerateMapper<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(Func<IDataReader, object> deserializer, Func<IDataReader, object>[] otherDeserializers, object map)
{ {
switch(otherDeserializers.Length) switch (otherDeserializers.Length)
{ {
case 1: case 1:
return r => ((Func<TFirst, TSecond, TReturn>)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r)); return r => ((Func<TFirst, TSecond, TReturn>)map)((TFirst)deserializer(r), (TSecond)otherDeserializers[0](r));
...@@ -1148,7 +1147,7 @@ private static CacheInfo GetCacheInfo(Identity identity) ...@@ -1148,7 +1147,7 @@ private static CacheInfo GetCacheInfo(Identity identity)
{ {
if (typeof(IDynamicParameters).IsAssignableFrom(identity.parametersType)) if (typeof(IDynamicParameters).IsAssignableFrom(identity.parametersType))
{ {
info.ParamReader = (cmd, obj) => { (obj as IDynamicParameters).AddParameters(cmd,identity); }; info.ParamReader = (cmd, obj) => { (obj as IDynamicParameters).AddParameters(cmd, identity); };
} }
#if !CSHARP30 #if !CSHARP30
else if (typeof(IEnumerable<KeyValuePair<string, object>>).IsAssignableFrom(identity.parametersType) && typeof(System.Dynamic.IDynamicMetaObjectProvider).IsAssignableFrom(identity.parametersType)) else if (typeof(IEnumerable<KeyValuePair<string, object>>).IsAssignableFrom(identity.parametersType) && typeof(System.Dynamic.IDynamicMetaObjectProvider).IsAssignableFrom(identity.parametersType))
...@@ -1175,9 +1174,9 @@ private static CacheInfo GetCacheInfo(Identity identity) ...@@ -1175,9 +1174,9 @@ private static CacheInfo GetCacheInfo(Identity identity)
#if !CSHARP30 #if !CSHARP30
// dynamic is passed in as Object ... by c# design // dynamic is passed in as Object ... by c# design
if (type == typeof(object) if (type == typeof(object)
|| type == typeof(DapperRow)) || type == typeof(FastExpando))
{ {
return GetDapperRowDeserializer(reader, startBound, length, returnNullIfFirstMissing); return GetDynamicDeserializer(reader, startBound, length, returnNullIfFirstMissing);
} }
#else #else
if(type.IsAssignableFrom(typeof(Dictionary<string,object>))) if(type.IsAssignableFrom(typeof(Dictionary<string,object>)))
...@@ -1194,398 +1193,131 @@ private static CacheInfo GetCacheInfo(Identity identity) ...@@ -1194,398 +1193,131 @@ private static CacheInfo GetCacheInfo(Identity identity)
return GetStructDeserializer(type, underlyingType ?? type, startBound); return GetStructDeserializer(type, underlyingType ?? type, startBound);
} }
#if !CSHARP30 #if !CSHARP30
sealed partial class DapperTable private partial class FastExpando : System.Dynamic.DynamicObject, IDictionary<string, object>
{ {
const int CutOff = 10; IDictionary<string, object> data;
public readonly static DapperTable Empty = new DapperTable(null);
readonly Tuple<string, int>[] m_nameToIndex; public static FastExpando Attach(IDictionary<string, object> data)
readonly Dictionary<string, int> m_nameToIndexLookup;
public DapperTable(IEnumerable<Tuple<string, int>> nameToIndex)
{ {
nameToIndex = nameToIndex ?? new Tuple<string, int>[0]; return new FastExpando { data = data };
m_nameToIndex = nameToIndex.ToArray();
if (m_nameToIndex.Length < CutOff)
{
return;
}
m_nameToIndexLookup = new Dictionary<string, int>();
for (var index = 0; index < m_nameToIndex.Length; index++)
{
var nti = m_nameToIndex[index];
var key = nti.Item1 ?? "";
// Duplicates are ignored
if (!m_nameToIndexLookup.ContainsKey(key))
{
m_nameToIndexLookup[key] = nti.Item2;
}
}
} }
internal Tuple<string, int>[] FieldNames public override bool TrySetMember(System.Dynamic.SetMemberBinder binder, object value)
{ {
get data[binder.Name] = value;
{ return true;
return m_nameToIndex;
}
}
internal int IndexOfName(string name)
{
name = name ?? "";
if (m_nameToIndexLookup == null)
{
for (var index = 0; index < m_nameToIndex.Length; index++)
{
var nti = m_nameToIndex[index];
if (nti.Item1.Equals(name, StringComparison.Ordinal))
{
return nti.Item2;
}
}
return -1;
}
int result;
return
m_nameToIndexLookup.TryGetValue(name, out result)
? result
: -1
;
}
}
sealed partial class DapperRowMetaObject : System.Dynamic.DynamicMetaObject
{
static MethodInfo GetMethod<T>(System.Linq.Expressions.Expression<Action<T>> expression)
{
return ((System.Linq.Expressions.MethodCallExpression) expression.Body).Method;
}
static readonly MethodInfo s_getValueMethod = GetMethod<DapperRow>(row => row.GetValue(default(string)));
static readonly MethodInfo s_setValueMethod = GetMethod<DapperRow>(row => row.SetValue(default(string), default(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(s_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(s_setValueMethod, parameters);
return callMethod;
}
}
sealed partial class DapperRow
: System.Dynamic.IDynamicMetaObjectProvider
, IDictionary<string, object>
{
static readonly object[] s_emptyValues = new object[0];
DapperTable m_table;
object[] m_values;
Dictionary<string, object> m_additionalValues;
public DapperRow(DapperTable table, object[] values)
{
m_table = table ?? DapperTable.Empty ;
m_values = values ?? s_emptyValues ;
} }
public DapperTable Table public override bool TryGetMember(System.Dynamic.GetMemberBinder binder, out object result)
{ {
get { return m_table; } return data.TryGetValue(binder.Name, out result);
} }
public int Count public override IEnumerable<string> GetDynamicMemberNames()
{ {
get { return Table.FieldNames.Length + (m_additionalValues != null ? m_additionalValues.Count : 0); } return data.Keys;
} }
public bool TryGetValue(string name, out object value) #region IDictionary<string,object> Members
{
value = null;
var index = Table.IndexOfName(name);
if (index == -1)
{
if (m_additionalValues == null)
{
return false;
}
return m_additionalValues.TryGetValue(name ?? "", out value);
}
value = GetFieldValueImpl(index);
return true; void IDictionary<string, object>.Add(string key, object value)
}
public object GetValue(string name)
{ {
object value; data.Add(key, value);
return TryGetValue(name, out value) ? value : null;
} }
public object SetValue(string name, object value) bool IDictionary<string, object>.ContainsKey(string key)
{ {
var index = Table.IndexOfName(name); return data.ContainsKey(key);
if (index == -1)
{
if (m_additionalValues == null)
{
m_additionalValues = new Dictionary<string, object>();
}
m_additionalValues[name ?? ""] = value;
return value;
}
return SetFieldValueImpl(index, value);
} }
object GetFieldValueImpl(int i) ICollection<string> IDictionary<string, object>.Keys
{ {
var value = m_values[i]; get { return data.Keys; }
if (value is DBNull)
{
return null;
}
return value;
} }
object SetFieldValueImpl(int i, object value) bool IDictionary<string, object>.Remove(string key)
{ {
m_values[i] = value; return data.Remove(key);
return value;
} }
public override string ToString() bool IDictionary<string, object>.TryGetValue(string key, out object value)
{ {
var sb = new StringBuilder("{DapperRow"); return data.TryGetValue(key, out value);
foreach (var kv in this)
{
var value = kv.Value;
sb.Append(", ");
sb.Append(kv.Key);
if (value != null)
{
sb.Append(" = '");
sb.Append(kv.Value);
sb.Append('\'');
}
else
{
sb.Append(" = NULL");
}
}
sb.Append('}');
return sb.ToString();
} }
public System.Dynamic.DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression parameter) ICollection<object> IDictionary<string, object>.Values
{ {
return new DapperRowMetaObject(parameter, System.Dynamic.BindingRestrictions.Empty, this); get { return data.Values; }
} }
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() object IDictionary<string, object>.this[string key]
{ {
for (var index = 0; index < Table.FieldNames.Length; index++) get
{ {
var fieldName = Table.FieldNames[index]; return data[key];
yield return new KeyValuePair<string, object>(fieldName.Item1, GetFieldValueImpl(fieldName.Item2));
} }
set
if (m_additionalValues != null)
{ {
foreach (var additionalValue in m_additionalValues) data[key] = value;
{
yield return additionalValue;
}
} }
} }
IEnumerator IEnumerable.GetEnumerator() #endregion
{
return GetEnumerator();
}
#region Implementation of ICollection<KeyValuePair<string,object>> #region ICollection<KeyValuePair<string,object>> Members
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item) void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
{ {
IDictionary<string, object> dic = this; data.Add(item);
dic.Add(item.Key, item.Value);
} }
void ICollection<KeyValuePair<string, object>>.Clear() void ICollection<KeyValuePair<string, object>>.Clear()
{ {
m_table = DapperTable.Empty ; data.Clear();
m_values = s_emptyValues ;
m_additionalValues = null ;
} }
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item) bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
{ {
object value; return data.Contains(item);
return TryGetValue(item.Key, out value) && Equals(value, item.Value);
} }
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{ {
foreach (var kv in this) data.CopyTo(array, arrayIndex);
{
if (arrayIndex < array.Length)
{
array[arrayIndex] = kv;
}
else
{
return;
}
++arrayIndex;
}
} }
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item) int ICollection<KeyValuePair<string, object>>.Count
{ {
IDictionary<string, object> dic = this; get { return data.Count; }
return dic.Remove(item.Key);
} }
bool ICollection<KeyValuePair<string, object>>.IsReadOnly bool ICollection<KeyValuePair<string, object>>.IsReadOnly
{ {
get { return false; } get { return true; }
} }
#endregion bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
#region Implementation of IDictionary<string,object>
bool IDictionary<string, object>.ContainsKey(string key)
{ {
object value; return data.Remove(item);
return TryGetValue(key, out value);
} }
void IDictionary<string, object>.Add(string key, object value) #endregion
{
IDictionary<string, object> dic = this;
if (dic.ContainsKey(key ?? ""))
{
throw new ArgumentException("An item with the same key has already been added." ,"key");
}
SetValue(key, value); #region IEnumerable<KeyValuePair<string,object>> Members
}
bool IDictionary<string, object>.Remove(string key) IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
{ {
var name = key ?? ""; return data.GetEnumerator();
if (m_additionalValues != null && m_additionalValues.Remove(name))
{
return true;
}
var index = Table.IndexOfName(name);
if (index == -1)
{
return false;
}
if (m_additionalValues == null)
{
m_additionalValues = new Dictionary<string, object>();
}
for (var i = 0; i < Table.FieldNames.Length; i++)
{
var fieldName = Table.FieldNames[i];
m_additionalValues[fieldName.Item1] = GetFieldValueImpl(fieldName.Item2);
}
m_additionalValues.Remove(name);
m_table = DapperTable.Empty;
return true;
} }
object IDictionary<string, object>.this[string key] #endregion
{
get { return GetValue(key); }
set { SetValue(key, value); }
}
ICollection<string> IDictionary<string, object>.Keys #region IEnumerable Members
{
get { return this.Select(kv => kv.Key).ToArray(); }
}
ICollection<object> IDictionary<string, object>.Values IEnumerator IEnumerable.GetEnumerator()
{ {
get { return this.Select(kv => kv.Value).ToArray(); } return data.GetEnumerator();
} }
#endregion #endregion
...@@ -1593,62 +1325,11 @@ IEnumerator IEnumerable.GetEnumerator() ...@@ -1593,62 +1325,11 @@ IEnumerator IEnumerable.GetEnumerator()
#endif #endif
#if !CSHARP30 #if !CSHARP30
internal static Func<IDataReader, object> GetDapperRowDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) private static Func<IDataReader, object> 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");
}
var effectiveFieldCount = fieldCount - startBound;
DapperTable table = null;
return
r =>
{
if(table == null)
{
table = new DapperTable(Enumerable
.Range(0, effectiveFieldCount)
.Select(i => Tuple.Create(reader.GetName(i + startBound), i)))
;
}
var values = new object[effectiveFieldCount];
if (returnNullIfFirstMissing)
{
values[0] = r.GetValue (startBound);
if (values[0] is DBNull)
{
return null;
}
}
if (startBound == 0)
{
r.GetValues(values);
}
else
{
var begin = returnNullIfFirstMissing ? 1 : 0;
for (var iter = begin; iter < effectiveFieldCount; ++iter)
{
values[iter] = r.GetValue(iter + startBound);
}
}
return new DapperRow(table, values);
};
}
#else #else
internal static Func<IDataReader, object> GetDictionaryDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing) private static Func<IDataReader, object> GetDictionaryDeserializer(IDataRecord reader, int startBound, int length, bool returnNullIfFirstMissing)
#endif
{ {
var fieldCount = reader.FieldCount; var fieldCount = reader.FieldCount;
if (length == -1) if (length == -1)
...@@ -1675,10 +1356,15 @@ IEnumerator IEnumerable.GetEnumerator() ...@@ -1675,10 +1356,15 @@ IEnumerator IEnumerable.GetEnumerator()
return null; return null;
} }
} }
#if !CSHARP30
//we know this is an object so it will not box
return FastExpando.Attach(row);
#else
return row; return row;
#endif
}; };
} }
#endif
/// <summary> /// <summary>
/// Internal use only /// Internal use only
/// </summary> /// </summary>
...@@ -1691,7 +1377,7 @@ public static char ReadChar(object value) ...@@ -1691,7 +1377,7 @@ public static char ReadChar(object value)
if (value == null || value is DBNull) throw new ArgumentNullException("value"); if (value == null || value is DBNull) throw new ArgumentNullException("value");
string s = value as string; string s = value as string;
if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value");
return s[0]; return s[0];
} }
/// <summary> /// <summary>
...@@ -1704,10 +1390,10 @@ public static char ReadChar(object value) ...@@ -1704,10 +1390,10 @@ public static char ReadChar(object value)
if (value == null || value is DBNull) return null; if (value == null || value is DBNull) return null;
string s = value as string; string s = value as string;
if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value"); if (s == null || s.Length != 1) throw new ArgumentException("A single-character was expected", "value");
return s[0]; return s[0];
} }
/// <summary> /// <summary>
/// Internal use only /// Internal use only
/// </summary> /// </summary>
...@@ -1744,60 +1430,60 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj ...@@ -1744,60 +1430,60 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj
if (list != null) if (list != null)
{ {
if (FeatureSupport.Get(command.Connection).Arrays) if (FeatureSupport.Get(command.Connection).Arrays)
{ {
var arrayParm = command.CreateParameter(); var arrayParm = command.CreateParameter();
arrayParm.Value = list; arrayParm.Value = list;
arrayParm.ParameterName = namePrefix; arrayParm.ParameterName = namePrefix;
command.Parameters.Add(arrayParm); command.Parameters.Add(arrayParm);
} }
else else
{ {
bool isString = value is IEnumerable<string>; bool isString = value is IEnumerable<string>;
bool isDbString = value is IEnumerable<DbString>; bool isDbString = value is IEnumerable<DbString>;
foreach (var item in list) foreach (var item in list)
{ {
count++; count++;
var listParam = command.CreateParameter(); var listParam = command.CreateParameter();
listParam.ParameterName = namePrefix + count; listParam.ParameterName = namePrefix + count;
listParam.Value = item ?? DBNull.Value; listParam.Value = item ?? DBNull.Value;
if (isString) if (isString)
{ {
listParam.Size = 4000; listParam.Size = 4000;
if (item != null && ((string) item).Length > 4000) if (item != null && ((string)item).Length > 4000)
{ {
listParam.Size = -1; listParam.Size = -1;
} }
} }
if (isDbString && item as DbString != null) if (isDbString && item as DbString != null)
{ {
var str = item as DbString; var str = item as DbString;
str.AddParameter(command, listParam.ParameterName); str.AddParameter(command, listParam.ParameterName);
} }
else else
{ {
command.Parameters.Add(listParam); command.Parameters.Add(listParam);
} }
} }
if (count == 0) if (count == 0)
{ {
command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), "(SELECT NULL WHERE 1 = 0)"); command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), "(SELECT NULL WHERE 1 = 0)");
} }
else else
{ {
command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), match => command.CommandText = Regex.Replace(command.CommandText, @"[?@:]" + Regex.Escape(namePrefix), match =>
{ {
var grp = match.Value; var grp = match.Value;
var sb = new StringBuilder("(").Append(grp).Append(1); var sb = new StringBuilder("(").Append(grp).Append(1);
for (int i = 2; i <= count; i++) for (int i = 2; i <= count; i++)
{ {
sb.Append(',').Append(grp).Append(i); sb.Append(',').Append(grp).Append(i);
} }
return sb.Append(')').ToString(); return sb.Append(')').ToString();
}); });
} }
} }
} }
} }
...@@ -1840,7 +1526,7 @@ private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyIn ...@@ -1840,7 +1526,7 @@ private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyIn
&& 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 { // can't see the parameter in the text (even in a comment, etc) - burn it with fire
continue; continue;
} }
} }
if (prop.PropertyType == typeof(DbString)) if (prop.PropertyType == typeof(DbString))
{ {
...@@ -1885,7 +1571,7 @@ private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyIn ...@@ -1885,7 +1571,7 @@ private static IEnumerable<PropertyInfo> FilterParameters(IEnumerable<PropertyIn
il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [parameter] [parameter] [name] 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.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("ParameterName").GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter]
} }
if(dbType != DbType.Time) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time if (dbType != DbType.Time) // 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] il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter]
EmitInt32(il, (int)dbType);// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] EmitInt32(il, (int)dbType);// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type]
...@@ -2071,13 +1757,13 @@ public static ITypeMap GetTypeMap(Type type) ...@@ -2071,13 +1757,13 @@ public static ITypeMap GetTypeMap(Type type)
{ {
if (type == null) throw new ArgumentNullException("type"); if (type == null) throw new ArgumentNullException("type");
var map = (ITypeMap)_typeMaps[type]; var map = (ITypeMap)_typeMaps[type];
if(map == null) if (map == null)
{ {
lock(_typeMaps) lock (_typeMaps)
{ // double-checked; store this to avoid reflection next time we see this type { // 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 // since multiple queries commonly use the same domain-entity/DTO/view-model type
map = (ITypeMap)_typeMaps[type]; map = (ITypeMap)_typeMaps[type];
if(map == null) if (map == null)
{ {
map = new DefaultTypeMap(type); map = new DefaultTypeMap(type);
_typeMaps[type] = map; _typeMaps[type] = map;
...@@ -2131,11 +1817,11 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2131,11 +1817,11 @@ public static void SetTypeMap(Type type, ITypeMap map)
#if CSHARP30 #if CSHARP30
Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing
#else #else
Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false
#endif #endif
) )
{ {
var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true); var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true);
var il = dm.GetILGenerator(); var il = dm.GetILGenerator();
il.DeclareLocal(typeof(int)); il.DeclareLocal(typeof(int));
...@@ -2191,7 +1877,7 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2191,7 +1877,7 @@ public static void SetTypeMap(Type type, ITypeMap map)
if (ctor.GetParameters().Length == 0) if (ctor.GetParameters().Length == 0)
{ {
il.Emit(OpCodes.Newobj, ctor); il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Stloc_1); il.Emit(OpCodes.Stloc_1);
} }
else else
specializedConstructor = ctor; specializedConstructor = ctor;
...@@ -2199,16 +1885,16 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2199,16 +1885,16 @@ public static void SetTypeMap(Type type, ITypeMap map)
} }
il.BeginExceptionBlock(); il.BeginExceptionBlock();
if(type.IsValueType) if (type.IsValueType)
{ {
il.Emit(OpCodes.Ldloca_S, (byte)1);// [target] il.Emit(OpCodes.Ldloca_S, (byte)1);// [target]
} }
else if(specializedConstructor == null) else if (specializedConstructor == null)
{ {
il.Emit(OpCodes.Ldloc_1);// [target] il.Emit(OpCodes.Ldloc_1);// [target]
} }
var members = (specializedConstructor != null var members = (specializedConstructor != null
? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n))
: names.Select(n => typeMap.GetMember(n))).ToList(); : names.Select(n => typeMap.GetMember(n))).ToList();
...@@ -2350,12 +2036,12 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2350,12 +2036,12 @@ public static void SetTypeMap(Type type, ITypeMap map)
{ // use flexible conversion { // use flexible conversion
il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][value][member-type-token] il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][value][member-type-token]
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); // stack is now [target][target][value][member-type] il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null); // stack is now [target][target][value][member-type]
il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod("ChangeType", new Type[] {typeof(object), typeof(Type)}),null); // stack is now [target][target][boxed-member-type-value] il.EmitCall(OpCodes.Call, typeof(Convert).GetMethod("ChangeType", new Type[] { typeof(object), typeof(Type) }), null); // stack is now [target][target][boxed-member-type-value]
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
} }
} }
} }
} }
if (specializedConstructor == null) if (specializedConstructor == null)
...@@ -2435,19 +2121,19 @@ public static void SetTypeMap(Type type, ITypeMap map) ...@@ -2435,19 +2121,19 @@ public static void SetTypeMap(Type type, ITypeMap map)
il.EndExceptionBlock(); il.EndExceptionBlock();
il.Emit(OpCodes.Ldloc_1); // stack is [rval] il.Emit(OpCodes.Ldloc_1); // stack is [rval]
if(type.IsValueType) if (type.IsValueType)
{ {
il.Emit(OpCodes.Box, type); il.Emit(OpCodes.Box, type);
} }
il.Emit(OpCodes.Ret); il.Emit(OpCodes.Ret);
return (Func<IDataReader, object>)dm.CreateDelegate(typeof(Func<IDataReader,object>)); return (Func<IDataReader, object>)dm.CreateDelegate(typeof(Func<IDataReader, object>));
} }
private static void LoadLocal(ILGenerator il, int index) private static void LoadLocal(ILGenerator il, int index)
{ {
if(index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index");
switch(index) switch (index)
{ {
case 0: il.Emit(OpCodes.Ldloc_0); break; case 0: il.Emit(OpCodes.Ldloc_0); break;
case 1: il.Emit(OpCodes.Ldloc_1); break; case 1: il.Emit(OpCodes.Ldloc_1); break;
...@@ -2489,7 +2175,7 @@ private static void StoreLocal(ILGenerator il, int index) ...@@ -2489,7 +2175,7 @@ private static void StoreLocal(ILGenerator il, int index)
private static void LoadLocalAddress(ILGenerator il, int index) private static void LoadLocalAddress(ILGenerator il, int index)
{ {
if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index"); if (index < 0 || index >= short.MaxValue) throw new ArgumentNullException("index");
if (index <= 255) if (index <= 255)
{ {
il.Emit(OpCodes.Ldloca_S, (byte)index); il.Emit(OpCodes.Ldloca_S, (byte)index);
...@@ -2525,9 +2211,10 @@ public static void ThrowDataException(Exception ex, int index, IDataReader reade ...@@ -2525,9 +2211,10 @@ public static void ThrowDataException(Exception ex, int index, IDataReader reade
} }
} }
toThrow = new DataException(string.Format("Error parsing column {0} ({1}={2})", index, name, value), ex); toThrow = new DataException(string.Format("Error parsing column {0} ({1}={2})", index, name, value), ex);
} catch }
catch
{ // throw the **original** exception, wrapped as DataException { // throw the **original** exception, wrapped as DataException
toThrow = new DataException(ex.Message, ex); toThrow = new DataException(ex.Message, ex);
} }
throw toThrow; throw toThrow;
} }
...@@ -2566,7 +2253,7 @@ public partial class GridReader : IDisposable ...@@ -2566,7 +2253,7 @@ public partial class GridReader : IDisposable
private IDataReader reader; private IDataReader reader;
private IDbCommand command; private IDbCommand command;
private Identity identity; private Identity identity;
internal GridReader(IDbCommand command, IDataReader reader, Identity identity) internal GridReader(IDbCommand command, IDataReader reader, Identity identity)
{ {
this.command = command; this.command = command;
...@@ -2581,7 +2268,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity) ...@@ -2581,7 +2268,7 @@ internal GridReader(IDbCommand command, IDataReader reader, Identity identity)
/// </summary> /// </summary>
public IEnumerable<dynamic> Read() public IEnumerable<dynamic> Read()
{ {
return Read<DapperRow>(); return Read<FastExpando>();
} }
#endif #endif
...@@ -2686,7 +2373,7 @@ public IEnumerable<T> Read<T>() ...@@ -2686,7 +2373,7 @@ public IEnumerable<T> Read<T>()
return MultiReadInternal<TFirst, TSecond, TThird, TFourth, DontMap, TReturn>(func, splitOn); return MultiReadInternal<TFirst, TSecond, TThird, TFourth, DontMap, TReturn>(func, splitOn);
} }
#if !CSHARP30 #if !CSHARP30
/// <summary> /// <summary>
/// Read multiple objects from a single record set on the grid /// Read multiple objects from a single record set on the grid
/// </summary> /// </summary>
...@@ -2700,7 +2387,6 @@ public IEnumerable<T> Read<T>() ...@@ -2700,7 +2387,6 @@ public IEnumerable<T> Read<T>()
/// <param name="splitOn"></param> /// <param name="splitOn"></param>
/// <returns></returns> /// <returns></returns>
public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn> func, string splitOn = "id") public IEnumerable<TReturn> Read<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(Func<TFirst, TSecond, TThird, TFourth, TFifth, TReturn> func, string splitOn = "id")
{ {
return MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(func, splitOn); return MultiReadInternal<TFirst, TSecond, TThird, TFourth, TFifth, TReturn>(func, splitOn);
} }
...@@ -2789,7 +2475,7 @@ partial class ParamInfo ...@@ -2789,7 +2475,7 @@ partial class ParamInfo
/// construct a dynamic parameter bag /// construct a dynamic parameter bag
/// </summary> /// </summary>
public DynamicParameters() { } public DynamicParameters() { }
/// <summary> /// <summary>
/// construct a dynamic parameter bag /// construct a dynamic parameter bag
/// </summary> /// </summary>
...@@ -2808,7 +2494,7 @@ public DynamicParameters(object template) ...@@ -2808,7 +2494,7 @@ public DynamicParameters(object template)
#if CSHARP30 #if CSHARP30
object param object param
#else #else
dynamic param dynamic param
#endif #endif
) )
{ {
...@@ -2977,7 +2663,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) ...@@ -2977,7 +2663,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity)
} }
param.AttachedParam = p; param.AttachedParam = p;
} }
} }
} }
...@@ -3066,34 +2752,34 @@ public void AddParameter(IDbCommand command, string name) ...@@ -3066,34 +2752,34 @@ public void AddParameter(IDbCommand command, string name)
} }
} }
/// <summary> /// <summary>
/// Handles variances in features per DBMS /// Handles variances in features per DBMS
/// </summary> /// </summary>
partial class FeatureSupport partial class FeatureSupport
{ {
/// <summary> /// <summary>
/// Dictionary of supported features index by connection type name /// Dictionary of supported features index by connection type name
/// </summary> /// </summary>
private static readonly Dictionary<string, FeatureSupport> FeatureList = new Dictionary<string, FeatureSupport>(StringComparer.InvariantCultureIgnoreCase) { private static readonly Dictionary<string, FeatureSupport> FeatureList = new Dictionary<string, FeatureSupport>(StringComparer.InvariantCultureIgnoreCase) {
{"sqlserverconnection", new FeatureSupport { Arrays = false}}, {"sqlserverconnection", new FeatureSupport { Arrays = false}},
{"npgsqlconnection", new FeatureSupport {Arrays = true}} {"npgsqlconnection", new FeatureSupport {Arrays = true}}
}; };
/// <summary> /// <summary>
/// Gets the featureset based on the passed connection /// Gets the featureset based on the passed connection
/// </summary> /// </summary>
public static FeatureSupport Get(IDbConnection connection) public static FeatureSupport Get(IDbConnection connection)
{ {
string name = connection.GetType().Name; string name = connection.GetType().Name;
FeatureSupport features; FeatureSupport features;
return FeatureList.TryGetValue(name, out features) ? features : FeatureList.Values.First(); return FeatureList.TryGetValue(name, out features) ? features : FeatureList.Values.First();
} }
/// <summary> /// <summary>
/// True if the db supports array columns e.g. Postgresql /// True if the db supports array columns e.g. Postgresql
/// </summary> /// </summary>
public bool Arrays { get; set; } public bool Arrays { get; set; }
} }
/// <summary> /// <summary>
/// Represents simple memeber map for one of target parameter or property or field to source DataReader column /// Represents simple memeber map for one of target parameter or property or field to source DataReader column
...@@ -3397,33 +3083,33 @@ public partial class SqlMapper ...@@ -3397,33 +3083,33 @@ public partial class SqlMapper
public partial class DynamicParameters public partial class DynamicParameters
{ {
} }
public partial class DbString public partial class DbString
{ {
} }
public partial class SimpleMemberMap public partial class SimpleMemberMap
{ {
} }
public partial class DefaultTypeMap public partial class DefaultTypeMap
{ {
} }
public partial class CustomPropertyTypeMap public partial class CustomPropertyTypeMap
{ {
} }
public partial class FeatureSupport public partial class FeatureSupport
{ {
} }
#endif #endif
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment