Kanject.Core.NoSqlDatabase
Type-safe NoSQL access across DynamoDB, ScyllaDB, and Amazon Keyspaces. Declare a [DbContext], give entities their key layout with AWS key attributes plus Kanject's [KeyTemplate], then mark an empty partial [Repository] class. The source generator emits the implementation — typed key composers, CRUD, finders, GSI queries, and child repositories — at compile time, with zero reflection at runtime.
Providers
The same context, entity, and repository model compiles against three engines. Swap the registration call; leave the rest of your application code untouched.
Install
dotnet add package Kanject.Core.NoSqlDatabase.Provider.DynamoDb Each provider ships as its own package — Kanject.Core.NoSqlDatabase.Provider.DynamoDb, …Provider.Scylla, or …Provider.Keyspaces — and pulls in the shared abstractions transitively.
Define the entity, context, and repository
using Amazon.DynamoDBv2.DataModel;using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Abstractions.DataContext.EntityBuilder;using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Abstractions.Interfaces;using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Annotations.Attributes;namespace Demo.Catalog.Data;// The entity. The [DynamoDB*] attributes are AWS's (Amazon.DynamoDBv2.// DataModel) — they name the physical key attributes. [KeyTemplate] is// Kanject's — it composes each key's value from the entity's own properties.public sealed class Product : IDynamoDbEntity{ [DynamoDBHashKey("pk")] [KeyTemplate("PRODUCT#{Sku}")] public string PartitionKey { get; set; } = string.Empty; [DynamoDBRangeKey("sk")] [KeyTemplate("META")] // literal-only template → sk = "META" public string SortKey { get; set; } = string.Empty; [DynamoDBProperty] public string Sku { get; set; } = string.Empty; [DynamoDBProperty] public string Name { get; set; } = string.Empty; [DynamoDBProperty] public long PriceCents { get; set; }}// One table, one context. MapEntity names the physical table and registers// every entity that lives in it (add more entities to the same call).[DbContext]public partial class CatalogDbContext{ protected override void OnModelCreating(EntityModelBuilder builder) => builder.MapEntity("Catalog", new Product());}// Declare the repository as an empty partial. The source generator emits the// CRUD, the typed finders, and an IProductRepository interface it registers// for you (GenerateInterface defaults true).[Repository(Name = "Products", Version = 1, Entity = typeof(Product))]public partial class ProductRepository; The [KeyTemplate] values become strongly typed key composers: set Sku = "sku-42" and the stored pk is PRODUCT#sku-42. [DbContext]'s MapEntity names the physical table and registers each entity in it. The [Repository] partial is completed by the generator — you write no method bodies.
Register
using Kanject.Core.NoSqlDatabase.Provider.DynamoDb.Extensions;builder.Services.AddDynamoDbContext<CatalogDbContext>( options: cfg => { cfg.Namespace = appSettings.Stage; // "prod" → physical table "prod.Catalog" cfg.AwsRegion = appSettings.AwsRegion; cfg.CreateTableCheck = true; // create the table on first run if missing }, dbContextOptions: options => options.ServiceCollection.RegisterDynamoDbRepository()); Switch providers by swapping the call — AddScyllaDbContext<TContext>(...) or Keyspaces' AddDbContext<TContext>(...) — leaving the entity and repository untouched. cfg.Namespace prefixes the physical table so multiple stages share an account safely: namespace prod maps table Catalog to prod.Catalog (dot-joined, the namespace lower-cased). dev and prod therefore never touch each other's data.
Use it
public sealed class ProductService(IProductRepository products){ // Write: set the template fields; the pk/sk strings compose themselves. public Task AddAsync(Product product) => products.InsertAsync(product); // Read one item. The finder takes every key-template parameter — here just // the Sku (the sort key is the literal "META"). Keys resolve to // pk = PRODUCT#<sku>, sk = META. public Task<Product?> GetAsync(string sku) => products.FindProductAsync(sku); public async Task UpdatePriceAsync(string sku, long priceCents) { var product = await products.FindProductAsync(sku); if (product is null) return; product.PriceCents = priceCents; await products.AddOrUpdateAsync(product); }} Inject the generated IProductRepository. The single-item finder is Find{Entity}Async (it takes every key-template parameter); a whole partition uses the pluralized, paginated Find{Entities}Async. Base CRUD — InsertAsync, AddOrUpdateAsync, UpdateAsync, RemoveAsync, GetSingleAsync — comes from IRepository<TEntity>.
Global secondary indexes
Add a [DynamoDbGsi(Name = "Gsi1Index", HashKey = "...", RangeKey = "...")] to the entity to declare an index, and a [DynamoDbGsiAlias<TEntity>(name: "Gsi1Index", alias: "SellerProduct")] to give an access pattern a readable name. The generator emits a static finder for it named from the alias and index — FindSellerProductsUsingGsi1IndexAsync(...) — so an index query reads as clearly as a primary-key one. [DynamoDbLsi] declares local secondary indexes the same way.
What ships with it
- Three providers: DynamoDB, ScyllaDB, Amazon Keyspaces — same
[DbContext]and[Repository]shape - Source-generated partial repositories — CRUD, typed finders, and an auto-registered
I<Name>Repository, zero reflection at runtime [KeyTemplate]key composition for single-table designs —PRODUCT#{Sku}composespkfrom the entity- Strongly-typed GSI / LSI access patterns via
[DynamoDbGsi]/[DynamoDbGsiAlias<T>]/[DynamoDbLsi] cfg.Namespaceprefixes the physical table (prod.Catalog) for multi-stage isolation[EntityRepository<T>]/[EmbedRepository<T>]for child collections and composed repositories under one unit of work- Compile-time diagnostics (
KAN…/KANJECT…) for invalid key templates, indexes, and repository shapes