For a complete walkthrough of defining a model, see Modeling.
If you were building an application using a relational database, then you would define tables to represent your model. In Jinaga, you write records.
[FactType("Blog.Site")]
public record Site(User creator, DateTime createdAt) { }
Database tables can easily grow to 15 or 20 columns. Facts, however, are often quite small. As a result, a model will have many facts.
Begin with an attribute to define the fact type. The type is a string that describes the part that the fact plays within the model. The format of the string is completely up to you, but here are conventions that I recommend.
Name a fact with a series of words or short phrases separated by periods. Start every fact type with a namespace that reflects the application or domain. In my examples, that is usually a short description, like "Blog". In my products, it's often a brand name.
The next segment is an entity name. For the fact representing the entity itself, this is the last segment.
[FactType("Blog.Site")]
public record Site(User creator, DateTime createdAt) { }
Other facts further describe the entity. They use a third segment to describe their function. For example, they may represent a property value.
[FactType("Blog.Site.Domain")]
public record SiteDomain(Site site, string value, SiteDomain[] prior) { }
Or they might represent an action that has been performed on the entity. These will typically use a past-tense verb.
[FactType("Blog.Site.Deleted")]
public record SiteDeleted(Site site, DateTime deletedAt) {}
A fact might also represent the relationship between two entities. Or it might be a step in a workflow. Give your fact a name that documents how it will be used.
The parameters of the record contain fields and predecessors. Fields are the simple data types, whereas predecessors are other facts. The following types of fields are supported:
The values of fields are used to uniquely identify facts.
Two facts that have all the same type and parameters are considered identical.
You should therefore always provide a unique field to distinguish entity facts.
A DateTime
is often sufficient, especially if the fact is related to a user.
[FactType("Blog.Site")]
public record Site(User creator, DateTime createdAt) { }
Sometimes the distinguishing value will be a GUID. And other times it will be a natural key, a unique value appearing in the domain. However, you should not use an incrementing number for the distinguishing field. There is no guaranteed way to generate a unique sequential ID from an edge device.
To relate two facts to one another, pass one of them as a parameter to the other. The parameter fact will be created first. We call that a predecessor.
A predecessor can be written as a non-nullable reference, a nullable reference, or an array. A non-nullable reference represents a parent or owner.
[FactType("Blog.Post")]
public record Post(Site site, User author, DateTime createdAt) { }
A nullable reference represents an optional relationship.
[FactType("Blog.Author.Image")]
public record AuthorImage(User author, Image? image, AuthorProfile[] prior) { }
And an array represents either an immutable collection or prior state. An example of prior state appears in the previous definition of AuthorImage. That is used to implement the Mutable Property pattern.
Facts are immutable, so their predecessors cannot change. If you want to model a relationship that can change, then you have to invert it. You can add successors to a fact, but you cannot add predecessors. And so, facts don't contain their children; they contain their parents.
Since facts are so small, it is common to write several facts in the same file. Unlike classes, facts will usually have no properties or methods, only the parameters in their primary constructor. You can declare a very large model in a surprisingly small file.
However, you may want to find seams in your model that let you break it into modules. Separate modules into their own files for better maintainability. You can even move module files closer to the view models and services that depend upon them, breaking your entire application vertically into functional areas.
A good seam to consider splitting along is which kind of user creates facts in that module. For example, a blog author will create sites, posts, set post titles, and publish them. But a reader will comment, share, and favorite posts. This suggests two different modules for this application. The reader module would depend upon the author module, as it uses facts such as site and post as predecessors.