Skip to content

Document Hierarchies

Marten now allows you to specify that hierarchies of document types should be stored in one table and allow you to query for either the base class or any of the subclasses.

One Level Hierarchies

To make that concrete, let's say you have a document type named User that has a pair of specialized subclasses called SuperUser and AdminUser. To use the document hierarchy storage, we need to tell Marten that SuperUser and AdminUser should just be stored as subclasses of User like this:

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

    _.Schema.For<User>()
        // generic version
        .AddSubClass<AdminUser>()

        // By document type object
        .AddSubClass(typeof(SuperUser));
});

using (var session = store.QuerySession())
{
    // query for all types of User and User itself
    session.Query<User>().ToList();

    // query for only SuperUser
    session.Query<SuperUser>().ToList();
}

snippet source | anchor

With the configuration above, you can now query by User and get AdminUser and SuperUser documents as part of the results, or query directly for any of the subclasses to limit the query.

The best description of what is possible with hierarchical storage is to read the acceptance tests for this feature.

There's a couple things to be aware of with type hierarchies:

  • A document type that is either abstract or an interface is automatically assumed to be a hierarchy
  • If you want to use a concrete type as the base class for a hierarchy, you will need to explicitly configure that by adding the subclasses as shown above
  • At this point, you can only specify "Searchable" fields on the top, base type
  • The subclass document types must be convertible to the top level type. As of right now, Marten does not support "structural typing", but may in the future
  • Internally, the subclass type documents are also stored as the parent type in the Identity Map mechanics. Many, many hours of banging my head on my desk were required to add this feature.

Multi Level Hierarchies

TIP

Use the AddSubClassHierarchy() if you want to be able to query against intermediate levels of a document hierarchy. Calling AddSubClass<T>() just directly adds a base class to the top level document type.

Say you have a document type named ISmurf that is implemented by Smurf. Now, say the latter has a pair of specialized subclasses called PapaSmurf and PapySmurf and that both implement IPapaSmurf and that PapaSmurf has the subclass BrainySmurf like so:

cs
public interface ISmurf
{
    string Ability { get; set; }
    Guid Id { get; set; }
}

public class Smurf: ISmurf
{
    public string Ability { get; set; }
    public Guid Id { get; set; }
}

public interface IPapaSmurf: ISmurf
{
}

public class PapaSmurf: Smurf, IPapaSmurf
{
}

public class PapySmurf: Smurf, IPapaSmurf
{
}

public class BrainySmurf: PapaSmurf
{
}

snippet source | anchor

If you wish to query over one of hierarchy classes and be able to get all of its documents as well as its subclasses, first you will need to map the hierarchy like so:

cs
public query_with_inheritance(ITestOutputHelper output)
{
    _output = output;
    StoreOptions(_ =>
    {
        _.Schema.For<ISmurf>()
            .AddSubClassHierarchy(typeof(Smurf), typeof(PapaSmurf), typeof(PapySmurf), typeof(IPapaSmurf),
                typeof(BrainySmurf));

        // Alternatively, you can use the following:
        // _.Schema.For<ISmurf>().AddSubClassHierarchy();
        // this, however, will use the assembly
        // of type ISmurf to get all its' subclasses/implementations.
        // In projects with many types, this approach will be unadvisable.

        _.Connection(ConnectionSource.ConnectionString);
        _.AutoCreateSchemaObjects = AutoCreate.All;

        _.Schema.For<ISmurf>().GinIndexJsonData();
    });
}

snippet source | anchor

Note that if you wish to use aliases on certain subclasses, you could pass a MappedType, which contains the type to map and its alias. Since Type implicitly converts to MappedType and the methods takes in params MappedType[], you could use a mix of both like so:

cs
_.Schema.For<ISmurf>()
    .AddSubClassHierarchy(
        typeof(Smurf),
        new MappedType(typeof(PapaSmurf), "papa"),
        typeof(PapySmurf),
        typeof(IPapaSmurf),
        typeof(BrainySmurf)
    );

snippet source | anchor

Now you can query the "complex" hierarchy in the following ways:

cs
[Fact]
public async Task get_all_subclasses_of_a_subclass()
{
    var smurf = new Smurf {Ability = "Follow the herd"};
    var papa = new PapaSmurf {Ability = "Lead"};
    var brainy = new BrainySmurf {Ability = "Invent"};
    theSession.Store(smurf, papa, brainy);

    await theSession.SaveChangesAsync();

    theSession.Query<Smurf>().Count().ShouldBe(3);
}

[Fact]
public async Task get_all_subclasses_of_a_subclass2()
{
    var smurf = new Smurf {Ability = "Follow the herd"};
    var papa = new PapaSmurf {Ability = "Lead"};
    var brainy = new BrainySmurf {Ability = "Invent"};
    theSession.Store(smurf, papa, brainy);

    await theSession.SaveChangesAsync();

    theSession.Logger = new TestOutputMartenLogger(_output);

    theSession.Query<PapaSmurf>().Count().ShouldBe(2);
}

[Fact]
public async Task get_all_subclasses_of_a_subclass_with_where()
{
    var smurf = new Smurf {Ability = "Follow the herd"};
    var papa = new PapaSmurf {Ability = "Lead"};
    var brainy = new BrainySmurf {Ability = "Invent"};
    theSession.Store(smurf, papa, brainy);

    await theSession.SaveChangesAsync();

    theSession.Query<PapaSmurf>().Count(s => s.Ability == "Invent").ShouldBe(1);
}

[Fact]
public async Task get_all_subclasses_of_a_subclass_with_where_with_camel_casing()
{
    StoreOptions(_ =>
    {
        _.Schema.For<ISmurf>()
            .AddSubClassHierarchy(typeof(Smurf), typeof(PapaSmurf), typeof(PapySmurf), typeof(IPapaSmurf),
                typeof(BrainySmurf));

        // Alternatively, you can use the following:
        // _.Schema.For<ISmurf>().AddSubClassHierarchy();
        // this, however, will use the assembly
        // of type ISmurf to get all its' subclasses/implementations.
        // In projects with many types, this approach will be undvisable.

        _.UseDefaultSerialization(EnumStorage.AsString, Casing.CamelCase);

        _.Connection(ConnectionSource.ConnectionString);
        _.AutoCreateSchemaObjects = AutoCreate.All;

        _.Schema.For<ISmurf>().GinIndexJsonData();
    });

    var smurf = new Smurf {Ability = "Follow the herd"};
    var papa = new PapaSmurf {Ability = "Lead"};
    var brainy = new BrainySmurf {Ability = "Invent"};
    theSession.Store(smurf, papa, brainy);

    await theSession.SaveChangesAsync();

    theSession.Query<PapaSmurf>().Count(s => s.Ability == "Invent").ShouldBe(1);
}

[Fact]
public async Task get_all_subclasses_of_an_interface()
{
    var smurf = new Smurf {Ability = "Follow the herd"};
    var papa = new PapaSmurf {Ability = "Lead"};
    var papy = new PapySmurf {Ability = "Lead"};
    var brainy = new BrainySmurf {Ability = "Invent"};
    theSession.Store(smurf, papa, brainy, papy);

    await theSession.SaveChangesAsync();

    theSession.Query<IPapaSmurf>().Count().ShouldBe(3);
}

snippet source | anchor

Released under the MIT License.