The options
Each option below adds to the smallest setup, in any order.
Configuring the database
AddRelatudeCMS() adds Relatude.DB with its defaults. To configure it, call AddRelatudeDB(o => …)
yourself, before or after, as often as you like.
| Option | Use it to | Example |
|---|---|---|
OnDatamodelInit |
register content types in code | (dm, _) => dm.AddCmsContentModel() |
CreateUrlManager |
shape the URLs: a home page, a page tree, domains (chapter 6) | _ => new DefaultUrlManager(new() { FallbackRootId = homeId }) |
OnStoreInit |
register transaction plugins and task runners before the store opens | db => db.RegisterTransactionPlugin(new PageAddressPlugin()) |
OnStoreOpenBackground |
seed content and warm caches after the store opens, off the request path | db => Seeder.SeedIfEmpty(db) |
OnStoreOpen |
seed content that modules build on; it finishes before their seeds start (chapter 18) | db => Seeder.SeedIfEmpty(db) |
DefaultDataFolderPath |
keep relatude.data/ and relatude.settings/ outside the project folder |
"D:/sites/mysite" |
The Relatude.DB manual lists the rest in Options and events in Program.cs.
Registering content types
Register content types in code, as above, or as datamodel sources in the admin UI. The two add up:
the sources load first, then OnDatamodelInit runs.
In code:
dm.AddCmsContentModel()addsIItem,IPage,ISite, the page treeRTree,ITemplateand the template kinds, with the Relatude.DB types they build on (users, groups, collections, cultures).dm.AddNamespace<IArticle>()adds every type in the namespace ofIArticle.dm.Add<IArticle>()adds that type and the types it refers to.- The CMS's users and groups,
ICMSUserandICMSUserGroup, are left out. Add them when the site signs users in (Who the visitor is).
A datamodel source takes the node types of one namespace in one assembly. The match is exact, so
Relatude.CMS.Models.Donations needs a source of its own, and so do the Relatude.DB types the CMS
builds on:
"DatamodelSources": [
{ "Id": "<unique guid>", "Name": "Native", "Namespace": "Relatude.DB.Native.Models", "Type": "CompiledTypes", "Reference": "Relatude.DB.NodeStore" },
{ "Id": "<unique guid>", "Name": "CMS", "Namespace": "Relatude.CMS.Models", "Type": "CompiledTypes", "Reference": "Relatude.DB.CMS" },
{ "Id": "<unique guid>", "Name": "Donations", "Namespace": "Relatude.CMS.Models.Donations", "Type": "CompiledTypes", "Reference": "Relatude.DB.CMS" },
{ "Id": "<unique guid>", "Name": "My site", "Namespace": "MySite.Models", "Type": "CompiledTypes", "Reference": null }
]Referencenames the assembly;nullis the application's own. A wildcard,MySite.*, takes several of your own namespaces.- Every source needs a unique
Id, or the store does not start. - Every base interface of a type must be in the datamodel. If one is missing, the store does not start, and the error names it.
Choosing how pages render
Add the rendering systems the site uses, one call each:
| Call | Adds | Needed for |
|---|---|---|
AddMvcRendering() |
MVC controllers and views (AddControllersWithViews()) |
pages rendered by a view or a controller |
AddRazorPagesRendering() |
Razor Pages (AddRazorPages()) |
pages rendered by a Razor Page |
AddRendering(o => …) |
code defaults per type, selectors and template kinds (chapter 8) | deciding in code |
With none of them, stored files, JSON results and GetContent<T>() still work. Your own
AddControllersWithViews(o => …) or AddRazorPages() works beside these calls. They configure the
same MVC, so its filters and options apply to content requests too.
The request pipeline
The smallest setup leaves the middleware order to ASP.NET Core. A real site adds static files, its own routes and a fallback. This order works:
var app = builder.Build();
app.UseRelatudeCMS(); // first: while the database opens, requests get a progress page
app.UseStaticFiles(); // before routing, so a static file never looks for content
app.UseRouting();
app.UseAuthentication(); // middleware after UseRouting runs for content requests too
app.UseAuthorization();
app.MapGet("/api/status", () => "ok"); // explicit routes win over content
app.MapRelatudeCMS(); // pages and stored files
app.MapFallback(() => Results.NotFound()); // what neither answers
app.Run();UseRelatudeCMS() calls UseRelatudeDB(). If you call both, the database still starts once.
Who the visitor is
Authentication is ASP.NET Core's: cookies, Identity, OpenID Connect or whatever the site uses. The CMS needs to know one thing: which user node the visitor is. The store then reads as that user, so the read access editors set on content, by group, applies.
Users are ICMSUser nodes and groups are ICMSUserGroup nodes. Add both to the datamodel with
dm.Add<ICMSUser>() and dm.Add<ICMSUserGroup>(), beside dm.AddCmsContentModel().
Sign a user in with CmsPrincipal.For. It writes the user's id, name and email as claims, and the
names of the user's groups as roles. Here the site's own controller checks the email and the
password stored on the user:
// Program.cs
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o => o.LoginPath = "/login");
// …
app.MapControllers(); // the site's own routes, such as POST /login
app.MapRelatudeCMS();// Controllers/AccountController.cs (SignInAsync is in Microsoft.AspNetCore.Authentication)
public class AccountController(RelatudeCmsContext ctx) : Controller {
[HttpPost("/login")]
public async Task<IActionResult> Login(string email, string password) {
var user = ctx.Admin.Query<ICMSUser>().Where(u => u.Email == email).Execute().FirstOrDefault();
if (user == null || !user.Password.Verify(password)) return Unauthorized();
await HttpContext.SignInAsync(CmsPrincipal.For(user, CookieAuthenticationDefaults.AuthenticationScheme));
return Redirect("/");
}
}[Authorize(Roles = "Editors")] then works on the site's own controllers. A sign-in for a user node
the store does not have counts as anonymous.
A page or file the visitor may not read sends an anonymous visitor to sign in, and refuses a signed-in one with 403 or the scheme's access-denied page (chapter 7). Without an authentication scheme, it answers as if it did not exist.
AddRelatudeCMS(o => …) changes how the CMS reads a request:
| Option | Default | Change it to |
|---|---|---|
UserId |
the NameIdentifier claim as the node id, else the user whose email is in the Email claim |
find the user another way, such as from a header a gateway sets |
CultureCode |
the URL's culture, else the user's preferred culture, else the store's default | choose the culture by host name or cookie |
LoginRedirect |
true |
false: content the visitor may not read answers as if it did not exist |
builder.AddRelatudeCMS(o => o.CultureCode = ctx => ctx.Http.Request.Host.Host.EndsWith(".no") ? "nb-NO" : "en-GB");Using the database in code
NodeStore (namespace Relatude.DB.Nodes) is the whole data API. In a request, inject
RelatudeCmsContext (namespace Relatude.DB.CMS.Web) and use its store. Code with no request uses
IRelatudeDBAccessor.
| Way | Reads as | Use it in |
|---|---|---|
RelatudeCmsContext.DB |
the visitor: their read access, the request's culture, published content only | controllers, views and services in a request |
RelatudeCmsContext.Admin |
the master admin: no read access or publication filter | trusted code in a request; never to render content for the visitor |
IRelatudeDBAccessor.Database (namespace Relatude.CMS.Engine) |
the master admin, the store's default context; .Context.User(id).Create() gives a store that reads as a user |
hosted services and other code with no request |
RelatudeDBRuntime.Database (namespace Relatude.DB.NodeServer) |
the master admin | places without dependency injection |
public class NewsController(RelatudeCmsContext ctx) : Controller {
public IActionResult Index() => View(ctx.DB.Query<IArticle>().Page(0, 10).Execute());
}
public class Warmup(IRelatudeDBAccessor db) : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stop) {
while (!db.IsOpen) await Task.Delay(500, stop); // false until the database has opened
var published = db.Database.Query<IArticle>().Count(); // as the master admin
}
}There is one RelatudeCmsContext per request, and each member is worked out the first time you use
it:
| Member | What it is |
|---|---|
DB, Admin |
the store as the visitor, and as the master admin |
User, UserId, IsAuthenticated |
the signed-in visitor's ICMSUser node, or null |
CultureCode |
the culture the request reads in |
Url, Node |
what the URL addresses, and that node read as the visitor |
AccessDenied, CanRead(key) |
whether the URL's page or file exists but the visitor may not read it, and whether the visitor may read a node |
Content, Render |
the page with its template, and what renders it (chapter 12) |
GetUrl(node), GetUrl(file) |
URLs for links, read through DB |
Http, IsOpen |
the HttpContext, and whether the database is open yet |
A read by id (Get, TryGet) ignores read access, while queries and relation ends apply it. Before
you show a node the visitor reached by id, check CanRead.
Hooking in from a library
A library should not set OnStoreInit. That option belongs to the host, and a later assignment
would replace it. Register an IStoreInitializer service instead (namespace
Relatude.DB.NodeServer). The server calls every one for each store before it opens:
public sealed class AuditInitializer : IStoreInitializer {
public void Initialize(NodeStore store) => store.RegisterTransactionPlugin(new AuditPlugin());
}
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStoreInitializer, AuditInitializer>());Two more services cover the other moments. An IDatamodelInitializer adds the library's node types
to every datamodel, after OnDatamodelInit. An IStoreOpenHandler runs on a background thread once
a store is open, to seed data or start work. A CMS module brings all of this, and more, in one class
(chapter 18).
What AddRelatudeCMS registers
Besides RelatudeCmsContext (scoped) and IRelatudeDBAccessor (singleton), two singletons are worth
knowing: ContentResolver, which you subclass to find content another way
(chapter 7), and ICmsLicense, which says what the license carries
(chapter 18). To replace a service, register your own before or after
AddRelatudeCMS().