Skip to content

Entities

Entities are supercharged database tables. They enable you to write assertions on the rows in the table itself as well as referenced entities.

Example

If you have a schema like this

schema.sql
CREATE TABLE authors (
    id UUID PRIMARY KEY,
    name text NOT NULL
);

CREATE TABLE posts (
    id UUID PRIMARY KEY,
    name text NOT NULL,
    author_id UUID NOT NULL REFERENCES authors (id)
);

then you would model the posts table as

public class Post : Entity
{
    public override TableReference TableReference => new("posts");

    public override IReadOnlyCollection<ColumnReference> PrimaryKeyColumns =>
        new[] { Id };

    public ColumnReference Id => ColumnReference.String(this, "id");

    public ColumnReference AuthorId => ColumnReference.String(this, "author_id");

    public ColumnReference Name => ColumnReference.String(this, "name");

    public EntityReference<Author> Author =>
        new(author => author.Id.IsEqualTo(AuthorId));
}

The TableReference property specifies the name of the table, in this case posts. Since the id column is used as the primary key in the schema, it should also be set in the PrimaryKey property. You could also use a different primary key such as AuthorId and Name, but you have to ensure that the primary key is unique for all rows. Otherwise the behaviour is undefined.

Use Cases

Asserting equality of two entities

Entities can be equality compared using IsEqualTo and IsNotEqualTo. They are equal when they are instances of the same entity type and have the same primary key column values. An exception is thrown when you try to equality compare entities of different types, because they can never be equal.

postA.IsEqualTo(postB)
postA.IsNotEqualTo(postB)

Asserting property holds for a set of instances of an entity

Use Check.For(times, ...) to assert that a constraint holds for a set of instances of an entity.

Possible times are

  • Times.All
  • Times.None
  • Times.AtLeastOnce
  • Times.AtLeast(n)
  • Times.AtMostOnce
  • Times.AtMost(n)
  • Times.Once
  • Times.Exactly(n)
  • Times.Between(a, b)

By combining multiple For constraints and time values you can model all the different relations between entities. Using times values with an upper bound higher than five is not recommended for performance reasons.

[Assertion]
public Constraint NameIsFilled()
{
    return Check.For(Times.All, (Post post) => post.Name.IsNotNull());
}
[Assertion]
public Constraint NameIsFilled()
{
    return Check.For(Times.None, (Post post) => post.Name.IsNull());
}
[Assertion]
public Constraint EveryAuthorHasAtLeastOneBlogPost()
{
    return Check.For(Times.All (Author author) =>
        Check.For(Times.AtLeastOnce, (Post post) =>
            post.AuthorId.IsEqualTo(author.Id))
    );
}

Asserting property on all combinations of different entities of the same type

If an assertion needs to check that a property hold for all combinations of different entities of the same type such as

[Assertion]
public Constraint AuthorsCanNotPublishTwoBlogPostsWithTheSameName()
{
    return Check.For(Times.All, (Post postA) =>
        Check.For(Times.All, (Post postB) =>
            postA.IsNotEqualTo(postB)
                .And(postA.AuthorId.IsEqualTo(postB.AuthorId))
                .Implies(postA.Name.IsNotEqualTo(postB.Name))
        )
    );
}
can be simplified using Check.ForAllDisjunct to
[Assertion]
public Constraint AuthorsCanNotPublishTwoBlogPostsWithTheSameName()
{
    return Check.ForAllDisjunct<Post>((postA, postB) =>
        postA.AuthorId.IsEqualTo(postB.AuthorId)
            .Implies(postA.Name.IsNotEqualTo(postB.Name))
    );
}