Relatude Help

2 Your first node type

The three namespaces a model file usually imports:

using Relatude.DB.Common;       // FileValue, GeoCoordinate, NodeKey
using Relatude.DB.Datamodels;   // NodeMeta, RevisionType
using Relatude.DB.Nodes;        // attributes, relation bases, Reference, References, EmbeddedMap

Here is IOrganizer, a company that runs events. The interface is the whole node type. There is no class, and none is needed:

namespace VenueApp.Models;

public interface IOrganizer {
    Guid Id { get; set; }

    [StringProperty(Indexed = true, IndexedByWords = true, MaxLength = 200, DisplayName = true)]
    string Name { get; set; }

    [StringProperty(UniqueValues = true, MaxLength = 254)]
    string ContactEmail { get; set; }

    [StringProperty(Indexed = true, UniqueValues = true, RegularExpression = @"^[a-z0-9-]+$")]
    string Slug { get; set; }

    [CreatedUtcProperty]
    DateTime CreatedUtc { get; }    // filled in by the store

    NodeMeta Meta { get; }          // read-only on the interface
}

Create, store and query it:

var org = db.Create<IOrganizer>();      // a generated proxy that implements IOrganizer
org.Name = "Nordic Live AS";
org.Slug = "nordic-live";               // required: the pattern does not match an empty value
org.ContactEmail = "hello@nordiclive.no";
db.Insert(org);

var again = db.Get<IOrganizer>(org.Id);
var all   = db.Query<IOrganizer>().Where(o => o.Name.StartsWith("Nordic")).Execute();

Three rules for interface node types:

  1. Meta is get-only, and so are relation, reference and embedded properties and the marker members ([CreatedUtcProperty], [AddressProperty] and the like, §4). The store provides them, and you never assign them. Scalar properties are { get; set; }.
  2. Leave Id as Guid.Empty and the store assigns one on insert, or set it yourself first.
  3. Put attributes on the interface. A property is defined by the type that first declares it, so attributes on a class member that implements it are ignored. The one exception is [PropertyOverride] (§3.1).

[Exclude] leaves a type or a property out of the datamodel.

  • Why interfaces are the better default — An interface model is shorter, has no initialisation to forget and, most important, composes.
  • Multiple inheritance with interfaces — The engine records every interface a type implements as a parent node type. So you can define small, focused facet interfaces:
  • Querying across the hierarchy — Query<T>() matches T and every type that inherits from it. So you can query a facet interface directly and get mixed results:
  • Two constraints to design around — The model builder refuses anything ambiguous. It checks these two rules when it builds the model, so you find out at startup: Two parent interfaces may not declare the same property name. If ILocatable and ITagged both…
  • When you do want a class — Classes are fully supported. Reach for one when you want to create nodes with new without a store (seed data, tests, an import job), want behaviour on the type, or deserialise straight into it from an external feed. A…
  • Optional: stable type ids — Without an id, the engine derives one from the type's full name. Renaming the type or moving it to another namespace then gives it a new identity, and the engine sees a new type with no data. Pin [Node(Id = …)] once, at…
  • Optional: an icon for the type — [Node(Icon = …)] sets the icon a type is shown with, in the admin UI and in applications built on the model, such as a CMS. Use a name from the icon library the server carries, written set:name, or the source of an SVG…
  • Optional: a title and a description — [Node(Title = …, Description = …)] says what people call the type and what it is for. Applications built on the model show them: the Relatude CMS, for example, when an editor chooses what kind of content to create.