Skip to content

Foreign Keys

Marten is built on top of a relational database, so why not take advantage of those abilities where they still add value? In this case, Marten allows for a special kind of "Searchable" column that also adds a foreign key constraint to enforce referential integrity between document types.

One of our sample document types in Marten is the Issue class that has a couple properties that link to the id's of related User documents:

cs
public class Issue
{
    public Issue()
    {
        Id = Guid.NewGuid();
    }

    public string[] Tags { get; set; }

    public Guid Id { get; set; }

    public string Title { get; set; }

    public int Number { get; set; }

    public Guid? AssigneeId { get; set; }

    public Guid? ReporterId { get; set; }

    public Guid? BugId { get; set; }
    public string Status { get; set; }
}

snippet source | anchor

If I want to enforce referential integrity between the Issue document and the User documents, I can use this syntax shown below to configure Marten:

cs
var store = DocumentStore
    .For(_ =>
    {
        _.Connection("some database connection");

        // In the following line of code, I'm setting
        // up a foreign key relationship to the User document
        _.Schema.For<Issue>().ForeignKey<User>(x => x.AssigneeId);
    });

snippet source | anchor

With the configuration above, Marten will make an assignee_id field in the database table and build a foreign key constraint to the User document like so:

sql
ALTER TABLE public.mt_doc_issue
ADD CONSTRAINT mt_doc_issue_assignee_id_fkey FOREIGN KEY (assignee_id)
REFERENCES public.mt_doc_user (id);

CREATE INDEX mt_doc_issue_idx_assignee_id ON public.mt_doc_issue ("assignee_id");

And some other things you probably want to know about how this works internally:

Marten is smart enough to order the "upsert" operations to make the dependent documents be updated last. In the Issue referencing User example above, this means that if you create a new User and a new Issue in the same session, when you call IDocumentSession.SaveChanges()/SaveChangesAsync(), Marten will know to save the new user first so that the issue will not fail with referential integrity violations.

Foreign Keys to non-Marten tables

Marten can also create a foreign key to tables that are not managed by Marten. Continuing the our sample of Issue, we can create a foreign key from our Issue to our external bug tracking system:

cs
var store = DocumentStore
    .For(_ =>
    {
        _.Connection("some database connection");

        // Here we create a foreign key to table that is not
        // created or managed by marten
        _.Schema.For<Issue>().ForeignKey(i => i.BugId, "bugtracker", "bugs", "id");
    });

snippet source | anchor

With the configuration above, Marten will generate a foreign key constraint from the Issue to a table in the bug-tracker schema called bugs on the id column. The constraint would be defined as:

sql
ALTER TABLE public.mt_doc_issue
ADD CONSTRAINT mt_doc_issue_bug_id_fkey FOREIGN KEY (bug_id)
REFERENCES bug-tracker.bugs (id);

Cascading deletes

Marten can also cascade deletes on the foreign keys that it creates. The ForeignKeyDefinition has a CascadeDeletes property that indicates whether the foreign key should enable cascading deletes. One way to enable this is to use a configuration function like:

cs
var store = DocumentStore
    .For(_ =>
    {
        _.Connection("some database connection");

        _.Schema.For<Issue>().ForeignKey<User>(x => x.AssigneeId, fkd => fkd.OnDelete = CascadeAction.Cascade);
    });

snippet source | anchor

Configuring with Attributes

You can optionally configure properties or fields as foreign key relationships with the [ForeignKey] attribute:

cs
public class Issue
{
    public Issue()
    {
        Id = Guid.NewGuid();
    }

    public Guid Id { get; set; }

    [ForeignKey(typeof(User))]
    public Guid UserId { get; set; }

    public Guid? OtherUserId { get; set; }
}

snippet source | anchor

Released under the MIT License.