diff --git a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs
index eb80910..595282a 100644
--- a/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs
+++ b/src/Dapper.AOT.Analyzers/CodeAnalysis/DapperInterceptorGenerator.cs
@@ -579,7 +579,7 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory
if (additionalCommandState is not null && additionalCommandState.HasCommandProperties)
{
sb.Indent()
- .NewLine().Append("var cmd = TryReuse(ref Storage, sql, commandType, args);")
+ .NewLine().Append("var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);")
.NewLine().Append("if (cmd is null)").Indent()
.NewLine().Append("cmd = base.GetCommand(connection, sql, commandType, args);");
WriteCommandProperties(ctx, sb, "cmd", additionalCommandState.CommandProperties);
@@ -587,22 +587,26 @@ private static void WriteCommandFactory(in GenerateState ctx, string baseFactory
}
else
{
- sb.Indent(false).NewLine().Append(" => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false);
+ sb.Indent(false).NewLine().Append(" => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);").Outdent(false);
}
- sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);").NewLine();
+ sb.NewLine().NewLine().Append("public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);").NewLine();
if (cacheCount == 1)
{
+ sb.Append("private static readonly DbCommandCache _cmdPool = new();").NewLine();
+ sb.Append("[global::System.ThreadStatic] // note this works correctly with ref").NewLine();
sb.Append("private static global::System.Data.Common.DbCommand? Storage;").NewLine();
}
else
{
+ sb.Append("private readonly DbCommandCache _cmdPool = new(); // note: per cache instance").NewLine();
sb.Append("protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}").NewLine().NewLine();
for (int i = 0; i < cacheCount; i++)
{
sb.Append("internal sealed class Cached").Append(i).Append(" : CommandFactory").Append(index).Indent().NewLine()
.Append("protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;").NewLine()
+ .Append("[global::System.ThreadStatic] // note this works correctly with ref-return").NewLine()
.Append("private static global::System.Data.Common.DbCommand? s_Storage;").NewLine()
.Outdent().NewLine();
}
diff --git a/src/Dapper.AOT/CommandFactory.cs b/src/Dapper.AOT/CommandFactory.cs
index b4331f9..cc60a6e 100644
--- a/src/Dapper.AOT/CommandFactory.cs
+++ b/src/Dapper.AOT/CommandFactory.cs
@@ -1,5 +1,6 @@
using Dapper.Internal;
using System;
+using System.Collections.Concurrent;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
@@ -175,12 +176,53 @@ protected static void SetValueWithDefaultSize(DbParameter parameter, string? val
///
/// Provides an opportunity to recycle and reuse command instances
///
- protected static bool TryRecycle(ref DbCommand? storage, DbCommand command)
+ protected static bool TryRecycleInterlocked(ref DbCommand? storage, DbCommand command, DbCommandCache? cache = null)
{
// detach and recycle
command.Connection = null;
command.Transaction = null;
- return Interlocked.CompareExchange(ref storage, command, null) is null;
+ if (Interlocked.CompareExchange(ref storage, command, null) is null)
+ {
+ return true;
+ }
+ return cache is not null && cache.TryPut(command);
+ }
+
+ ///
+ /// Provides an opportunity to recycle and reuse command instances
+ ///
+ protected static bool TryRecycleThreadStatic(ref DbCommand? storage, DbCommand command, DbCommandCache? cache = null)
+ {
+ // detach and recycle
+ command.Connection = null;
+ command.Transaction = null;
+ if (storage is null)
+ {
+ storage = command;
+ return true;
+ }
+ return cache is not null && cache.TryPut(command);
+ }
+
+
+ ///
+ /// A simple store for command re-use.
+ ///
+ protected sealed class DbCommandCache(int capacity = 16)
+ {
+ private readonly ConcurrentQueue store = [];
+ internal bool TryPut(DbCommand command)
+ {
+ if (store.Count < capacity) // not exact - inherent race condition
+ {
+ store.Enqueue(command);
+ return true;
+ }
+ return false;
+ }
+
+ internal DbCommand? TryTake()
+ => store.TryDequeue(out var cmd) ? cmd : null;
}
}
@@ -252,9 +294,26 @@ public virtual void UpdateParameters(in UnifiedCommand command, T args)
///
/// Provides an opportunity to recycle and reuse command instances
///
- protected DbCommand? TryReuse(ref DbCommand? storage, string sql, CommandType commandType, T args)
+ protected DbCommand? TryReuseThreadStatic(ref DbCommand? storage, string sql, CommandType commandType, T args, DbCommandCache? cache = null)
+ {
+ var cmd = storage ?? cache?.TryTake();
+ storage = null;
+ if (cmd is not null)
+ {
+ // try to avoid any dirty detection in the setters
+ if (cmd.CommandText != sql) cmd.CommandText = sql;
+ if (cmd.CommandType != commandType) cmd.CommandType = commandType;
+ UpdateParameters(new(cmd), args);
+ }
+ return cmd;
+ }
+
+ ///
+ /// Provides an opportunity to recycle and reuse command instances
+ ///
+ protected DbCommand? TryReuseInterlocked(ref DbCommand? storage, string sql, CommandType commandType, T args, DbCommandCache? cache = null)
{
- var cmd = Interlocked.Exchange(ref storage, null);
+ var cmd = Interlocked.Exchange(ref storage, null) ?? cache?.TryTake();
if (cmd is not null)
{
// try to avoid any dirty detection in the setters
diff --git a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs
index f3a94ae..0786934 100644
--- a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs
+++ b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.cs
@@ -118,20 +118,23 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
- => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
+ => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private readonly DbCommandCache _cmdPool = new(); // note: per cache instance
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
internal sealed class Cached0 : CommandFactory0
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
internal sealed class Cached1 : CommandFactory0
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
@@ -165,9 +168,11 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
- => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
+ => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private static readonly DbCommandCache _cmdPool = new();
+ [global::System.ThreadStatic] // note this works correctly with ref
private static global::System.Data.Common.DbCommand? Storage;
}
diff --git a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs
index f3a94ae..0786934 100644
--- a/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs
+++ b/test/Dapper.AOT.Test/Interceptors/CacheCommand.output.netfx.cs
@@ -118,20 +118,23 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
- => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
+ => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private readonly DbCommandCache _cmdPool = new(); // note: per cache instance
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
internal sealed class Cached0 : CommandFactory0
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
internal sealed class Cached1 : CommandFactory0
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
@@ -165,9 +168,11 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
- => TryReuse(ref Storage, sql, commandType, args) ?? base.GetCommand(connection, sql, commandType, args);
+ => TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool) ?? base.GetCommand(connection, sql, commandType, args);
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private static readonly DbCommandCache _cmdPool = new();
+ [global::System.ThreadStatic] // note this works correctly with ref
private static global::System.Data.Common.DbCommand? Storage;
}
diff --git a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs
index c57fae0..fd44a52 100644
--- a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs
+++ b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.cs
@@ -199,7 +199,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
{
- var cmd = TryReuse(ref Storage, sql, commandType, args);
+ var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
if (cmd is null)
{
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -212,7 +212,9 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
return cmd;
}
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private readonly DbCommandCache _cmdPool = new();
+ [global::System.ThreadStatic] // note this works correctly with ref
private static global::System.Data.Common.DbCommand? Storage;
}
@@ -256,7 +258,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
{
- var cmd = TryReuse(ref Storage, sql, commandType, args);
+ var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
if (cmd is null)
{
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -269,18 +271,21 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
return cmd;
}
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private readonly DbCommandCache _cmdPool = new();
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
internal sealed class Cached0 : CommandFactory1
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
internal sealed class Cached1 : CommandFactory1
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
diff --git a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs
index c57fae0..fd44a52 100644
--- a/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs
+++ b/test/Dapper.AOT.Test/Interceptors/CommandProperties.output.netfx.cs
@@ -199,7 +199,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
{
- var cmd = TryReuse(ref Storage, sql, commandType, args);
+ var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
if (cmd is null)
{
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -212,7 +212,9 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
return cmd;
}
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private readonly DbCommandCache _cmdPool = new();
+ [global::System.ThreadStatic] // note this works correctly with ref
private static global::System.Data.Common.DbCommand? Storage;
}
@@ -256,7 +258,7 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
public override global::System.Data.Common.DbCommand GetCommand(global::System.Data.Common.DbConnection connection,
string sql, global::System.Data.CommandType commandType, object? args)
{
- var cmd = TryReuse(ref Storage, sql, commandType, args);
+ var cmd = TryReuseThreadStatic(ref Storage, sql, commandType, args, _cmdPool);
if (cmd is null)
{
cmd = base.GetCommand(connection, sql, commandType, args);
@@ -269,18 +271,21 @@ public override void UpdateParameters(in global::Dapper.UnifiedCommand cmd, obje
return cmd;
}
- public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycle(ref Storage, command);
+ public override bool TryRecycle(global::System.Data.Common.DbCommand command) => TryRecycleThreadStatic(ref Storage, command, _cmdPool);
+ private readonly DbCommandCache _cmdPool = new();
protected abstract ref global::System.Data.Common.DbCommand? Storage {get;}
internal sealed class Cached0 : CommandFactory1
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}
internal sealed class Cached1 : CommandFactory1
{
protected override ref global::System.Data.Common.DbCommand? Storage => ref s_Storage;
+ [global::System.ThreadStatic] // note this works correctly with ref-return
private static global::System.Data.Common.DbCommand? s_Storage;
}