Is your feature request related to a problem? Please describe.
SqlParameter.Value only accepts a handful of hardcoded CLR representations for
Binary/VarBinary/Image parameters. Tracing SqlParameter.CoerceValue (decompiled from
Microsoft.Data.SqlClient 6.0.2): when the destination MetaType.ClassType is typeof(byte[]),
the method special-cases exactly byte[], Stream (via StreamDataFeed), SqlBytes, and
SqlBinary. Anything else falls through to the generic fallback:
value = Convert.ChangeType(value, destinationType.ClassType, null);
Memory<byte> and ReadOnlyMemory<byte> don't implement IConvertible, so this throws
InvalidCastException("Object must implement IConvertible."), which ADP.ParameterConversionFailed
re-wraps as:
InvalidCastException: Failed to convert parameter value from a ReadOnlyMemory`1 to a Byte[].
This forces any caller already holding binary data as Memory<byte>/ReadOnlyMemory<byte> — e.g.
a slice of a larger buffer, or a rented ArrayPool<byte> segment — to call .ToArray() first,
which is exactly the extra allocation + copy that Memory<T> exists to avoid. There's effectively
no way to bind a byte-slice parameter today without either allocating a full new array or holding
the whole surrounding buffer as a full-size byte[] from the start.
The read side has the same shape of gap: SqlDataReader always materializes and returns binary
columns as a freshly allocated byte[] (via GetValue/GetFieldValue<byte[]>); there's no way to
read a varbinary/binary/image column directly into a caller-owned buffer without an extra copy.
Describe the solution you'd like
Write side: recognize Memory<byte> and ReadOnlyMemory<byte> in SqlParameter.CoerceValue
(and the MetaType/GetMetaTypeFromValue dispatch) as additional carriers for
Binary/VarBinary/Image, alongside the existing byte[]/Stream/SqlBytes/SqlBinary
special-cases — mirroring how Stream is already handled instead of going through
Convert.ChangeType. (Note: Span<byte> is a ref struct and can never be boxed into an
object-typed property, so it structurally cannot participate in SqlParameter.Value — this
request is scoped to Memory<byte>/ReadOnlyMemory<byte> for the write side.)
Read side: add a Span<byte>-based fill API for binary columns (a ref struct parameter is
fine here, since it's a method argument, not a stored property) — something like:
int SqlDataReader.GetBytes(int ordinal, long dataIndex, Span<byte> buffer);
analogous to Stream.Read(Span<byte>), so a caller can read directly into a pooled/stack buffer
without forcing a fresh byte[] allocation per row.
Describe alternatives you've considered
- Calling
.ToArray()/.Span.ToArray() before assigning SqlParameter.Value. Works today,
but is exactly the per-call allocation + copy that holding data as Memory<byte> (from an
ArrayPool<byte> rental or a slice of a larger buffer) was meant to avoid.
- Using
Stream instead (already supported via StreamDataFeed). Reasonable for genuinely
large values, but is heavyweight and async-oriented for small, already-in-memory buffers that
just happen to be Memory<byte> rather than byte[].
- Passing the underlying
byte[] plus a separate offset/length pair. Defeats the point of a
self-describing Memory<byte> slice and reintroduces manual bookkeeping the Memory<T> API
exists to eliminate.
Additional context
- A closely related request already exists for the SQLite provider:
dotnet/efcore#37484 — "Support Memory and
ReadOnlyMemory parameter binding in Microsoft.Data.Sqlite," motivated by the same
array-pool/slicing scenario ("what if the desired blob to bind is not at the beginning of the
array... you want to slice a byte array into multiple columns"). Parity across ADO.NET providers
would help code that targets more than one backend.
- Minimal repro of the current failure:
byte[] backing = [1, 2, 3, 4, 5];
ReadOnlyMemory<byte> slice = backing.AsMemory(1, 3);
using var command = connection.CreateCommand();
command.CommandText = "INSERT INTO T (Col) VALUES (@p)";
command.Parameters.Add(new SqlParameter("@p", SqlDbType.VarBinary) { Value = slice });
command.ExecuteNonQuery(); // InvalidCastException: Failed to convert parameter value from a ReadOnlyMemory`1 to a Byte[].
- Even just the write-side change (
Memory<byte>/ReadOnlyMemory<byte> as SqlParameter.Value)
would already remove the most common pain point; the read-side Span<byte> fill API is a
separate, larger ask and could be tracked independently if that's preferred.
Is your feature request related to a problem? Please describe.
SqlParameter.Valueonly accepts a handful of hardcoded CLR representations forBinary/VarBinary/Imageparameters. TracingSqlParameter.CoerceValue(decompiled fromMicrosoft.Data.SqlClient6.0.2): when the destinationMetaType.ClassTypeistypeof(byte[]),the method special-cases exactly
byte[],Stream(viaStreamDataFeed),SqlBytes, andSqlBinary. Anything else falls through to the generic fallback:Memory<byte>andReadOnlyMemory<byte>don't implementIConvertible, so this throwsInvalidCastException("Object must implement IConvertible."), whichADP.ParameterConversionFailedre-wraps as:
This forces any caller already holding binary data as
Memory<byte>/ReadOnlyMemory<byte>— e.g.a slice of a larger buffer, or a rented
ArrayPool<byte>segment — to call.ToArray()first,which is exactly the extra allocation + copy that
Memory<T>exists to avoid. There's effectivelyno way to bind a byte-slice parameter today without either allocating a full new array or holding
the whole surrounding buffer as a full-size
byte[]from the start.The read side has the same shape of gap:
SqlDataReaderalways materializes and returns binarycolumns as a freshly allocated
byte[](viaGetValue/GetFieldValue<byte[]>); there's no way toread a
varbinary/binary/imagecolumn directly into a caller-owned buffer without an extra copy.Describe the solution you'd like
Write side: recognize
Memory<byte>andReadOnlyMemory<byte>inSqlParameter.CoerceValue(and the
MetaType/GetMetaTypeFromValuedispatch) as additional carriers forBinary/VarBinary/Image, alongside the existingbyte[]/Stream/SqlBytes/SqlBinaryspecial-cases — mirroring how
Streamis already handled instead of going throughConvert.ChangeType. (Note:Span<byte>is aref structand can never be boxed into anobject-typed property, so it structurally cannot participate inSqlParameter.Value— thisrequest is scoped to
Memory<byte>/ReadOnlyMemory<byte>for the write side.)Read side: add a
Span<byte>-based fill API for binary columns (aref structparameter isfine here, since it's a method argument, not a stored property) — something like:
analogous to
Stream.Read(Span<byte>), so a caller can read directly into a pooled/stack bufferwithout forcing a fresh
byte[]allocation per row.Describe alternatives you've considered
.ToArray()/.Span.ToArray()before assigningSqlParameter.Value. Works today,but is exactly the per-call allocation + copy that holding data as
Memory<byte>(from anArrayPool<byte>rental or a slice of a larger buffer) was meant to avoid.Streaminstead (already supported viaStreamDataFeed). Reasonable for genuinelylarge values, but is heavyweight and async-oriented for small, already-in-memory buffers that
just happen to be
Memory<byte>rather thanbyte[].byte[]plus a separate offset/length pair. Defeats the point of aself-describing
Memory<byte>slice and reintroduces manual bookkeeping theMemory<T>APIexists to eliminate.
Additional context
dotnet/efcore#37484 — "Support Memory and
ReadOnlyMemory parameter binding in Microsoft.Data.Sqlite," motivated by the same
array-pool/slicing scenario ("what if the desired blob to bind is not at the beginning of the
array... you want to slice a byte array into multiple columns"). Parity across ADO.NET providers
would help code that targets more than one backend.
Memory<byte>/ReadOnlyMemory<byte>asSqlParameter.Value)would already remove the most common pain point; the read-side
Span<byte>fill API is aseparate, larger ask and could be tracked independently if that's preferred.