Relatude.DB.CMS 1.1.1.3-alpha

This is a prerelease version of Relatude.DB.CMS.
dotnet add package Relatude.DB.CMS --version 1.1.1.3-alpha
                    
NuGet\Install-Package Relatude.DB.CMS -Version 1.1.1.3-alpha
                    
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="Relatude.DB.CMS" Version="1.1.1.3-alpha" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Relatude.DB.CMS" Version="1.1.1.3-alpha" />
                    
Directory.Packages.props
<PackageReference Include="Relatude.DB.CMS" />
                    
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 Relatude.DB.CMS --version 1.1.1.3-alpha
                    
#r "nuget: Relatude.DB.CMS, 1.1.1.3-alpha"
                    
#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 Relatude.DB.CMS@1.1.1.3-alpha
                    
#: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=Relatude.DB.CMS&version=1.1.1.3-alpha&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Relatude.DB.CMS&version=1.1.1.3-alpha&prerelease
                    
Install as a Cake Tool

Relatude.CMS

A .NET 8 content backend layered on Relatude.DB, an in-process object-oriented graph database (NuGet Relatude.DB.Server). You model your domain as C# interfaces (the engine generates the implementing classes), declare relationships as small marker classes, and read/write through a NodeStore. Provides the base content model (IItem, IPage, ISite, ITemplate), dynamic URL routing, and DB access plumbing.

Using an AI coding agent? This package ships a Claude Code skill (relatude-cms) with the same guidance below. The commerce layer is documented in Relatude.Ecom (relatude-ecom skill), which builds on this one.

Host setup (Program.cs)

Relatude.DB runs in-process inside an ASP.NET Core app:

var builder = WebApplication.CreateBuilder(args);
builder.AddRelatudeDB();          // from Relatude.DB.Server, no namespace needed
builder.Services.AddHttpContextAccessor();
// register CMS services (routing, URL service, request context) here

var app = builder.Build();
app.UseRelatudeDB();              // starts the engine; admin UI at /relatude.db
app.Run();

AddRelatudeDB registers a DI type Database : NodeStore (transient) you can inject directly. The schema (datamodel) is managed through the admin UI at /relatude.db, where you point a datamodel source at the assembly/types holding your model interfaces.

Getting a NodeStore

NodeStore (namespace Relatude.DB.Nodes) is the entire data API. Obtain one by injecting Database, by injecting IRelatudeDBAccessor (Relatude.CMS.Engine) and using .Database, or statically via RelatudeDBRuntime.Server. Convention: methods that touch data take NodeStore db as a parameter rather than capturing it.

Modeling: interfaces, not classes

An entity is an interface extending IItem (and usually IPage for routable content):

public interface IPage : IItem {
    string MetaTitle { get; set; }
    string MetaDescription { get; set; }
    TemplatePages.Template Template { get; set; }   // relation end → the one Template for this page
}

IItem gives every node Guid Id, string Name, NodeMeta Meta (Meta.Address is the URL slug). IPage adds SEO metadata and a Template relation end. Other base types: ISite, ITemplate, ITree, ICountry, ICMSUser, ICmsUserGroup.

Relations are declared as classes deriving from OneToMany<,>, ManyToMany<,>, OneToOne<,>, OneOne<>, or ManyMany<>, with nested marker classes naming each end:

public class TemplatePages : OneToMany<ITemplate, IPage> {
    public class Template : One { }   // exposed on IPage
    public class Pages : Many { }     // exposed on ITemplate
}

OneToMany<TOne,TMany> exposes One/Many; ManyToMany<TFrom,TTo> exposes ManyFrom/ManyTo; OneToOne<,> exposes OneFrom/OneTo. Attributes (Relatude.DB.Nodes) tune mapping: [Node(...)] on the interface, [Exclude], and per-type property attributes ([StringProperty], [IntegerProperty(Indexed=true, UniqueValues=true)], [DecimalProperty], [GuidProperty], …). Embedded value maps use [EmbeddedMapProperty(KeyProperty = ...)].

