Skip to content

Support Memory<byte>/ReadOnlyMemory<byte> as SqlParameter.Value for binary/varbinary/image parameters #4491

Description

@StefanGreve

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Approval NeededIssues/PRs that require approval from the maintainers before changes will be accepted.Design Review NeededPartner Approval Needed 🤝Issues/PRs that require approval/feedback from partner teamsPublic API 🆕Issues/PRs that introduce new APIs to the driver.

    Type

    No type

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions