Relatude Help

11.1 The same model with facet interfaces

The model above declares everything on each type, so each type reads on its own. In a real project you would move the shared properties into facet interfaces, as in §2. Most of the repetition goes, and queries across types get simple.

One change comes with it: the names (Name, Title, FullName, FileName) become one INamedNode.Title. A shared name is worth it: it is what makes one search box and one "recently changed" feed possible. The enums, OpeningHours and the relation classes stay as above.

// ── Facets ───────────────────────────────────────

public interface INamedNode {
    Guid Id { get; set; }
    NodeMeta Meta { get; }

    [StringProperty(Indexed = true, IndexedByWords = true, PrefixSearch = true, DisplayName = true)]
    string Title { get; set; }

    [AddressProperty]    string Slug { get; }
    [CreatedUtcProperty] DateTime CreatedUtc { get; }
    [ChangedUtcProperty] DateTime ChangedUtc { get; }
}

public interface IDescribed {
    [HtmlProperty(IndexedByWords = true, IndexedBySemantic = true)]
    string Description { get; set; }
}

public interface ILocatable {
    [GeoCoordinateProperty(Indexed = true)]
    GeoCoordinate Location { get; set; }

    [StringProperty(Indexed = true, MaxLength = 2)]
    string CountryCode { get; set; }
}

public interface ITagged {
    [StringArrayProperty(Indexed = true)]
    string[] Tags { get; set; }
}

// ── Composed node types ──────────────────────────

public interface IVenue : INamedNode, IDescribed, ILocatable, ITagged {
    [IntegerProperty(Indexed = true, MinValue = 0)] int Capacity { get; set; }
    [IntegerProperty(Indexed = true)]               VenueKind Kind { get; set; }
    [BooleanProperty(Indexed = true)]               bool IsAccessible { get; set; }
    [FileProperty]                                  FileValue Photo { get; set; }

    [EmbeddedMapProperty(KeyProperty = nameof(OpeningHours.DayCode))]
    EmbeddedMap<string, OpeningHours> Hours { get; }

    VenueTree.Parent     Parent { get; }
    VenueTree.Children   Halls  { get; }
    EventsAtVenue.Events Events { get; }
}

public interface IEvent : INamedNode, IDescribed, ITagged {
    [DateTimeProperty(Indexed = true)]                          DateTime StartsUtc { get; set; }
    [TimeSpanProperty(Indexed = true, MaxValue = "1.00:00:00")] TimeSpan Duration { get; set; }
    [IntegerProperty(Indexed = true)]                           EventStatus Status { get; set; }

    [DecimalProperty(Indexed = true, MinValue = "0", DefaultValue = "0",
                     FacetRangePowerBase = 2.0, FacetRangeCount = 6)]
    decimal Price { get; set; }

    [ReferenceProperty(Indexed = true)]  Reference<IMediaAsset> Cover    { get; }
    [ReferencesProperty(Indexed = true)] References<IOrganizer> Sponsors { get; }

    EventsAtVenue.Venue  Venue     { get; }
    OrganizerEvents.Host Host      { get; }
    Attendance.Attendees Attendees { get; }
}

public interface IAttendee : INamedNode, ILocatable {
    [StringProperty(Indexed = true, UniqueValues = true)]
    string Email { get; set; }

    Attendance.Events Attending { get; }
    Friends.Peers     Friends   { get; }
}

public interface IOrganizer : INamedNode, IDescribed {
    [StringProperty(UniqueValues = true, IgnoreDuplicateEmptyValues = true)]
    string ContactEmail { get; set; }

    OrganizerEvents.Events Events { get; }
}

public interface IMediaAsset : INamedNode {
    [FileProperty] FileValue File { get; set; }
}

Id, Meta, Title, Slug and the timestamps are now written once, in INamedNode, and every type inherits them. Location is written once and shared by venues and attendees.

The payoff comes at query time:

// one search box across venues, events, organizers and assets
db.Query<INamedNode>().WhereSearch("nordic jazz", semanticRatio: 0.5).Execute();

// one map query across venues and attendees
db.Query<ILocatable>().Where(x => x.Location.IsWithin(oslo, 5_000)).Execute();

// one tag cloud across venues and events
db.Query<ITagged>().Facets().AddValueFacet(x => x.Tags).Execute();

// one "recently changed" feed across everything
db.Query<INamedNode>().OrderByDescending(x => x.ChangedUtc).Page(0, 50).Execute();

Each of these is a single indexed query. Without the facet interfaces, each would be one query per type, merged and sorted again in memory, and changed every time you add a type.

Mind the two constraints: because INamedNode declares Title, no other facet may declare a Title, and no composed type may declare it again.