Relation ends are objects, not plain collections:

if (page.Template.IsSet()) { … }          // One: has value? / Many: Count() > 0
var template = page.Template.Get();         // One → T (throws if not set; guard first)
page.Template.TryGet(out var t);            // safe variant
foreach (var child in node.Children.Get())  // Many → IEnumerable<T>

In queries prefer Include(...) over lazy .Get() to avoid N+1.

CRUD through NodeStore

var page = db.Create<IPage>();             // in-memory, NOT yet persisted
db.Insert(page);                            // or CreateAndInsert<IPage>(p => { ... })
db.Update(existing);                        // UpdateOrFail / UpdateIfExists / Upsert / ForceUpsert
db.Delete(node);                            // also Delete(Guid id) / Delete(IEnumerable<Guid>)
var p = db.Get<IPage>(id);                  // throws if missing
db.TryGet<IPage>(id, out var page2);        // safe
bool exists = db.Exists<IPage>(id);

Relations are set/cleared as their own operations (property-selector lambda):

db.SetRelation(page, p => p.Template, templateId);  // one-end: replaces
db.AddRelation(template, t => t.Pages, page);        // add to many-end
db.RemoveRelation(template, t => t.Pages, page);     // remove one link
db.ClearAndSetRelation(node, x => x.Many, newSet);   // replace whole set

Write methods accept flushToDisk (default false) and have …Async counterparts. Mutating a property on a loaded node still requires an Update call to persist.

Querying

db.Query<T>() returns IQueryOfNodes<T,T> — fluent and expression-based; nothing runs until a terminal call:

var results = db.Query<IPage>()
    .Where(p => p.MetaTitle != null)
    .Include(p => p.Template)            // eager-load; ThenInclude to go deeper
    .OrderBy(p => p.Name)
    .Page(0, 20)                          // pageIndex0based, pageSize (or Take/Skip)
    .Execute();                           // ResultSet<IPage>; .ToList()/.ToArray() also

var one  = db.Query<IPage>().Where(p => p.Meta.Address == slug).FirstOrDefault();
long n   = db.Query<IPage>().Count();

Also: WhereSearch/Search (BM25 + optional semantic/vector), WhereRelates/WhereNotRelates/WhereRelatesAny, WhereIn, WhereTypes, Facets(), Sum(...), SelectId(), a string-based Where("a => …") overload, and db.TryGetFromAddress<IPage>(address, out var page) for URL → node. Query context controls culture/visibility/revisions: db.Context.Culture("nb-NO").Hidden().Admin().Create(). Default queries exclude hidden nodes.

Transaction plugins (triggers)

Cross-cutting node behavior goes in a plugin. Derive NodeTransactionPlugin<T>, override OnBeforeNodeAction / lifecycle hooks, register with db.RegisterTransactionPlugin(...). Example: UrlAddressPlugin : NodeTransactionPlugin<IPage> generates a unique URL slug on upsert via transaction.UpdateAddress(...). Switch on the NodeOperation enum (Upsert, Insert*, Update*, ForceUpsert, Delete*).

Dynamic URL routing (Relatude.CMS.Engine.Web)

  • RelatudeDynamicRouting : DynamicRouteValueTransformer maps URL → {controller, action, id} via IRelatudeAddressResolver (→ RequestInfo), with per-request state in IRelatudeRequestContext.
  • IRelatudeUrlService.GetUrl(IPage) reverses it: stored Meta.Address, then a template-based action link, then /rnode/{id} fallback.
  • RelatudeUrlHelper.GetUniqueUrlSlug(name, db) generates collision-free slugs.

CMS settings and analytics (Relatude.CMS.Settings, Relatude.CMS.Analytics)

