This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr Vector .NET SDK

Store embeddings and run similarity queries using the Dapr Vector .NET SDK

The Dapr.Vector package provides a dedicated .NET client for Dapr’s Vector building block. It supports collection management, vector upserts and retrieval, typed or opaque payloads, metadata filters, query-by-vector or query-by-ID, score thresholds, and ordered batch query results. A bundled source generator can create a typed client bound to a component, collection, and payload type.

These guides target Dapr .NET SDK 1.19.0 or later and Dapr runtime 1.19 or later.

Start with How to: Store and query vectors. For configuration, distance metrics, batch errors, serialization, and generated clients, see the Vector client guidance.

Your application must supply precomputed embeddings; this client does not generate them. For lexical/full-text search over JSON documents, use the Search .NET SDK.

1 - How to: Store and query vectors with the Dapr .NET SDK

Create collections, store typed vector records, query embeddings, and handle batch results using Dapr.Vector

Use DaprVectorClient to store precomputed embeddings and retrieve similar records through a Dapr vector component. This guide uses three-dimensional vectors for readability; real embeddings must match your model’s dimensions.

Prerequisites

Install the package in an ASP.NET Core project:

dotnet add package Dapr.Vector

The Vector SDK overview describes the alpha API and release requirements.

Configure a vector component

Create components\vectorstore.yaml in your application directory:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: vectorstore
spec:
  type: vector.in-memory
  version: v1
  metadata:
  - name: indexingDelay
    value: "0s"

The in-memory component needs no external database. Data is lost when the sidecar stops and is not shared between sidecars. Use a supported durable vector component for production.

Register and resolve the client

Add the following to Program.cs:

using Dapr.Vector;
using Dapr.Vector.Extensions;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprVectorClient();

var app = builder.Build();
var client = app.Services.GetRequiredService<DaprVectorClient>();

Place the remaining operation snippets after this code, then call app.Run(); at the end of Program.cs. Create the payload type in Product.cs:

public sealed record Product(string Title);

Run the application with a sidecar:

dapr run --app-id vector-example --resources-path .\components -- dotnet run

The client reads its endpoint and API token from configuration or the environment and uses gRPC. It is separate from DaprClient and does not require AddDaprClient().

Create and inspect a collection

const string storeName = "vectorstore";
const string collectionName = "products";

await client.CreateCollectionAsync(
    storeName, collectionName, dimensions: 3, metric: DistanceMetric.Cosine);

var info = await client.GetCollectionAsync(storeName, collectionName);
Console.WriteLine($"{info.Collection}: {info.Dimensions} dimensions, {info.Metric}");
Console.WriteLine($"Approximately {info.RecordCount} records");

var collections = await client.ListCollectionsAsync(storeName);
Console.WriteLine(string.Join(", ", collections));

Dimensions must be positive and match every stored embedding. Creating an existing collection fails with AlreadyExists; it does not reconfigure the collection. Delete the example collection before rerunning against a sidecar that still holds it.

Upsert records with typed payloads

var writeOptions = new IndexingOptions
{
    Mode = IndexingMode.WaitForCompletion,
    WaitTimeout = TimeSpan.FromSeconds(10),
    OnWaitTimeout = IndexingWaitTimeoutAction.FailRequest
};

var attributes = JsonSerializer.Deserialize<JsonElement>("""{"category":"books"}""");
var write = await client.UpsertVectorsAsync<Product>(
    storeName,
    collectionName,
    [
        new("dapr", [1, 0, 0], new Product("Dapr guide"), attributes),
        new("distributed", [0.8f, 0.6f, 0], new Product("Distributed systems"), attributes),
        new("other", [0, 1, 0], new Product("Other topics"), attributes)
    ],
    writeOptions);

foreach (var failure in write.FailedItems)
{
    Console.Error.WriteLine($"{failure.Id}: {failure.ErrorCode} {failure.ErrorMessage}");
}

if (write.FailedItems.Count != 0 || write.Ack != IndexAck.Completed)
{
    throw new InvalidOperationException("Not all vector records were confirmed written.");
}

VectorRecord<T> has an Id, IReadOnlyList<float> embedding Values, optional typed Payload, and optional JSON-object Metadata. IDs must be non-empty and unique within the request. Upserting an existing ID replaces the stored record.

Metadata is structured and filterable; payloads are opaque to the vector component and are not filterable. This differs from Search document metadata, which is not indexed. Other than byte[], payload types are serialized with the client’s JSON options.

Without explicit options, writes can return IndexAck.Queued before records are queryable. Waiting for completion makes the following queries see completed writes, but you must still check FailedItems. Invalid vector dimensions or values can fail individual records while the rest succeed.

