Vespa.NET.Testcontainers 2.0.0

dotnet add package Vespa.NET.Testcontainers --version 2.0.0
                    
NuGet\Install-Package Vespa.NET.Testcontainers -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Vespa.NET.Testcontainers" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Vespa.NET.Testcontainers" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Vespa.NET.Testcontainers" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Vespa.NET.Testcontainers --version 2.0.0
                    
#r "nuget: Vespa.NET.Testcontainers, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Vespa.NET.Testcontainers@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Vespa.NET.Testcontainers&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Vespa.NET.Testcontainers&version=2.0.0
                    
Install as a Cake Tool

<div align="center">

Vespa.NET

The strongly-typed .NET SDK for Vespa.ai.

NuGet .NET 10 .NET 8 License: MIT

</div>


// Define your model → deploy schema → search. That's it.
await client.Admin.DeploySchemaAsync<Product>();
await client.Documents.PutAsync(product.Id, product);
var results = await client.Search.NearestNeighborSearchAsync<Product>(embedding, p => p.Embedding, topK: 10);

Why Vespa.NET?

Without Vespa.NET With Vespa.NET
Write .sd schema files by hand [VespaDocument] + [VespaField] on your C# model
Build ZIP packages, POST to config server await client.Admin.DeploySchemaAsync<Product>()
Construct YQL strings with string concatenation Fluent YqlBuilder<T> with lambda field selectors
Manual HTTP calls, JSON parsing, error handling Typed SearchAsync<T>, GetAsync<T>, BulkPutAsync<T>
Roll your own retry, circuit breaker, metrics Built-in Polly resilience + OpenTelemetry out of the box

Features

Schema Code-first .sd generation from attributes, multi-type deploy, custom services.xml
Documents CRUD, conditional writes, field-level ops, visit/iterate, group/number addressing (read+write), selection-based update/delete/copy with continuation auto-loop + crash-safe manual paging
Search Full-text, nearest-neighbor, hybrid, streaming, auto-paginated IAsyncEnumerable
YQL Fluent type-safe builder with boolean composition, grouping, ranking DSL
Feed Parallel /document/v1 pipeline with Channel<T> backpressure, progress callbacks
Multi-tenant [VespaExtraFields] catch-all for dynamic fields, configurable tenant
Cloud mTLS (PEM, PFX, in-memory cert), Bearer token auth
Ops OpenTelemetry traces + metrics, ASP.NET Core health checks, Testcontainers
Performance HTTP/2 multiplexing, connection pooling, GZip/Deflate, ResponseHeadersRead
Resilience Retry + circuit breaker via Polly, zero-allocation [LoggerMessage] logging

Quick Start

1. Install

dotnet add package Vespa.NET

2. Define a model

[VespaDocument("product", Namespace = "myapp")]
public record Product
{
    [VespaId]
    public string Id { get; init; } = "";

    [VespaField(Name = "product_name", IndexingMode = IndexingMode.IndexAttributeSummary)]
    public string Name { get; init; } = "";

    [VespaField(Name = "price", IndexingMode = IndexingMode.AttributeSummary)]
    public decimal Price { get; init; }

    [VespaTensor("tensor<float>(x[128])", EnableIndex = true, DistanceMetric = DistanceMetric.Euclidean)]
    public VespaTensor Embedding { get; init; } = null!;
}
var options = new VespaClientOptions
{
    Endpoint         = "http://localhost:8080",
    DefaultNamespace = "myapp"
};

using var httpClient = new HttpClient { BaseAddress = new Uri(options.Endpoint) };
using var client     = new VespaClient(httpClient, options);

// Deploy schema from C# attributes
await client.Admin.DeploySchemaAsync<Product>();

// Index a document
var product = new Product { Id = "p-1", Name = "Laptop", Price = 999.99m, Embedding = embeddings };
await client.Documents.PutAsync(product.Id, product);

// Search
var results = await client.Search.NearestNeighborSearchAsync<Product>(
    queryEmbedding, p => p.Embedding, topK: 10);

Tip: With [VespaDocument] on your model, documentType and namespace are inferred automatically in all operations. No strings needed.


Model-Aware API

When your model has [VespaDocument], all operations infer types automatically. [VespaField(Name)] drives both schema generation and JSON serialization.