builder.Services.AddRelatudeCms() registers the CMS's own settings, CmsSettings, as a Relatude.DB custom settings set: a CMS tab beside each database's settings in the admin UI, read from code as db.Settings.Get<CmsSettings>(). It is layered like every custom setting: relatude.settings/cms.json (SHARED), the admin UI's saves (THIS SERVER), then RelatudeDB:CustomSettings:cms in appsettings. Its first group is Analytics; more CMS settings go in the same class.

builder.AddRelatudeDB();
builder.Services.AddRelatudeCms();
var app = builder.Build();
app.UseRelatudeDB();
app.UseRelatudeCmsAnalytics();   // after UseRelatudeDB, before the site's pages and static files

Analytics is off until turned on (CMS > Analytics > Record page views). Then:

  • Every page view is recorded in the custom log cms-pageviews ("Page views" under Logs in the admin UI, with graphs from its statistics): path, host, referrer host, visitor hash, visitor type (human, bot, unknown), bot name, country, region, city, position, browser, operating system, device, language and duration. A page view is a GET answered 200 with HTML. The admin UI, /-info/, the paths in ExcludedPaths and prefetches are never page views.
  • The log is the database's once it is made. It is created when it is missing - the first time analytics is on, and again if it is deleted in the admin UI. After that, changes made in the admin UI (retention, columns taken out, statistics, switches) are kept. The code only adds columns: each column in PageViewLog carries the DefinitionVersion that introduced it, and the database's storage remembers the version its log has (cms/pageviews-log.json). When the CMS is newer, the columns added since are put into the log if missing, once; a column removed afterwards stays removed. A log made before this file existed counts as version 1. To add a column, give it the next version and raise DefinitionVersion.
  • Nothing happens while the visitor waits. The middleware only queues the page view (PageViewQueue). AnalyticsWorker, a hosted service, waits LookupDelaySeconds (5) for the browser's bot check to report, looks the visitors of a batch up at once with db.Services.IPLookup (once per address and user agent; settled verdicts are kept for 10 minutes), and records the entries. A failed lookup records the page views without it and warns once per 10 minutes.
  • The bot check is added to every HTML page just before its last </body>, as the CMS's HtmlRewriter finds it (InjectBotCheck, InlineBotCheck). Only an uncompressed UTF-8 HTML 200 of at most 16 MB is held to do it; everything else streams through untouched. Call UseResponseCompression() before UseRelatudeCmsAnalytics(), if at all.
  • No cookie and no address are kept (GDPR). The visitor is the daily visitor hash, SHA-256(IP + User-Agent + Language + daily salt) (DailyVisitorHash). The salt is 32 random bytes per UTC day, held in memory only and never written, so a hash cannot be traced back or linked across days. A process restart starts a new salt, so a visitor seen on both sides of it counts twice that day. The referrer is only its host, and the query string is left out unless LogQueryString is on.
  • IAnalyticsHost is the database side (settings, custom logs, the database's storage, IP lookups, reserved paths). The default is RelatudeAnalyticsHost, the default database's; tests replace it.
  • Behind a proxy, call app.UseForwardedHeaders() first, so the address hashed and looked up is the visitor's.

Donations (Relatude.CMS.Models.Donations)

A generic donation node model with the list/detail projection behind the WAF.Client Donations admin module (postWebAPI(session, "Donations", "List" | "Get", …)). The model namespace holds only the IDonation interface — a datamodel source scans it, so anything else in that namespace would be registered as a node type. The wire DTOs (DonationRow/DonationListResult/DonationDetail) and the pure DonationAdminProjection live in Relatude.CMS.Models.Donations.Admin. Sites extend the model with their own node interface (public interface IMyDonation : IDonation { … }) — a member may be declared in only one interface of the hierarchy.

Both namespaces are registered as datamodel sources — matching is exact, and an unregistered base interface silently drops all inherited fields from the derived type:

"DatamodelSources": [
  { "Id": "<unique guid>", "Name": "My site",   "Namespace": "MySite.Models",                 "Type": "AssemblyNameReference", "Reference": null },
  { "Id": "<unique guid>", "Name": "Donations", "Namespace": "Relatude.CMS.Models.Donations", "Type": "AssemblyNameReference", "Reference": "Relatude.DB.CMS" }
]

The admin controller stays in the host — it derives from the host framework's admin API base (e.g. WAF's BaseAdminAPIController), which this package does not reference. Routed as wafapi/Donations/{action}, the shape is:

