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, EmbeddedMapHere 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:
Metais 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; }.- Leave
IdasGuid.Emptyand the store assigns one on insert, or set it yourself first. - 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.