Relatude.DB.CMS
1.0.37
See the version list below for details.
dotnet add package Relatude.DB.CMS --version 1.0.37
NuGet\Install-Package Relatude.DB.CMS -Version 1.0.37
<PackageReference Include="Relatude.DB.CMS" Version="1.0.37" />
<PackageVersion Include="Relatude.DB.CMS" Version="1.0.37" />
<PackageReference Include="Relatude.DB.CMS" />
paket add Relatude.DB.CMS --version 1.0.37
#r "nuget: Relatude.DB.CMS, 1.0.37"
#:package Relatude.DB.CMS@1.0.37
#addin nuget:?package=Relatude.DB.CMS&version=1.0.37
#tool nuget:?package=Relatude.DB.CMS&version=1.0.37
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 inRelatude.Ecom(relatude-ecomskill), 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 = ...)].
Navigating relations on a loaded node
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 : DynamicRouteValueTransformermaps URL →{controller, action, id}viaIRelatudeAddressResolver(→RequestInfo), with per-request state inIRelatudeRequestContext.IRelatudeUrlService.GetUrl(IPage)reverses it: storedMeta.Address, then a template-based action link, then/rnode/{id}fallback.RelatudeUrlHelper.GetUniqueUrlSlug(name, db)generates collision-free slugs.
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 withInsert/CreateAndInsert; callUpdateafter 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.dbadmin UI. - Relatude.DB source/API reference: https://github.com/Relatude/Relatude.DB
- Donations: every
DatamodelSourcesentry needs its own uniqueId— 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 onIDonation(all lowercase, e.g.vippsrecurring).
| Product | Versions 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 was computed. 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. |
-
net8.0
- Microsoft.AspNetCore.Mvc.Core (>= 2.3.11)
- Relatude.DB.Server (>= 0.2.0.138-alpha)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Relatude.DB.CMS:
| Package | Downloads |
|---|---|
|
Relatude.Core
Relatude .Net 8.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.0.38 | 70 | 9/1/2026 |
| 1.0.37 | 110 | 8/26/2026 |
| 1.0.36 | 107 | 8/21/2026 |
| 1.0.35 | 117 | 8/18/2026 |
| 1.0.34 | 99 | 8/17/2026 |
| 1.0.33 | 114 | 8/17/2026 |
| 1.0.32 | 146 | 8/5/2026 |
| 1.0.31 | 110 | 8/4/2026 |
| 1.0.30 | 102 | 8/3/2026 |
| 1.0.29 | 112 | 7/26/2026 |
| 1.0.28 | 115 | 7/26/2026 |
| 1.0.27 | 120 | 7/26/2026 |
| 1.0.26 | 121 | 7/21/2026 |
| 1.0.25 | 113 | 7/21/2026 |
| 1.0.24 | 115 | 7/21/2026 |
| 1.0.23 | 118 | 7/21/2026 |
| 1.0.22 | 123 | 7/7/2026 |
| 1.0.21 | 152 | 6/26/2026 |
| 1.0.20 | 138 | 6/26/2026 |