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
.NET framework support
Be advised that .NET 8 and .NET 9 reach end of support in November 2026. Support for both will be dropped
in Dapr v1.20; only .NET 10 and .NET 11 will be supported through the end of 2027.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.
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.
| Operation | Result |
|---|
CreateCollectionAsync | Create a collection with positive uint dimensions and an optional distance metric. |
GetCollectionAsync | VectorCollection with Collection, approximate RecordCount, Properties, Dimensions, and effective Metric. |
ListCollectionsAsync | Collection names in the component. |
DeleteCollectionAsync | Delete 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. |
DeleteVectorsAsync | IndexAck 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
DistanceMetric | Interpretation |
|---|
Unspecified | Component default at collection creation; collection metric when querying. |
Cosine | Cosine similarity in [-1, 1]; higher is closer. |
DotProduct | Unnormalized dot product; higher is closer. |
Euclidean | Euclidean 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 property | Behavior |
|---|
Vector | Non-empty IReadOnlyList<float> query embedding. Mutually exclusive with ById. |
ById | Non-empty stored record ID whose embedding is used as the query. Exactly one query source is required. |
TopK | Nullable uint maximum match count; null or zero uses the component default. |
Filter | Portable JSON-object filter addressing record metadata, including dotted paths. |
IncludeValues | Null or false omits embedding values; returned Values is empty. |
IncludePayload | Null or false omits payloads; returned Payload is default(T). |
Metric | Requested metric, defaulting to Unspecified. |
ScoreThreshold | Optional 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.