// CRUD — no documentType needed
await client.Documents.PutAsync(product.Id, product);
var doc = await client.Documents.GetAsync<Product>(product.Id);
await client.Documents.DeleteAsync<Product>(product.Id);

// Field-level update with typed lambda builder
await client.Documents.UpdateFieldsAsync<Product>(product.Id, ops => ops
    .Field(p => p.Name, FieldOp.Assign("New Name"))
    .Field(p => p.Price, FieldOp.Multiply(0.9)));

// Nearest-neighbor — field name resolved via lambda
var results = await client.Search.NearestNeighborSearchAsync<Product>(
    queryEmbedding, p => p.Embedding, topK: 10);

// Visit
await foreach (var d in client.Documents.VisitAsync<Product>(selection: "product.price > 100"))
    Process(d.Fields);

// Selection-based bulk ops — typed builder, auto-loops on Vespa's continuation token
var resp = await client.Documents.UpdateBySelectionAsync<Product>(
    "product.category == \"legacy\"",
    ops => ops.Field(p => p.Status, FieldOp.Assign("archived")),
    cluster: "content");
// resp.DocumentCount is the total across all chunks

// Cross-cluster copy
await client.Documents.CopyBySelectionAsync(
    "product.tier == \"cold\"", "product",
    cluster: "hot", destinationCluster: "cold");

Note: ID normalisation: pass either "p-1" or "id:myapp:product::p-1" — the client strips the prefix automatically.


YQL Builder

using Vespa.Query;

var yql = YqlBuilder<Product>
    .Select(p => p.Name, p => p.Price)
    .Where(w => w.Field(p => p.Price).GreaterThan(10)
                 .And(sub => sub.Field(p => p.Name).Contains("laptop")))
    .OrderBy(p => p.Price, descending: true)
    .Limit(20)
    .Build();

Fluent request builder captures YQL + ranking in one chain:

var request = YqlBuilder<Product>
    .Select()
    .Where(w => w.HybridSearch(p => p.Embedding, "q", "userQuery", targetHits: 100))
    .Limit(20)
    .ToSearchRequest()
    .WithRankProfile("hybrid_twophase")
    .WithQueryTensor("q", "embed(e5small, @userQuery)")
    .WithUserInput("userQuery", searchText);

Full predicate reference, boolean composition, and validation rules in docs/yql-builder.md


Grouping & Aggregation

var request = new VespaSearchRequest
{
    Yql = YqlBuilder<Product>
        .Select()
        .GroupBy(
            GroupingBuilder.All()
                .Group("category")
                .Max(10)
                .OrderByDescending(GroupingAgg.Count())
                .Each(e => e.Output(GroupingAgg.Count(), GroupingAgg.Avg("price"))))
        .Build(),
    Hits = 0
};

var result = await client.Search.GroupByAsync<Product>(request);

Nested grouping, buckets, having, pagination, and full aggregation reference in docs/grouping.md


Bulk Feed

// Streaming pipeline with backpressure
var result = await client.Feed.FeedAsync(
    ReadFromDatabase(),             // IAsyncEnumerable<FeedDocument<Product>>
    documentType: "product",
    maxConcurrency: 64,             // parallel HTTP/2 streams
    boundedCapacity: 256,           // backpressure buffer
    onProgress: p => Console.Write($"\r{p.SuccessCount} docs..."));

Per-document conditions, BulkPut/Update/Delete, and FeedResult details in docs/feed.md


Multi-Tenant Dynamic Fields

For platforms where tenants define custom fields at runtime, [VespaExtraFields] provides a catch-all bag — similar to MongoDB's [BsonExtraElements]:

public record ProductFields
{
    // App-owned fields — strongly typed, used for ranking
    [VespaField(IndexingMode = IndexingMode.AttributeSummary)]
    public double Popularity { get; init; }

    // Tenant-defined fields — captured dynamically
    [VespaExtraFields]
    public Dictionary<string, JsonElement>? TenantFields { get; init; }
}
  • Unmapped JSON fields are captured in the dictionary during deserialization
  • Serialized flat alongside declared properties (not nested)
  • Round-trip safe: deserialize then serialize preserves everything
  • Schema builder ignores [VespaExtraFields] properties