[HttpPost]
public DonationListResult List(int pageSize = 25, int pageIndex = 0, string? sortBy = null, bool sortDesc = true,
    string? categories = null, string? statuses = null, string? paymentMethods = null, string? search = null) {
    _ = Context.Session;                             // WAF's admin gate — throws (→ 403) without a session
    var query = _cx.Database.Query<IMyDonation>()    // WhereIn(d => d.Status/…) for non-empty filters
        .OrderBy(DonationAdminProjection.SortExpression<IMyDonation>(sortBy), sortDesc);
    var page = query.Page(pageIndex, pageSize).Execute();
    return new DonationListResult { Rows = page.Values.Select(d => DonationAdminProjection.ToRow(d)).ToList(), TotalCount = page.TotalCount };
}

Get(id) is TryGet<IMyDonation> → DonationAdminProjection.ToDetail(d, formGuid: …), where the host resolves the originating form's content guid from IDonation.FormId (see DonationDetail.FormGuid — the admin client cannot query by ContentId). The host also snapshots the form's name onto IDonation.FormName when the donation is created — denormalized on purpose, since the list sorts and searches it server-side (AQL orders node properties, not values computed at projection time); a later rename therefore does not retro-rename existing donations. Category (onetime/recurring/business) and free-text search have no AQL form — when active, materialise the ordered set, filter with MatchesCategory/MatchesSearch, then page.

Notes

  • Create<T>() does not persist — follow with Insert/CreateAndInsert; call Update after mutating a loaded node.
  • Relation ends aren't Lists — use .Get() / .IsSet() / .TryGet().
  • Relation changes go through SetRelation/AddRelation/RemoveRelation, not property assignment.
  • This is a library with no entry point; engine start and schema live in the host app and the /relatude.db admin UI.
  • Relatude.DB source/API reference: https://github.com/Relatude/Relatude.DB
  • Donations: every DatamodelSources entry needs its own unique Id — the store refuses to boot without one.
  • Donations: the enum-like string values (PaymentMethod, Status, GiftType, Frequency) are matched case-sensitively by the admin module's filters — store exactly the values documented on IDonation (all lowercase, e.g. vippsrecurring).
Product Compatible and additional computed target framework versions.
.NET 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 (2)

Showing the top 2 NuGet packages that depend on Relatude.DB.CMS:

Package Downloads
Relatude.Core

Relatude .Net 10.0 Headless CMS and E-commerce solution

Relatude.DB.Ecom

Relatude E-commerce backend API for Relatude.DB. Builds on the Relatude.CMS package.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1.3-alpha 54 10/17/2026
1.0.44-alpha 114 10/12/2026
1.0.43 183 10/5/2026
1.0.42 155 9/23/2026
1.0.41 105 9/23/2026
1.0.40 101 9/23/2026
1.0.39 178 9/7/2026
1.0.38 175 9/1/2026
1.0.37 195 8/26/2026
1.0.36 135 8/21/2026
1.0.35 142 8/18/2026
1.0.34 128 8/17/2026
1.0.33 138 8/17/2026
1.0.32 176 8/5/2026
1.0.31 139 8/4/2026
1.0.30 125 8/3/2026
1.0.29 129 7/26/2026
1.0.28 136 7/26/2026
1.0.1.3-alpha 42 10/16/2026
1.0.1.2-alpha 54 10/15/2026
Loading failed