-
Notifications
You must be signed in to change notification settings - Fork 4
First steps
This guide builds a small ASP.NET Core app end to end — a register of cats — to show how the pieces
of Scrinium fit together: domain models, model maps, a DbContext, DI registration, and basic CRUD.
The complete project is samples/AspNetCoreSample.
- .NET 8, 9 or 10 SDK.
- A local MongoDB instance reachable at its default address. Scrinium connects there unless you configure a connection string (see Startup and configuration).
Start from the ASP.NET Core Razor Pages template ("Web Application"), then install the meta
package Etherna.Scrinium, which brings in ASP.NET Core and Hangfire integration together:
PM> Install-Package Etherna.Scrinium
Add Etherna.Scrinium.AspNetCore.UI too if you want the optional Admin dashboard. See
Packages and feeds for the full package map.
Scrinium works with domain models: entities with an identity and relations expressed by composition — a great fit for DDD. There are two kinds:
-
Entity — has an identity (an
Id) and can be mutated by methods. ImplementsIEntityModel<TKey>. -
Value object — no identity, immutable after construction, lives only inside an entity or another
value object. Implements
IModel.
It's good practice to define two abstract base types and derive your domain from them. IModel only
requires the MongoDB ExtraElements bag:
using Etherna.Scrinium.Core.Domain.Models;
public abstract class ModelBase : IModel
{
public virtual IDictionary<string, object> ExtraElements { get; protected set; }
}using Etherna.Scrinium.Core.Domain.Models;
public abstract class EntityModelBase<TKey> : ModelBase, IEntityModel<TKey>
{
public virtual TKey Id { get; protected set; } = default!;
}Now the actual model:
public class Cat : EntityModelBase<string>
{
public Cat(string name, DateTime birthday)
{
Name = name;
Birthday = birthday;
}
protected Cat() { }
public virtual int Age => (int)((DateTime.Now - Birthday).TotalDays / 365);
public virtual DateTime Birthday { get; protected set; }
public virtual string Name { get; protected set; }
}A few rules are at play here — they apply to every persisted model (full list in Domain models):
Model rules
- Everything is
virtual. Scrinium subclasses your models with a proxy generated at compile time (a source generator shipped inside the core package) to provide lazy loading and change tracking; non-virtual members can't be overridden.- A protected parameterless constructor is required so the serializer can materialize an empty instance and fill it. Keep your public constructor for valid application-side creation.
- Writable members need a setter, which may be non-public (
protectedhere). Members with no setter — likeAge— are computed and ignored by the serializer by default.Idis left unset: the id generator configured in the model map (step 3 below) assigns it on insert (via reflection, so a non-public setter is fine).
A model map tells the serializer how a model is written to and read from documents. You register
maps by implementing IModelMapsCollector and calling MapRegistry.AddModelMap<TModel>(...). The
first argument is the map's schema id: an immutable string stamped into each document to record
which map wrote it, letting several schema versions coexist in one collection (see
Versioned schemas). Any non-empty string works; GUIDs are a convenient choice.
using Etherna.MongoDB.Bson;
using Etherna.MongoDB.Bson.Serialization.IdGenerators;
using Etherna.MongoDB.Bson.Serialization.Serializers;
using Etherna.Scrinium.Core;
using Etherna.Scrinium.Core.Serialization;
class ModelBaseMap : IModelMapsCollector
{
public void Register(IDbContextEngine dbContextEngine)
{
dbContextEngine.MapRegistry.AddModelMap<ModelBase>("1252861f-82d9-4c72-975e-3571d5e1b6e6");
dbContextEngine.MapRegistry.AddModelMap<EntityModelBase<string>>(
"81dd8b35-a0af-44d9-80b4-ab7ae9844eb5",
schema =>
{
schema.AutoMap();
schema.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId))
.SetIdGenerator(new StringObjectIdGenerator());
});
}
}using Etherna.Scrinium.Core;
using Etherna.Scrinium.Core.Serialization;
class CatMap : IModelMapsCollector
{
public void Register(IDbContextEngine dbContextEngine)
{
dbContextEngine.MapRegistry.AddModelMap<Cat>("cd37bafa-a36d-4b1f-815a-deb50c49d030");
}
}The optional second argument configures the map. Without it, Scrinium applies AutoMap(). Here the
base map calls AutoMap() and then customizes the Id member to serialize a string as a MongoDB
ObjectId and generate one on insert. CatMap just auto-maps Cat. Splitting maps into collectors
is optional but keeps things tidy. Model mapping is covered in depth in Model mapping.
A DbContext is a unit of work that exposes the repositories of one domain boundary. Declare an
interface (recommended, for DI and testing) and a class deriving from DbContext:
using Etherna.Scrinium.Core;
using Etherna.Scrinium.Core.Repositories;
public interface ISampleDbContext : IDbContext
{
IRepository<Cat, string> Cats { get; }
}using Etherna.Scrinium.Core;
using Etherna.Scrinium.Core.Repositories;
using Etherna.Scrinium.Core.Serialization;
public class SampleDbContext : DbContext, ISampleDbContext
{
public IRepository<Cat, string> Cats { get; } = new Repository<Cat, string>("cats");
protected override IEnumerable<IModelMapsCollector> ModelMapsCollectors =>
new IModelMapsCollector[]
{
new ModelBaseMap(),
new CatMap()
};
protected override Task SeedAsync()
{
// Seed initial data here.
return base.SeedAsync();
}
}-
Catsis a repository overCatdocuments in the"cats"collection. -
ModelMapsCollectorsenumerates the collectors from step 3 so their maps get registered. -
SeedAsyncpopulates the database, once per context across every application instance (see Database seeding) — leave it callingbase.SeedAsync()for now.
Note. The
DbContextis created and wired by Scrinium — you don't write a constructor that takes dependencies or options. Options (connection string, database name…) are set at registration time; see Startup and configuration.
Wire Scrinium into DI with AddScriniumWithHangfire() and register the context with AddDbContext.
Add the dashboard and Hangfire server, and seed the contexts on startup:
using Etherna.Scrinium.AspNetCore.Extensions;
using Etherna.Scrinium.AspNetCore.UI; // only if using the dashboard
using Etherna.Scrinium.Extensions;
using Hangfire;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddHangfireServer();
builder.Services.AddScriniumWithHangfire()
.AddDbContext<ISampleDbContext, SampleDbContext>();
builder.Services.AddScriniumAdminDashboard(); // optional
var app = builder.Build();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization(); // required by the dashboard
app.UseHangfireDashboard();
app.MapRazorPages();
app.SeedDbContexts(); // run each context's SeedAsync
app.Run();Note. Registration variants (including composing
Etherna.Scrinium.AspNetCorewith a non-Hangfire task runner) and every option are documented in Startup and configuration.
Inject the context interface and call the repository. This Razor Page adds, lists and removes cats:
using Etherna.MongoDB.Driver.Linq; // ToListAsync on the LINQ query
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
public class IndexModel : PageModel
{
public class InputModel
{
[Required]
[DataType(DataType.Date)]
public DateTime Birthday { get; set; }
[Required]
public string Name { get; set; } = default!;
}
private readonly ISampleDbContext sampleDbContext;
public IndexModel(ISampleDbContext sampleDbContext) =>
this.sampleDbContext = sampleDbContext;
public List<Cat> Cats { get; } = new();
[BindProperty]
public InputModel Input { get; set; } = default!;
public async Task<IActionResult> OnGetAsync()
{
await LoadCatsAsync();
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
await LoadCatsAsync();
if (!ModelState.IsValid)
return Page();
var cat = new Cat(Input.Name, Input.Birthday);
await sampleDbContext.Cats.CreateAsync(cat);
return RedirectToPage();
}
public async Task<IActionResult> OnPostRemoveAsync(string id)
{
await sampleDbContext.Cats.DeleteAsync(id);
return RedirectToPage();
}
private async Task LoadCatsAsync()
{
var cats = await sampleDbContext.Cats.QueryElementsAsync(elements =>
elements.ToListAsync());
Cats.AddRange(cats);
}
}That's the full loop: CreateAsync inserts the document (generating its Id), DeleteAsync
removes it, and QueryElementsAsync runs a LINQ query over the collection — the complete repository
API is in CRUD operations and Querying.

- Startup and configuration — registration variants and options
- Domain models — the complete rules for persisted models
- Model mapping — customize how models serialize
- References and denormalization — relate documents and keep denormalized copies in sync
- DbContext and engine — the unit of work, identity map and scope model
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference