Correspondence Tutorial

This tutorial walks you through using Correspondence to model an application. It is written from the perspective of a .NET developer. To get the most out of this tutorial, start up Visual Studio and follow along.

Setup

In this lesson, we will write unit tests to exercise a model.
  • Create a blank solution called "Tutorial".
  • Add a Class Library project called "Tutorial.Model".
  • Delete Class1.cs.
  • Right-click the project and "Manage NuGet Packages". Search for Correspondence.Model
  • Click the "Transform All Templates" button.
  • Add a Test project called "Tutorial.UnitTest".
  • Manage NuGet Packages. Search for Correspondence.Desktop.UnitTest
  • Delete UnitTest1.cs.
  • Right-click Tutorial.UnitTest and add a reference to the project Tutorial.Model.
  • Add the namespace reference "using Tutorial.Model;" to ModelTest.cs.
  • The solution should now compile.
Press Ctrl+R, A, or select Test, Run, All Tests in Solution. Both tests should pass.

Identity

A fact is immutable. You set its fields during construction. From that point on, it cannot be changed. Those fields identify the fact.

Open the model.fact file. Notice the definition of the fact Individual, in particular the key section.

namespace Tutorial.Model;

fact Individual {
key:
    string anonymousId;
}

This means that an Individual is uniquely identified by its anonymous ID. Let's write a new test to verify this. In ModelTest.cs, add a new test method.

[TestMethod]
public async Task IndividualIsUniquelyIdentified()
{
    Individual meFirst = await _communityFlynn.AddFactAsync(
        new Individual("my id"));
    Individual meAgain = await _communityFlynn.AddFactAsync(
        new Individual("my id"));

    Assert.AreSame(meFirst, meAgain);
    Assert.AreEqual("my id", meFirst.AnonymousId);
}

The test initializes a couple of communities. We're just borrowing Flynn's. When you add a fact to the community, you pass in a prototype. It returns the actual fact. If all components of the key are the same, then it will return the same fact. You can access the fields in the key with read-only properties. But you cannot change the fields in the key; they are immutable.

What do you think will happen if a fact has no components in its key? Define this fact:

fact Singleton {
key:
}

Click the "Transform All Templates" button. Then write this unit test:

[TestMethod]
public async Task SingletonHasNoKeyFields()
{
    Singleton one = await _communityFlynn.AddFactAsync(new Singleton());
    Singleton two = await _communityFlynn.AddFactAsync(new Singleton());

    Assert.AreSame(one, two);
}

So you can see that a fact with no key fields is a singleton. There can be only one instance of the fact, since there is nothing to distinguish it from other facts of the same type.

But what if you want instances to be unique, and you don't have a natural identifier? Just use the keyword "unique" in the key.

fact Distinct {
key:
    unique;
}
[TestMethod]
public async Task UniqueKeyIsDistinct()
{
    Distinct one = await _communityFlynn.AddFactAsync(new Distinct());
    Distinct two = await _communityFlynn.AddFactAsync(new Distinct());

    Assert.AreNotSame(one, two);
}
So the components of the key distinguish one fact from another. If there are no components, then that fact is a singleton. The "unique" keyword will make the fact unique.

Predecessors

The fields of a fact's key don't have to be simple data types. They can also be references to other facts. For example, define the following facts.
fact MessageBoard {
key:
    string topic;
}

fact Share {
key:
    Individual individual;
    MessageBoard messageBoard;
}
This fact records that a message board has been shared with an individual. As you might expect, this fact is identified by the two components of its key. And you can access predecessors with read-only properties.
[TestMethod]
public async Task PredecessorsArePartOfKey()
{
    await CreateIndividualsAsync();

    MessageBoard board = await _communityFlynn.AddFactAsync(
        new MessageBoard("Games"));
    Share one = await _communityFlynn.AddFactAsync(
        new Share(_individualFlynn, board));
    Share two = await _communityFlynn.AddFactAsync(
        new Share(_individualFlynn, board));

    Assert.AreSame(one, two);
    Assert.AreSame(board, one.MessageBoard);
}
A reference to another fact as part of the key is called a predecessor. That's because the predecessor must exist first. Notice in the previous unit test we had to create the message board before we could create the share.

Successors

Now let's define the Message fact. It has message board as a predecessor.

fact Message {
key:
    unique;
    MessageBoard messageBoard;
    string text;
}
Given a message board, it is useful to find all of the successor messages. To do that, we can write a query.
fact MessageBoard {
key:
    string topic;

query:
    Message* messages {
        Message m : m.messageBoard = this
    }
}

The colon (:) is read "such that". This query is "The set of all Messages m such that m's message board is this". This query will return all successor messages.

You can access the results of a query using a read-only collection property. For example you can access the messages in a message board. When you create a successor, it appears in the query results.

[TestMethod]
public async Task QueryReturnsAMessage()
{
    MessageBoard board = await _communityAlan.AddFactAsync(
        new MessageBoard("Games"));
    await _communityAlan.AddFactAsync(
        new Message(board, "Reindeer flotilla"));

    Assert.AreEqual(1, board.Messages.Count());
}

You can join sets within a query to walk the predecessor/successor graph. For example, starting at an individual, you can find all of the message boards that have been shared with that person. First, find all of the individual's shares (successors), and then find all of the shares' message boards (predecessors).

fact Individual {
key:
    string anonymousId;

query:
    MessageBoard* messageBoards {
        Share s : s.individual = this
        MessageBoard b : b = s.messageBoard
    }
}

This query will take all shares such that the share's individual is this, and then find all message boards such that it is the share's message board. Once you create a share, the message board will be returned from this query.

[TestMethod]
public async Task CanFindSharedMessageBoards()
{
    await CreateIndividualsAsync();

    MessageBoard board = await _communityAlan.AddFactAsync(
        new MessageBoard("Games"));
    await _communityAlan.AddFactAsync(new Share(_individualAlan, board));

    MessageBoard found = _individualAlan.MessageBoards.Single();
    Assert.AreSame(board, found);
}

A query will return the successors of a fact. You can join it with other sets to get the predecessors of those successors. In this way, you can zig-zag your way through the fact graph and find useful information. Facts that you add later will automatically appear in the query results.