Relatude Help

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() adds IItem, IPage, ISite, the page tree RTree, ITemplate and the template kinds, with the Relatude.DB types they build on (users, groups, collections, cultures).
  • dm.AddNamespace<IArticle>() adds every type in the namespace of IArticle. dm.Add<IArticle>() adds that type and the types it refers to.
  • The CMS's users and groups, ICMSUser and ICMSUserGroup, 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 }
]
  • Reference names the assembly; null is 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().