Query by embedding

var query = new VectorQuery
{
    Vector = [1, 0, 0],
    TopK = 2,
    Filter = JsonSerializer.Deserialize<JsonElement>("""{"category":{"$eq":"books"}}"""),
    ScoreThreshold = 0.75,
    IncludePayload = true,
    IncludeValues = true
};

var result = await client.QueryVectorsAsync<Product>(storeName, collectionName, query);

foreach (var match in result.Matches)
{
    Console.WriteLine($"{match.Record.Id}: {match.Record.Payload?.Title}, " +
        $"{result.Metric} score {match.Score}");
}

VectorQuery.Vector is directly a list of floats, not a VectorRecord<T>. For cosine similarity, higher scores are closer, and the inclusive threshold keeps scores at least 0.75. See the metric guidance for dot product and Euclidean semantics.

Values and payloads are omitted by default on queries. Set IncludeValues and IncludePayload separately as needed.

Query by stored ID and in batches

Use ById instead of Vector to reuse an existing record’s embedding:

var similar = await client.QueryVectorsAsync<Product>(
    storeName, collectionName,
    new VectorQuery { ById = "dapr", TopK = 2, IncludePayload = true });

Exactly one of Vector and ById is required. Batch queries target the same collection and return one outcome per query in request order:

var batch = await client.BatchQueryVectorsAsync<Product>(
    storeName, collectionName,
    [
        query,
        new() { ById = "dapr", TopK = 1, IncludePayload = true },
        new() { ById = "missing", TopK = 1 }
    ]);

for (var i = 0; i < batch.Count; i++)
{
    if (batch[i].Error is { } error)
    {
        Console.Error.WriteLine($"Query {i}: {error.Code} {error.Message}");
    }
    else if (batch[i].Response is { } response)
    {
        Console.WriteLine($"Query {i}: {response.Matches.Count} matches ({response.Metric})");
    }
}

With the in-memory component, the missing ID produces an error in that query’s slot without discarding the successful results. Invalid client-side query arguments, such as setting neither Vector nor ById, throw before any query in the batch is sent. Request-wide RPC failures also throw instead of producing per-query errors.

Retrieve and delete records

var records = await client.GetVectorsAsync<Product>(
    storeName, collectionName, ["dapr", "missing"], includeValues: true);

foreach (var record in records)
{
    Console.WriteLine($"{record.Id}: {record.Payload?.Title}, {record.Values.Count} values");
}

var ack = await client.DeleteVectorsAsync(
    storeName, collectionName, ["distributed"], writeOptions);

if (ack != IndexAck.Completed)
{
    throw new InvalidOperationException("Vector deletion was not confirmed complete.");
}

await client.DeleteCollectionAsync(storeName, collectionName);

Get returns found records in request order and omits missing IDs. Payloads are returned when present; values are omitted unless includeValues is true. There is no includePayload argument on GetVectorsAsync<T>. Deleting a missing record ID is not an error. Deleting a collection removes all its records.

Next steps

2 - Dapr Vector client

Configure DaprVectorClient, interpret similarity scores, handle batch results, and generate typed collection clients

DaprVectorClient in Dapr.Vector implements the alpha Vector API through gRPC. Use .NET SDK 1.19.0 or later with a Dapr runtime and component that support Vector. See the overview for release requirements and the how-to for a complete collection lifecycle.

Client lifetime and configuration

Reuse a long-lived client. Dependency injection registers a singleton by default and disposes it when the container is disposed:

using Dapr.Vector.Extensions;

builder.Services.AddDaprVectorClient((serviceProvider, clientBuilder) =>
{
    clientBuilder.UseGrpcEndpoint("http://localhost:50001");
});

The callback can resolve configuration or other services through serviceProvider. The optional lifetime argument supports other ServiceLifetime values.

Without dependency injection, create and dispose your own instance:

using Dapr.Vector;

using var client = new DaprVectorClientBuilder()
    .UseGrpcEndpoint("http://localhost:50001")
    .Build();

The builder accepts an optional IConfiguration. Defaults use DAPR_GRPC_ENDPOINT, or the local endpoint with DAPR_GRPC_PORT (50001 by default), and DAPR_API_TOKEN for sidecar authentication. Explicit builder settings override defaults; use UseDaprApiToken to override the token.

Use UseJsonSerializationOptions for payload serialization and UseGrpcChannelOptions for gRPC settings. Retain ThrowOperationCanceledOnCancellation = true when replacing channel options. Shared HTTP endpoint, HTTP client factory, and HTTP timeout settings do not switch Vector operations to HTTP or establish a per-RPC deadline. Bound caller wait time with a time-limited CancellationToken, not UseTimeout.

