Configuring Document Storage with StoreOptions
The StoreOptions
object in Marten is the root of all of the configuration for a DocumentStore
object. The static builder methods like DocumentStore.For(configuration)
or IServiceCollection.AddMarten(configuration)
are just syntactic sugar around building up a StoreOptions
object and passing that to the constructor function of a DocumentStore
:
public static DocumentStore For(Action<StoreOptions> configure)
{
var options = new StoreOptions();
configure(options);
return new DocumentStore(options);
}
The major parts of StoreOptions
are shown in the class diagram below:
For some explanation, the major pieces are:
EventGraph
-- The configuration for the Event Store functionality is all on theStoreOptions.Events
property. See the Event Store documentation for more information.DocumentMapping
-- This is the configuration for a specific document type including all indexes and rules for multi-tenancy, deletes, and metadata usageMartenRegistry
-- TheStoreOptions.Schema
property is aMartenRegistry
that provides a fluent interface to explicitly configure document storage by document typeIDocumentPolicy
-- Registered policies on aStoreOptions
object that apply to all document types. An example would be "all document types are soft deleted."MartenAttribute
-- Document type configuration can also be done with attributes on the actual document types
To be clear, the configuration on a single document type is applied in order by:
- Calling the static
ConfigureMarten(DocumentMapping)
method on the document type. See the section below on Embedding Configuration in Document Types - Any policies at the
StoreOptions
level - Attributes on the specific document type
- Explicit configuration through
MartenRegistry
The order of precedence is in the reverse order, such that explicit configuration takes precedence over policies or attributes.
TIP
While it is possible to mix and match configuration styles, the Marten team recommends being consistent in your approach to prevent confusion later.
Custom StoreOptions
It's perfectly valid to create your own subclass of StoreOptions
that configures itself, as shown below.
public class MyStoreOptions: StoreOptions
{
public static IDocumentStore ToStore()
{
return new DocumentStore(new MyStoreOptions());
}
public MyStoreOptions()
{
Connection(ConnectionSource.ConnectionString);
Serializer(new JsonNetSerializer { EnumStorage = EnumStorage.AsString });
Schema.For<User>().Index(x => x.UserName);
}
}
This strategy might be beneficial if you need to share Marten configuration across different applications or testing harnesses or custom migration tooling.
Explicit Document Configuration with MartenRegistry
While there are some limited abilities to configure storage with attributes, the most complete option right now is a fluent interface implemented by the MartenRegistry
that is exposed from the StoreOptions.Schema
property, or you can choose to compose your document type configuration in additional MartenRegistry
objects.
To use your own subclass of MartenRegistry
and place declarations in the constructor function like this example:
public class OrganizationRegistry: MartenRegistry
{
public OrganizationRegistry()
{
For<Organization>().Duplicate(x => x.OtherName);
For<User>().Duplicate(x => x.UserName);
}
}
To apply your new MartenRegistry
, just include it when you bootstrap the IDocumentStore
as in this example:
var store = DocumentStore.For(opts =>
{
opts.Schema.For<Organization>().Duplicate(x => x.Name);
opts.Schema.Include<OrganizationRegistry>();
opts.Connection(ConnectionSource.ConnectionString);
});
Do note that you could happily use multiple MartenRegistry
classes in larger applications if that is advantageous.
If you dislike using infrastructure attributes in your application code, you will probably prefer to use MartenRegistry.
Lastly, note that you can use StoreOptions.Schema
property for all configuration like this:
var store = DocumentStore.For(opts =>
{
opts.Connection(ConnectionSource.ConnectionString);
opts.Schema.For<Organization>()
.Duplicate(x => x.OtherName);
opts.Schema
.For<User>().Duplicate(x => x.UserName);
});
Custom Attributes
If there's some kind of customization you'd like to use attributes for that isn't already supported by Marten, you're still in luck. If you write a subclass of the MartenAttribute
shown below:
public abstract class MartenAttribute: Attribute
{
/// <summary>
/// Customize Document storage at the document level
/// </summary>
/// <param name="mapping"></param>
public virtual void Modify(DocumentMapping mapping) { }
/// <summary>
/// Customize the Document storage for a single member
/// </summary>
/// <param name="mapping"></param>
/// <param name="member"></param>
public virtual void Modify(DocumentMapping mapping, MemberInfo member) { }
/// <summary>
/// When used with the automatic type discovery (assembly scanning), this will be called
/// to make registrations to the Marten configuration with the type that this attribute
/// decorates
/// </summary>
/// <param name="discoveredType"></param>
/// <param name="options"></param>
public virtual void Register(Type discoveredType, StoreOptions options){}
}
And decorate either classes or individual field or properties on a document type, your custom attribute will be picked up and used by Marten to configure the underlying DocumentMapping
model for that document type. The MartenRegistry
is just a fluent interface over the top of this same DocumentMapping
model.
As an example, an attribute to add a gin index to the JSONB storage for more efficient adhoc querying of a document would look like this:
[AttributeUsage(AttributeTargets.Class)]
public class GinIndexedAttribute: MartenAttribute
{
public override void Modify(DocumentMapping mapping)
{
mapping.AddGinIndexToData();
}
}
Embedding Configuration in Document Types
Lastly, Marten can examine the document types themselves for a public static ConfigureMarten()
method and invoke that to let the document type make its own customizations for its storage. Here's an example from the unit tests:
public class ConfiguresItself
{
public Guid Id;
public static void ConfigureMarten(DocumentMapping mapping)
{
mapping.Alias = "different";
}
}
The DocumentMapping
type is the core configuration class representing how a document type is persisted or queried from within a Marten application. All the other configuration options end up writing to a DocumentMapping
object.
You can optionally take in the more specific DocumentMapping<T>
for your document type to get at some convenience methods for indexing or duplicating fields that depend on .Net Expression's:
public class ConfiguresItselfSpecifically
{
public Guid Id;
public string Name;
public static void ConfigureMarten(DocumentMapping<ConfiguresItselfSpecifically> mapping)
{
mapping.Duplicate(x => x.Name);
}
}
Document Policies
Document Policies enable convention-based customizations to be applied across the Document Store. While Marten has some existing policies that can be enabled, any custom policy can be introduced through implementing the IDocumentPolicy
interface and applying it on StoreOptions.Policies
or through using the Policies.ForAllDocuments(Action<DocumentMapping> configure)
shorthand.
The following sample demonstrates a policy that sets types implementing IRequireMultiTenancy
marker-interface to be multi-tenanted (see tenancy).
var store = DocumentStore.For(storeOptions =>
{
// Apply custom policy
storeOptions.Policies.OnDocuments<TenancyPolicy>();
The actual policy is shown below:
public interface IRequireMultiTenancy
{
}
public class TenancyPolicy: IDocumentPolicy
{
public void Apply(DocumentMapping mapping)
{
if (mapping.DocumentType.GetInterfaces().Any(x => x == typeof(IRequireMultiTenancy)))
{
mapping.TenancyStyle = TenancyStyle.Conjoined;
}
}
}
To set all types to be multi-tenanted, the pre-baked Policies.AllDocumentsAreMultiTenanted
could also have been used.
Remarks: Given the sample, you might not want to let tenancy concerns propagate to your types in a real data model.
Configuring the Database Schema
By default, Marten will put all database schema objects into the main public schema. If you want to override this behavior, use the StoreOptions.DocumentSchemaName
property when configuring your IDocumentStore
:
var store = DocumentStore.For(opts =>
{
opts.Connection("some connection string");
opts.DatabaseSchemaName = "other";
});
If you have some reason to place different document types into separate schemas, that is also supported and the document type specific configuration will override the StoreOptions.DatabaseSchemaName
value as shown below:
var store = DocumentStore.For(opts =>
{
opts.Connection("some connection string");
opts.DatabaseSchemaName = "other";
// This would take precedence for the
// User document type storage
opts.Schema.For<User>()
.DatabaseSchemaName("users");
});
Postgres Limits on Naming
Postgresql has a default limitation on the length of database object names (64). This can be overridden in a Postgresql database by setting the NAMEDATALEN property.
This can unfortunately have a negative impact on Marten's ability to detect changes to the schema configuration when Postgresql quietly truncates the name of database objects. To guard against this, Marten will now warn you if a schema name exceeds the NAMEDATALEN
value, but you do need to tell Marten about any non-default length limit like so:
var store = DocumentStore.For(_ =>
{
// If you have overridden NAMEDATALEN in your
// Postgresql database to 100
_.NameDataLength = 100;
});