Admin API

// Deploy from C# attributes
await client.Admin.DeploySchemaAsync<Product>();

// Multi-tenant: override tenant per-call
await client.Admin.DeploySchemaAsync<Product>(tenant: "acme");

// Deploy raw ZIP
await client.Admin.DeployAsync(zipStream);

// Cluster status
var status = await client.Admin.GetApplicationStatusAsync();
var ready   = await client.IsReadyAsync();
var version = await client.GetVersionAsync();
var metrics = await client.GetMetricsAsync();

Note: ApiKey, DefaultRequestHeaders, compression, timeout, and mTLS settings are applied to both the data-plane client and the admin/config-server client.

Tip: Health/state helpers such as HealthCheckAsync, IsReadyAsync, GetMetricsAsync, GetVersionAsync, and GetHistogramsAsync are best-effort on ordinary failures, but they still propagate OperationCanceledException when the request is cancelled.

Schema attributes, [VespaExtraFields], ApplicationPackageOptions, and CustomServicesXml in docs/schema.md


Configuration & Ops

<details> <summary><strong>DI Integration</strong></summary>

builder.Services.AddVespaClient(new VespaClientOptions
{
    Endpoint         = "http://localhost:8080",
    DefaultNamespace = "myapp",
    EnableRetry      = true
});

// Named client (multi-cluster)
builder.Services.AddVespaClient("analytics", new VespaClientOptions
{
    Endpoint = "http://analytics-vespa:8080"
});

// Inject
public class ProductService(IVespaClient vespa)
{
    public Task<VespaDocument<Product>?> Get(string id)
        => vespa.Documents.GetAsync<Product>(id);
}

</details>

<details> <summary><strong>Vespa Cloud (mTLS)</strong></summary>

var options = new VespaClientOptions
{
    Endpoint        = "https://myapp.vespa-cloud.com",
    CertificatePath = "/path/to/cert.pem",
    ClientKeyPath   = "/path/to/key.pem",
    // or: ClientCertificate = myCert,
    // or: ApiKey = "bearer-token"
};

</details>

<details> <summary><strong>Observability (OpenTelemetry)</strong></summary>

builder.Services.AddOpenTelemetry()
    .WithTracing(b => b.AddSource("Vespa.NET"))
    .WithMetrics(m => m.AddMeter("Vespa.NET"));

Spans: vespa.search, vespa.document.*, vespa.feed.pipeline, etc. Metrics: vespa.client.requests, vespa.client.request_duration, vespa.client.documents_written, etc.

</details>

<details> <summary><strong>Health Checks</strong></summary>

builder.Services.AddHealthChecks().AddVespa();

</details>

<details> <summary><strong>Testcontainers</strong></summary>

await using var vespa = new VespaContainer();
await vespa.StartAsync();
var (client, http) = vespa.CreateClientWithHttp("test");

Set VESPA_INTEGRATION_TESTS=1 to enable in CI.

</details>

Full configuration reference, exceptions, metrics table, and more in docs/configuration.md


Documentation

Guide Content
Schema & Attributes Code-first generation, [VespaExtraFields], ApplicationPackageOptions
Document Operations CRUD, field ops, conditional writes, visit, addressing
Search Basic, nearest-neighbor, hybrid, streaming, request options
YQL Builder Fluent query builder, predicates, boolean composition
Grouping Aggregations, buckets, nested grouping, pagination
Ranking DSL RankingBuilder, code-first rank profiles
Feed Bulk operations, streaming pipeline, backpressure
Configuration Options, DI, mTLS, observability, health checks, testing

Running the Sample App

docker run --detach --name vespa --publish 8080:8080 --publish 19071:19071 vespaengine/vespa
dotnet run --project samples/Vespa.NET.Samples

Dependencies

Package Purpose
Microsoft.Extensions.Http IHttpClientFactory
Microsoft.Extensions.Http.Resilience Retry + circuit breaker (Polly)
Microsoft.Extensions.Diagnostics.HealthChecks ASP.NET Core health check
System.Text.Json JSON serialization (built-in)
DotNet.Testcontainers (test project only) Docker test fixtures

Resources

License

MIT License — see LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 103 6/10/2026
1.1.0 103 4/30/2026
1.0.0 113 4/29/2026