API coverage

Every operation accepts optional component-specific request metadata (IReadOnlyDictionary<string, string>) and a CancellationToken.

OperationResult
CreateCollectionAsyncCreate a collection with positive uint dimensions and an optional distance metric.
GetCollectionAsyncVectorCollection with Collection, approximate RecordCount, Properties, Dimensions, and effective Metric.
ListCollectionsAsyncCollection names in the component.
DeleteCollectionAsyncDelete the collection and its records.
UpsertVectorsAsync<T>UpsertVectorsResult with Ack and FailedItems.
GetVectorsAsync<T>Found VectorRecord<T> entries in request order; includeValues defaults to false.
DeleteVectorsAsyncIndexAck for the deletion.
QueryVectorsAsync<T>VectorQueryResult<T> with Matches (a list of VectorMatch<T>, each exposing the Record and its Score) and the effective Metric.
BatchQueryVectorsAsync<T>Ordered VectorBatchQueryResult<T> entries, each containing a response or error.

Request metadata configures an operation and is separate from each record’s structured, filterable Metadata. There is no embedding-generation API, hybrid lexical/vector query, or continuation-token pagination in this alpha client. Generate embeddings in your application and use Search separately for lexical queries.

Distance metrics and query options

DistanceMetricInterpretation
UnspecifiedComponent default at collection creation; collection metric when querying.
CosineCosine similarity in [-1, 1]; higher is closer.
DotProductUnnormalized dot product; higher is closer.
EuclideanEuclidean distance; lower is closer.

ScoreThreshold is inclusive: cosine and dot-product matches must score at least the threshold; Euclidean matches must score at most it. Scores are not converted to a normalized relevance percentage. Interpret each response using its effective Metric. A query can request a metric override where the component supports it; unsupported metrics are rejected rather than silently substituted.

VectorQuery propertyBehavior
VectorNon-empty IReadOnlyList<float> query embedding. Mutually exclusive with ById.
ByIdNon-empty stored record ID whose embedding is used as the query. Exactly one query source is required.
TopKNullable uint maximum match count; null or zero uses the component default.
FilterPortable JSON-object filter addressing record metadata, including dotted paths.
IncludeValuesNull or false omits embedding values; returned Values is empty.
IncludePayloadNull or false omits payloads; returned Payload is default(T).
MetricRequested metric, defaulting to Unspecified.
ScoreThresholdOptional inclusive cutoff in the metric’s own scale.

Filters support $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $and, $or, and $not. Multiple conditions in the same object are ANDed. For example:

using System.Text.Json;
using Dapr.Vector;

var query = new VectorQuery
{
    Vector = [1, 0, 0],
    Filter = JsonSerializer.Deserialize<JsonElement>(
        """{"category":"books","dealer.city":{"$in":["Seattle","Portland"]}}"""),
    TopK = 10,
    Metric = DistanceMetric.Cosine,
    IncludePayload = true
};

The filter addresses record metadata, not payload properties. Metadata and filters must be JSON objects. See the component specifications for restrictions and the filter language.

Payload serialization

VectorRecord<T> stores an optional payload separately from its embedding and metadata. Most payload types use System.Text.Json with JsonSerializerDefaults.Web by default; the client deserializes returned payloads to the T selected by your read or query call. Use a compatible type, or JsonElement for variable JSON shapes.

For opaque binary payloads, use byte[] as the generic type. The SDK transfers these bytes unchanged instead of JSON/base64-serializing them:

var write = await client.UpsertVectorsAsync<byte[]>(
    "vectorstore", "products",
    [new("binary", [1, 0, 0], new byte[] { 0, 255, 128 })],
    new IndexingOptions
    {
        Mode = IndexingMode.WaitForCompletion,
        WaitTimeout = TimeSpan.FromSeconds(10),
        OnWaitTimeout = IndexingWaitTimeoutAction.FailRequest
    });

foreach (var failure in write.FailedItems)
{
    Console.Error.WriteLine($"{failure.Id}: {failure.ErrorMessage}");
}

if (write.FailedItems.Count != 0 || write.Ack != IndexAck.Completed)
{
    throw new InvalidOperationException("The binary record was not confirmed written.");
}

var records = await client.GetVectorsAsync<byte[]>(
    "vectorstore", "products", ["binary"], includeValues: true);

Read the bytes back with GetVectorsAsync<byte[]> or QueryVectorsAsync<byte[]> (with IncludePayload = true). An absent binary payload is null, while a present empty payload remains an empty byte array. This raw-byte behavior applies specifically to T = byte[], not arbitrary memory types or a byte[] passed as object.

