Schema Feature Extensions
New in Marten 2.4.0 is the ability to add additional features with custom database schema objects that simply plug into Marten's [schema management facilities)[/schema/migrations). The key abstraction is the IFeatureSchema
interface shown below:
/// <summary>
/// Defines the database objects for a named feature within your
/// Marten application
/// </summary>
public interface IFeatureSchema
{
/// <summary>
/// Any document or feature types that this feature depends on. Used
/// to intelligently order the creation and scripting of database
/// schema objects
/// </summary>
/// <returns></returns>
IEnumerable<Type> DependentTypes();
/// <summary>
/// All the schema objects in this feature
/// </summary>
ISchemaObject[] Objects { get; }
/// <summary>
/// Identifier by type for this feature. Used along with the DependentTypes()
/// collection to control the proper ordering of object creation or scripting
/// </summary>
Type StorageType { get; }
/// <summary>
/// Really just the filename when the SQL is exported
/// </summary>
string Identifier { get; }
/// <summary>
/// Write any permission SQL when this feature is exported to a SQL
/// file
/// </summary>
/// <param name="rules"></param>
/// <param name="writer"></param>
void WritePermissions(DdlRules rules, TextWriter writer);
}
Not to worry though, Marten comes with a base class that makes it a bit simpler to build out new features. Here's a very simple example that defines a custom table with one column:
public class FakeStorage : FeatureSchemaBase
{
private readonly StoreOptions _options;
public FakeStorage(StoreOptions options) : base("fake")
{
_options = options;
}
protected override IEnumerable<ISchemaObject> schemaObjects()
{
var table = new Table(new DbObjectName(_options.DatabaseSchemaName, "mt_fake_table"));
table.AddColumn("name", "varchar");
yield return table;
}
}
Now, to actually apply this feature to your Marten applications, use this syntax:
var store = DocumentStore.For(_ =>
{
// Creates a new instance of FakeStorage and
// passes along the current StoreOptions
_.Storage.Add<FakeStorage>();
// or
_.Storage.Add(new FakeStorage(_));
});
Do note that when you use the Add<T>()
syntax, Marten will pass along the current StoreOptions
to the constructor function if there is a constructor with that signature. Otherwise, it uses the no-arg constructor.
While you can directly implement the ISchemaObject
interface for something Marten doesn't already support, it's probably far easier to just configure one of the existing implementations shown in the following sections.
Table
Function
Sequence
Table
Postgresql tables can be modeled with the Table
class as shown in this example from the event store inside of Marten:
internal class EventsTable: Table
{
public EventsTable(EventGraph events): base(new DbObjectName(events.DatabaseSchemaName, "mt_events"))
{
AddColumn(new EventTableColumn("seq_id", x => x.Sequence)).AsPrimaryKey();
AddColumn(new EventTableColumn("id", x => x.Id)).NotNull();
AddColumn(new StreamIdColumn(events));
AddColumn(new EventTableColumn("version", x => x.Version)).NotNull();
AddColumn<EventJsonDataColumn>();
AddColumn<EventTypeColumn>();
AddColumn(new EventTableColumn("timestamp", x => x.Timestamp))
.NotNull().DefaultValueByString("(now())");
AddColumn<TenantIdColumn>();
AddColumn<DotNetTypeColumn>().AllowNulls();
AddIfActive(events.Metadata.CorrelationId);
AddIfActive(events.Metadata.CausationId);
AddIfActive(events.Metadata.Headers);
if (events.TenancyStyle == TenancyStyle.Conjoined)
{
ForeignKeys.Add(new ForeignKey("fkey_mt_events_stream_id_tenant_id")
{
ColumnNames = new string[]{"stream_id", TenantIdColumn.Name},
LinkedNames = new string[]{"id", TenantIdColumn.Name},
LinkedTable = new DbObjectName(events.DatabaseSchemaName, "mt_streams")
});
Indexes.Add(new IndexDefinition("pk_mt_events_stream_and_version")
{
IsUnique = true,
Columns = new string[]{"stream_id", TenantIdColumn.Name, "version"}
});
}
else
{
ForeignKeys.Add(new ForeignKey("fkey_mt_events_stream_id")
{
ColumnNames = new string[]{"stream_id"},
LinkedNames = new string[]{"id"},
LinkedTable = new DbObjectName(events.DatabaseSchemaName, "mt_streams"),
OnDelete = CascadeAction.Cascade
});
Indexes.Add(new IndexDefinition("pk_mt_events_stream_and_version")
{
IsUnique = true,
Columns = new string[]{"stream_id", "version"}
});
}
Indexes.Add(new IndexDefinition("pk_mt_events_id_unique")
{
Columns = new string[]{"id"},
IsUnique = true
});
AddColumn<IsArchivedColumn>();
}
internal IList<IEventTableColumn> SelectColumns()
{
var columns = new List<IEventTableColumn>();
columns.AddRange(Columns.OfType<IEventTableColumn>());
var data = columns.OfType<EventJsonDataColumn>().Single();
var typeName = columns.OfType<EventTypeColumn>().Single();
var dotNetTypeName = columns.OfType<DotNetTypeColumn>().Single();
columns.Remove(data);
columns.Insert(0, data);
columns.Remove(typeName);
columns.Insert(1, typeName);
columns.Remove(dotNetTypeName);
columns.Insert(2, dotNetTypeName);
return columns;
}
private void AddIfActive(MetadataColumn column)
{
if (column.Enabled)
{
AddColumn(column);
}
}
}
Function
Postgresql functions can be managed by creating a subclass of the Function
base class as shown below from the big "append event" function in the event store:
// TODO: Add sample
Sequence
Postgresql sequences can be managed with this usage:
var sequence = new Sequence(new DbObjectName(DatabaseSchemaName, "mt_events_sequence"))
{
Owner = eventsTable.Identifier,
OwnerColumn = "seq_id"
};