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:
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();
}
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:
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
{
}
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:
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();
});
}
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:
_.Schema.For<ISmurf>()
.AddSubClassHierarchy(
typeof(Smurf),
new MappedType(typeof(PapaSmurf), "papa"),
typeof(PapySmurf),
typeof(IPapaSmurf),
typeof(BrainySmurf)
);
Now you can query the "complex" hierarchy in the following ways:
[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);
}