For source-generated JSON serialization, configure a context covering your payload type, such as the Product record from the how-to:

using System.Text.Json.Serialization;
using Dapr.Vector.Extensions;

builder.Services.AddDaprVectorClient((_, clientBuilder) =>
    clientBuilder.UseJsonSerializationOptions(ProductJsonContext.Default.Options));

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(Product))]
internal partial class ProductJsonContext : JsonSerializerContext
{
}

The typed collection generator does not generate JSON serialization metadata. Configure it separately for reflection-free payload serialization; opaque byte[] payloads bypass the JSON serializer.

Write acknowledgements and partial failures

Vector upserts and record deletions accept Dapr.Vector.IndexingOptions. These have the same behavior as the Search write options, but are distinct types in the Dapr.Vector namespace.

Omitted options use ReturnOnAcceptance. IndexAck.Queued means work was accepted, not that it succeeded or is queryable. WaitForCompletion requires a positive WaitTimeout and explicit OnWaitTimeout. ContinueAsync returns Queued if the wait expires and requires queued-ack support; FailRequest raises DeadlineExceeded, but provider work may still complete. Wait fields are invalid with acceptance mode.

The wait timeout must fit below any remaining RPC deadline and leave at least 10% and at least 100 ms of headroom below the store resiliency timeout. It is a provider wait limit, not a cancellation deadline for your application.

Inspect both UpsertVectorsResult.Ack and FailedItems. Each VectorFailedItem includes Id, canonical numeric ErrorCode, ErrorMessage, and raw protobuf error Details (TypeUrl and Value). A completed batch can have individual failures. The SDK leaves dimension and value validation to the runtime/component so invalid records can fail individually without discarding valid records. At a queued boundary only known failures are returned, and this client has no final-result polling API.

If you use Search and Vector together, qualify or alias IndexingOptions, IndexingMode, IndexingWaitTimeoutAction, and IndexAck to avoid ambiguous names.

Batch query errors

BatchQueryVectorsAsync<T> submits queries against one component and collection with shared request metadata. Results preserve query order and have exactly one populated property:

  • Response: a VectorQueryResult<T> with matches and effective metric.
  • Error: a VectorQueryError with canonical numeric Code, Message, and preserved protobuf Details.

Check each outcome, even if other queries succeeded. The batch example demonstrates both outcomes.

Local argument validation occurs for the entire batch before sending the RPC. A query with neither source, both sources, an empty vector, a blank ID, a non-object filter, or an invalid metric throws instead of becoming a per-query result. A valid query that fails during runtime evaluation, such as a missing stored ID, can return an error in its own slot. Request-wide RPC failures throw DaprException.

Generate a typed collection client

The Dapr.Vector package bundles a source generator. Define a top-level, non-generic, non-file-local partial interface extending exactly one IDaprVectorCollection<T> with no additional abstract members:

using Dapr.Vector;

[VectorCollection("vectorstore", "products")]
public partial interface IProductVectors : IDaprVectorCollection<Product>
{
}

The component and collection names must be non-empty. Register the generated wrapper after the underlying client:

builder.Services.AddDaprVectorClient().AddProductVectors();

AddProductVectors is named after the interface with the leading I removed. Inject the interface to query without repeating the component, collection, or payload type:

public sealed class RecommendationService(IProductVectors products)
{
    public Task<VectorQueryResult<Product>> FindAsync(
        IReadOnlyList<float> embedding, CancellationToken cancellationToken) =>
        products.QueryVectorsAsync(
            new VectorQuery { Vector = embedding, TopK = 5, IncludePayload = true },
            cancellationToken: cancellationToken);
}

The wrapper exposes all collection-scoped operations, including creation (which still requires dimensions and accepts an optional metric), writes, reads, single/batch queries, and deletion. Store-wide ListCollectionsAsync remains on DaprVectorClient. The wrapper forwards metadata, options, and cancellation, inherits the client’s registered lifetime, and does not dispose that client. Registration alone does not create a collection.

Invalid declarations, bindings, or contracts produce build errors DAPR1901, DAPR1902, or DAPR1903, respectively.

Cancellation and errors

RPC failures throw DaprException whose InnerException is the original Grpc.Core.RpcException; inspect its StatusCode and trailers for provider/runtime errors. Argument validation and JSON serialization errors are distinct from RPC failures. Batch query errors and upsert item failures are returned in their result models, not thrown as request-wide errors.

Every operation accepts a CancellationToken. Caller cancellation can throw OperationCanceledException, but does not guarantee the database stopped processing a write. Configure retries and timeout policies through Dapr resiliency.