diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj index f202f8f6c..40533b2d4 100644 --- a/Dapper.StrongName/Dapper.StrongName.csproj +++ b/Dapper.StrongName/Dapper.StrongName.csproj @@ -19,10 +19,12 @@ + + diff --git a/Dapper/CollectorT.cs b/Dapper/CollectorT.cs new file mode 100644 index 000000000..29d532a1f --- /dev/null +++ b/Dapper/CollectorT.cs @@ -0,0 +1,149 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dapper; + +/// +/// Allows efficient collection of data into lists, arrays, etc. +/// +/// This is a mutable struct; treat with caution. +/// +[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] +[SuppressMessage("Usage", "CA2231:Overload operator equals on overriding value type Equals", Justification = "Equality not supported")] +public struct Collector +{ + /// + /// Create a new collector using a size hint for the number of elements expected. + /// + public Collector(int capacityHint) + { + oversized = capacityHint > 0 ? ArrayPool.Shared.Rent(capacityHint) : []; + capacity = oversized.Length; + } + + /// + public readonly override string ToString() => $"Count: {count}"; + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public readonly override bool Equals([NotNullWhen(true)] object? obj) => throw new NotSupportedException(); + + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public readonly override int GetHashCode() => throw new NotSupportedException(); + + private T[] oversized; + private int count, capacity; + + /// + /// Gets the current capacity of the backing buffer of this instance. + /// + internal readonly int Capacity => capacity; + + /// + /// Gets the number of elements represented by this instance. + /// + public readonly int Count => count; + + /// + /// Gets the underlying elements represented by this instance. + /// + public readonly Span Span => new(oversized, 0, count); + + /// + /// Gets the underlying elements represented by this instance. + /// + public readonly ArraySegment ArraySegment => new(oversized, 0, count); + + /// + /// Gets the element at the specified index. + /// + public readonly ref T this[int index] + { + get + { + return ref index >= 0 & index < count ? ref oversized[index] : ref OutOfRange(); + + static ref T OutOfRange() => throw new ArgumentOutOfRangeException(nameof(index)); + } + } + + /// + /// Add an element to the collection. + /// + public void Add(T value) + { + if (capacity == count) Expand(); + oversized[count++] = value; + } + + /// + /// Add elements to the collection. + /// + public void AddRange(ReadOnlySpan values) + { + EnsureCapacity(count + values.Length); + values.CopyTo(new(oversized, count, values.Length)); + count += values.Length; + } + + private void EnsureCapacity(int minCapacity) + { + if (capacity < minCapacity) + { + var newBuffer = ArrayPool.Shared.Rent(minCapacity); + Span.CopyTo(newBuffer); + var oldBuffer = oversized; + oversized = newBuffer; + capacity = newBuffer.Length; + + if (oldBuffer is not null) + { + ArrayPool.Shared.Return(oldBuffer); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Expand() => EnsureCapacity(Math.Max(capacity * 2, 16)); + + /// + /// Release any resources associated with this instance. + /// + public void Clear() + { + count = 0; + if (capacity != 0) + { + capacity = 0; + ArrayPool.Shared.Return(oversized); + oversized = []; + } + } + + /// + /// Create an array with the elements associated with this instance, and release any resources. + /// + public T[] ToArrayAndClear() + { + T[] result = [.. Span]; // let the compiler worry about the per-platform implementation + Clear(); + return result; + } + + /// + /// Create an array with the elements associated with this instance, and release any resources. + /// + public List ToListAndClear() + { + List result = [.. Span]; // let the compiler worry about the per-platform implementation (net8+ in particular) + Clear(); + return result; + } +} diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj index af5febfb8..dfe52194f 100644 --- a/Dapper/Dapper.csproj +++ b/Dapper/Dapper.csproj @@ -26,10 +26,12 @@ + + diff --git a/Dapper/DefaultTypeMap.cs b/Dapper/DefaultTypeMap.cs index 98029741a..0e90bf329 100644 --- a/Dapper/DefaultTypeMap.cs +++ b/Dapper/DefaultTypeMap.cs @@ -51,12 +51,12 @@ internal static List GetSettableProps(Type t) return t .GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Where(p => GetPropertySetter(p, t) is not null) - .ToList(); + .AsList(); } internal static List GetSettableFields(Type t) { - return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToList(); + return t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).AsList(); } /// @@ -115,7 +115,7 @@ internal static List GetSettableFields(Type t) public ConstructorInfo? FindExplicitConstructor() { var constructors = _type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); - var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).ToList(); + var withAttr = constructors.Where(c => c.GetCustomAttributes(typeof(ExplicitConstructorAttribute), true).Length > 0).AsList(); if (withAttr.Count == 1) { diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index 456aa8bb1..022442f63 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -177,6 +177,7 @@ static Dapper.SqlMapper.AddTypeHandlerImpl(System.Type! type, Dapper.SqlMapper.I static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType) -> void static Dapper.SqlMapper.AddTypeMap(System.Type! type, System.Data.DbType dbType, bool useGetFieldValue) -> void static Dapper.SqlMapper.AsList(this System.Collections.Generic.IEnumerable? source) -> System.Collections.Generic.List! +static Dapper.SqlMapper.AsListAsync(this System.Collections.Generic.IAsyncEnumerable? source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! static Dapper.SqlMapper.AsTableValuedParameter(this System.Data.DataTable! table, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter! static Dapper.SqlMapper.AsTableValuedParameter(this System.Collections.Generic.IEnumerable! list, string? typeName = null) -> Dapper.SqlMapper.ICustomQueryParameter! static Dapper.SqlMapper.ConnectionStringComparer.get -> System.Collections.Generic.IEqualityComparer! @@ -332,4 +333,20 @@ static Dapper.SqlMapper.ThrowDataException(System.Exception! ex, int index, Syst static Dapper.SqlMapper.ThrowNullCustomQueryParameter(string! name) -> void static Dapper.SqlMapper.TypeHandlerCache.Parse(object! value) -> T? static Dapper.SqlMapper.TypeHandlerCache.SetValue(System.Data.IDbDataParameter! parameter, object! value) -> void -static Dapper.SqlMapper.TypeMapProvider -> System.Func! \ No newline at end of file +static Dapper.SqlMapper.TypeMapProvider -> System.Func! + +Dapper.Collector +Dapper.Collector.Collector() -> void +Dapper.Collector.Collector(int capacityHint) -> void +Dapper.Collector.Count.get -> int +Dapper.Collector.Span.get -> System.Span +Dapper.Collector.ArraySegment.get -> System.ArraySegment +Dapper.Collector.Clear() -> void +Dapper.Collector.Add(T value) -> void +Dapper.Collector.AddRange(System.ReadOnlySpan values) -> void +Dapper.Collector.ToListAndClear() -> System.Collections.Generic.List! +Dapper.Collector.ToArrayAndClear() -> T[]! +Dapper.Collector.this[int index].get -> T +override Dapper.Collector.ToString() -> string! +override Dapper.Collector.GetHashCode() -> int +override Dapper.Collector.Equals(object? obj) -> bool \ No newline at end of file diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index eade08cb2..799d05c14 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -447,7 +447,7 @@ private static async Task> QueryAsync(this IDbConnection cnn, if (command.Buffered) { - var buffer = new List(); + var buffer = new Collector(); var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType; while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { @@ -456,7 +456,7 @@ private static async Task> QueryAsync(this IDbConnection cnn, } while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ } command.OnCompleted(); - return buffer; + return buffer.ToListAndClear(); } else { @@ -546,6 +546,32 @@ public static Task ExecuteAsync(this IDbConnection cnn, CommandDefinition c } } + /// + /// Asynchronously collect a sequence of data into a list. + /// + /// The type of element in the list. + /// The enumerable to return as a list. + /// A to observe while waiting for the task to complete. + public static Task> AsListAsync(this IAsyncEnumerable? source, CancellationToken cancellationToken = default) + { + if (source is null) return null!; // GIGO + + return EnumerateAsync(source, cancellationToken); + + static async Task> EnumerateAsync(IAsyncEnumerable source, CancellationToken cancellationToken) + { + var buffer = new Collector(); // amortizes intermediate buffers + await using (var iterator = source.GetAsyncEnumerator(cancellationToken)) + { + while (await iterator.MoveNextAsync().ConfigureAwait(false)) + { + buffer.Add(iterator.Current); + } + } + return buffer.ToListAndClear(); + } + } + private readonly struct AsyncExecState { public readonly DbCommand Command; @@ -941,7 +967,7 @@ private static async Task> MultiMapAsync(null, CommandDefinition.ForCallback(command.Parameters, command.Flags), map, splitOn, reader, identity, true); - return command.Buffered ? results.ToList() : results; + return command.Buffered ? results.AsList() : results; } finally { @@ -989,7 +1015,7 @@ private static async Task> MultiMapAsync(this IDbC using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); using var reader = await ExecuteReaderWithFlagsFallbackAsync(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult, command.CancellationToken).ConfigureAwait(false); var results = MultiMapImpl(null, default, types, map, splitOn, reader, identity, true); - return command.Buffered ? results.ToList() : results; + return command.Buffered ? results.AsList() : results; } finally { diff --git a/Dapper/SqlMapper.GridReader.Async.cs b/Dapper/SqlMapper.GridReader.Async.cs index a32d53124..72e22f93d 100644 --- a/Dapper/SqlMapper.GridReader.Async.cs +++ b/Dapper/SqlMapper.GridReader.Async.cs @@ -229,12 +229,12 @@ private async Task> ReadBufferedAsync(int index, Func(); + var buffer = new Collector(); while (index == ResultIndex && await reader!.ReadAsync(cancel).ConfigureAwait(false)) { buffer.Add(ConvertTo(deserializer(reader))); } - return buffer; + return buffer.ToListAndClear(); } finally // finally so that First etc progresses things even when multiple rows { diff --git a/Dapper/SqlMapper.GridReader.cs b/Dapper/SqlMapper.GridReader.cs index 1370f7cc9..08ec3c645 100644 --- a/Dapper/SqlMapper.GridReader.cs +++ b/Dapper/SqlMapper.GridReader.cs @@ -202,7 +202,7 @@ private IEnumerable ReadImpl(Type type, bool buffered) cache.Deserializer = deserializer; } var result = ReadDeferred(index, deserializer.Func, type); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } private T ReadRow(Type type, Row row) @@ -283,7 +283,7 @@ private IEnumerable MultiReadInternal(Type[] types, Func Read(Func func, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } /// @@ -299,7 +299,7 @@ public IEnumerable Read(Func Read(Func func, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } /// @@ -316,7 +316,7 @@ public IEnumerable Read(Func Read(Func func, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } /// @@ -334,7 +334,7 @@ public IEnumerable Read(Func public IEnumerable Read(Func func, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } /// @@ -353,7 +353,7 @@ public IEnumerable Read Read(Func func, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } /// @@ -373,7 +373,7 @@ public IEnumerable Read Read(Func func, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(func, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } /// @@ -387,7 +387,7 @@ public IEnumerable Read Read(Type[] types, Func map, string splitOn = "id", bool buffered = true) { var result = MultiReadInternal(types, map, splitOn); - return buffered ? result.ToList() : result; + return buffered ? result.AsList() : result; } private IEnumerable ReadDeferred(int index, Func deserializer, Type effectiveType) diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index d23e949a5..a09396147 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -31,6 +31,10 @@ public static partial class SqlMapper { private class PropertyInfoByNameComparer : IComparer { + private PropertyInfoByNameComparer() { } + private static PropertyInfoByNameComparer? instance; + public static PropertyInfoByNameComparer Instance => instance ??= new(); + public int Compare(PropertyInfo? x, PropertyInfo? y) => string.CompareOrdinal(x?.Name, y?.Name); } private static int GetColumnHash(DbDataReader reader, int startBound = 0, int length = -1) @@ -533,12 +537,29 @@ public static void SetDbType(IDataParameter parameter, object value) /// /// The type of element in the list. /// The enumerable to return as a list. - public static List AsList(this IEnumerable? source) => source switch + public static List AsList(this IEnumerable? source) { - null => null!, - List list => list, - _ => Enumerable.ToList(source), - }; + return source switch + { + null => null!, // GIGO + List list => list, // already a list + ICollection col => new(col), // handled efficiently internally + _ => Enumerate(source), // use custom implementation + }; + + static List Enumerate(IEnumerable source) + { + var buffer = new Collector(); // amortizes intermediate buffers + using (var iterator = source.GetEnumerator()) + { + while (iterator.MoveNext()) + { + buffer.Add(iterator.Current); + } + } + return buffer.ToListAndClear(); + } + } /// /// Execute parameterized SQL. @@ -841,7 +862,7 @@ public static IEnumerable Query(this IDbConnection cnn, string sql, object { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var data = QueryImpl(cnn, command, typeof(T)); - return command.Buffered ? data.ToList() : data; + return command.Buffered ? data.AsList() : data; } /// @@ -950,7 +971,7 @@ public static IEnumerable Query(this IDbConnection cnn, Type type, strin if (type is null) throw new ArgumentNullException(nameof(type)); var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var data = QueryImpl(cnn, command, type); - return command.Buffered ? data.ToList() : data; + return command.Buffered ? data.AsList() : data; } /// @@ -1058,7 +1079,7 @@ public static object QuerySingle(this IDbConnection cnn, Type type, string sql, public static IEnumerable Query(this IDbConnection cnn, CommandDefinition command) { var data = QueryImpl(cnn, command, typeof(T)); - return command.Buffered ? data.ToList() : data; + return command.Buffered ? data.AsList() : data; } /// @@ -1566,7 +1587,7 @@ public static IEnumerable Query(this IDbConnection cnn, string { var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, buffered ? CommandFlags.Buffered : CommandFlags.None); var results = MultiMapImpl(cnn, command, types, map, splitOn, null, null, true); - return buffered ? results.ToList() : results; + return buffered ? results.AsList() : results; } private static IEnumerable MultiMap( @@ -1574,7 +1595,7 @@ private static IEnumerable MultiMap(cnn, command, map, splitOn, null, null, true); - return buffered ? results.ToList() : results; + return buffered ? results.AsList() : results; } private static IEnumerable MultiMapImpl(this IDbConnection? cnn, CommandDefinition command, Delegate map, string splitOn, DbDataReader? reader, Identity? identity, bool finalize) @@ -1739,7 +1760,7 @@ private static Func GenerateMapper(int length, F private static Func[] GenerateDeserializers(Identity identity, string splitOn, DbDataReader reader) { - var deserializers = new List>(); + var deserializers = new Collector>(); var splits = splitOn.Split(',').Select(s => s.Trim()).ToArray(); bool isMultiSplit = splits.Length > 1; @@ -1800,10 +1821,10 @@ private static Func[] GenerateDeserializers(Identity ident currentPos = splitPoint; } - deserializers.Reverse(); + deserializers.Span.Reverse(); } - return deserializers.ToArray(); + return deserializers.ToArrayAndClear(); } private static int GetNextSplitDynamic(int startIdx, string splitOn, DbDataReader reader) @@ -2324,7 +2345,7 @@ private static bool TryStringSplit(ref IEnumerable list, int splitAt, stri { if (list is not ICollection typed) { - typed = list.ToList(); + typed = list.AsList(); list = typed; // because we still need to be able to iterate it, even if we fail here } if (typed.Count < splitAt) return false; @@ -2404,15 +2425,15 @@ public static object SanitizeParameterValue(object? value) return value; } - private static IEnumerable FilterParameters(IEnumerable parameters, string sql) + private static PropertyInfo[] FilterParameters(IEnumerable parameters, string sql) { - var list = new List(16); + var list = new Collector(); foreach (var p in parameters) { if (Regex.IsMatch(sql, @"[?@:$]" + p.Name + @"([^\p{L}\p{N}_]+|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.CultureInvariant)) list.Add(p); } - return list; + return list.ToArrayAndClear(); } /// @@ -2524,7 +2545,7 @@ internal static IList GetLiteralTokens(string sql) var matches = CompiledRegex.LiteralTokens.Matches(sql); var found = new HashSet(StringComparer.Ordinal); - var list = new List(matches.Count); + var list = new Collector(matches.Count); foreach (Match match in matches) { string token = match.Value; @@ -2533,7 +2554,15 @@ internal static IList GetLiteralTokens(string sql) list.Add(new LiteralToken(token, match.Groups[1].Value)); } } - return list.Count == 0 ? LiteralToken.None : list; + if (list.Count == 0) + { + list.Clear(); + return LiteralToken.None; + } + else + { + return list.ToListAndClear(); + } } /// @@ -2591,7 +2620,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true il.EmitCall(OpCodes.Callvirt, typeof(IDbCommand).GetProperty(nameof(IDbCommand.Parameters))!.GetGetMethod()!, null); // stack is now [parameters] var allTypeProps = type.GetProperties(); - var propsList = new List(allTypeProps.Length); + var propsList = new Collector(allTypeProps.Length); for (int i = 0; i < allTypeProps.Length; ++i) { var p = allTypeProps[i]; @@ -2601,7 +2630,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true var ctors = type.GetConstructors(); ParameterInfo[] ctorParams; - IEnumerable? props = null; + PropertyInfo[]? props = null; // try to detect tuple patterns, e.g. anon-types, and use that to choose the order // otherwise: alphabetical if (ctors.Length == 1 && propsList.Count == (ctorParams = ctors[0].GetParameters()).Length) @@ -2619,7 +2648,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true if (ok) { // pre-sorted; the reflection gods have smiled upon us - props = propsList; + props = propsList.Span.ToArray(); } else { // might still all be accounted for; check the hard way @@ -2643,7 +2672,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } if (ok) { - props = propsList.ToArray(); + props = propsList.Span.ToArray(); Array.Sort(positions, (PropertyInfo[])props); } } @@ -2651,8 +2680,13 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } if (props is null) { - propsList.Sort(new PropertyInfoByNameComparer()); - props = propsList; +#if NET5_0_OR_GREATER + propsList.Span.Sort(PropertyInfoByNameComparer.Instance); +#else + var array = propsList.ArraySegment; + Array.Sort(array.Array, array.Offset, array.Count, PropertyInfoByNameComparer.Instance); +#endif + props = propsList.Span.ToArray(); } if (filterParams) { @@ -2872,7 +2906,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true // stack is currently [parameters] il.Emit(OpCodes.Pop); // stack is now empty - if (literals.Count != 0 && propsList is not null) + if (literals.Count != 0 && propsList.Count != 0) { il.Emit(OpCodes.Ldarg_0); // command il.Emit(OpCodes.Ldarg_0); // command, command @@ -2966,6 +3000,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } il.Emit(OpCodes.Ret); + propsList.Clear(); return (Action)dm.CreateDelegate(typeof(Action)); } @@ -3342,8 +3377,8 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataRe var nullableUnderlyingType = Nullable.GetUnderlyingType(valueTupleType); var currentValueTupleType = nullableUnderlyingType ?? valueTupleType; - var constructors = new List(); - var languageTupleElementTypes = new List(); + var constructors = new Collector(); + var languageTupleElementTypes = new Collector(); while (true) { @@ -3437,6 +3472,9 @@ private static void GenerateValueTupleDeserializer(Type valueTupleType, DbDataRe il.Emit(OpCodes.Box, valueTupleType); il.Emit(OpCodes.Ret); + + constructors.Clear(); + languageTupleElementTypes.Clear(); } private static void GenerateDeserializerFromMap(Type type, DbDataReader reader, int startBound, int length, bool returnNullIfFirstMissing, ILGenerator il) @@ -3532,7 +3570,7 @@ private static void GenerateDeserializerFromMap(Type type, DbDataReader reader, var members = (specializedConstructor is not null ? names.Select(n => typeMap.GetConstructorParameter(specializedConstructor, n)) - : names.Select(n => typeMap.GetMember(n))).ToList(); + : names.Select(n => typeMap.GetMember(n))).AsList(); // stack is now [target] bool first = true; diff --git a/Directory.Packages.props b/Directory.Packages.props index d72d03090..c0dce7e83 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,34 +8,36 @@ - - + + + + - + - - - + + + - - - - + + + + - + - - + + @@ -43,10 +45,10 @@ - + - + - + \ No newline at end of file diff --git a/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj b/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj index fa18bd9c5..dd52c4ee0 100644 --- a/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj +++ b/benchmarks/Dapper.Tests.Performance/Dapper.Tests.Performance.csproj @@ -34,7 +34,7 @@ - +