Kanject.Core.NoSqlDatabase

Type-safe NoSQL access across DynamoDB, ScyllaDB, and Amazon Keyspaces. Declare a [Repository] interface, annotate the entity with [PartitionKey] / [SortKey] / [Indexed], and a source generator emits the implementation — including secondary-index queries — at compile time.

Providers

The same [Repository] interfaces and entity annotations compile against three engines. Swap the registration extension, leave the rest of your code untouched.

DynamoDB
`AddDynamoNoSqlDatabase()` — single-digit-ms reads/writes, on-demand capacity, multi-region tables. The default provider.
ScyllaDB
`AddScyllaNoSqlDatabase()` — same DynamoDB-shaped data model, run on self-hosted or ScyllaCloud clusters when you need cost ceilings or co-location.
Keyspaces
`AddKeyspacesNoSqlDatabase()` — Apache Cassandra on AWS. Same `[Repository]` shape, CQL underneath. Pick this for Cassandra-native customers.

Install

bash
dotnet add package Kanject.Core.NoSqlDatabase

Register

csharp
using Kanject.Core.NoSqlDatabase.Provider.DynamoDbV2.Extensions;

builder.Services.AddDynamoNoSqlDatabase(options =>
{
    options.Namespace = appSettings.DatabaseNamespace;  // e.g. "prod"
    options.AwsRegion = appSettings.AwsRegion;
});

Switch providers with one line — AddScyllaNoSqlDatabase(...) or AddKeyspacesNoSqlDatabase(...) in place of AddDynamoNoSqlDatabase(...). Namespace prefixes every table/keyspace at runtime (prod-Products, staging-Products) so multiple stages can share an account safely.

Define a repository

csharp
using Kanject.Core.NoSqlDatabase.Abstractions;
using Kanject.Core.NoSqlDatabase.Abstractions.Attributes;

[Repository(table: "Products")]
public partial interface IProductRepository : IRepository<Product>
{
    [Query(by: nameof(Product.SellerId))]
    Task<IReadOnlyList<Product>> ListBySellerAsync(string sellerId);

    [Query(by: nameof(Product.Status), index: "ByStatus")]
    Task<IReadOnlyList<Product>> ListByStatusAsync(ProductStatus status);
}

public record Product
{
    [PartitionKey]                       public Guid Id { get; init; }
    [SortKey, Indexed("BySeller")]       public string SellerId { get; init; } = default!;
    [Indexed("ByStatus")]                public ProductStatus Status { get; init; }
    public string Title { get; init; } = default!;
    public decimal Price { get; init; }
}

The partial interface is filled in by the source generator. [Query] methods compile to GSI lookups; [Indexed] declares the GSIs they read from.

Use it

csharp
public class ProductService(IProductRepository products)
{
    public Task<Product?> GetAsync(Guid id) => products.FindAsync(id);

    public Task<IReadOnlyList<Product>> ListByStatusAsync(ProductStatus s)
        => products.ListByStatusAsync(s);

    public Task UpdatePriceAsync(Guid id, decimal price)
        => products.UpdateAsync(id, p => p with { Price = price });
}

What ships with it

  • Three providers: DynamoDB, ScyllaDB, Amazon Keyspaces — same [Repository] shape
  • Source-generated repository implementations — zero reflection at runtime
  • Strongly-typed GSI / LSI queries with compile-time index validation
  • Automatic Namespace prefixing for multi-stage isolation
  • Optimistic concurrency via [Version] attribute
  • Mockable IRepository<T> interface for unit tests
Need relational?
See Kanject.Core.SqlDatabase (beta) — the relational sibling with an Aurora DSQL provider, same [Repository] ergonomics. /docs/core-sql