The Admin Module DSL (M2) -- Backend Management
This DSL maps Diem's modules.yml and generator.yml to C# attributes. In Diem, a module declaration in YAML auto-generated an admin interface with list views, forms, filters, and batch actions. The CMF does the same, but with compile-time type safety and Blazor component generation.
AdminModule
The [AdminModule] attribute links an admin interface to an aggregate root:
[MetaConcept("AdminModule")]
[MetaConstraint("MustTargetAggregate",
"Aggregate != null",
Message = "Every admin module must target an aggregate root")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminModuleAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("Aggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string Aggregate { get; set; }
[MetaProperty("Icon")]
public string? Icon { get; set; }
[MetaProperty("MenuGroup")]
public string? MenuGroup { get; set; }
[MetaProperty("MenuOrder")]
public int MenuOrder { get; set; } = 100;
public AdminModuleAttribute(string name) => Name = name;
}[MetaConcept("AdminModule")]
[MetaConstraint("MustTargetAggregate",
"Aggregate != null",
Message = "Every admin module must target an aggregate root")]
[AttributeUsage(AttributeTargets.Class)]
public sealed class AdminModuleAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("Aggregate", "AggregateRoot", Multiplicity = Multiplicity.One)]
public string Aggregate { get; set; }
[MetaProperty("Icon")]
public string? Icon { get; set; }
[MetaProperty("MenuGroup")]
public string? MenuGroup { get; set; }
[MetaProperty("MenuOrder")]
public int MenuOrder { get; set; } = 100;
public AdminModuleAttribute(string name) => Name = name;
}Convention: if no [AdminModule] is declared for an aggregate root, one is auto-generated with default settings. The explicit attribute overrides presentation.
AdminField
The [AdminField] attribute controls how each property appears in list views and forms:
[MetaConcept("AdminField")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminFieldAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("ListVisible")]
public bool ListVisible { get; set; } = true;
[MetaProperty("ListSortable")]
public bool ListSortable { get; set; } = true;
[MetaProperty("FormGroup")]
public string? FormGroup { get; set; }
[MetaProperty("FormOrder")]
public int FormOrder { get; set; } = 100;
[MetaProperty("FormWidget")]
public FormWidgetType FormWidget { get; set; } = FormWidgetType.Auto;
public AdminFieldAttribute(string name) => Name = name;
}
[MetaConcept("AdminFilter")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminFilterAttribute : Attribute
{
[MetaProperty("FilterType")]
public FilterType FilterType { get; set; } = FilterType.Auto;
}
public enum FormWidgetType
{
Auto, // inferred from property type
TextBox,
TextArea,
RichText,
Dropdown,
DatePicker,
DateRangePicker,
MediaPicker,
RelationPicker,
Toggle,
ColorPicker,
JsonEditor
}
public enum FilterType { Auto, Equals, Contains, Range, MultiSelect }[MetaConcept("AdminField")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminFieldAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaProperty("ListVisible")]
public bool ListVisible { get; set; } = true;
[MetaProperty("ListSortable")]
public bool ListSortable { get; set; } = true;
[MetaProperty("FormGroup")]
public string? FormGroup { get; set; }
[MetaProperty("FormOrder")]
public int FormOrder { get; set; } = 100;
[MetaProperty("FormWidget")]
public FormWidgetType FormWidget { get; set; } = FormWidgetType.Auto;
public AdminFieldAttribute(string name) => Name = name;
}
[MetaConcept("AdminFilter")]
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminFilterAttribute : Attribute
{
[MetaProperty("FilterType")]
public FilterType FilterType { get; set; } = FilterType.Auto;
}
public enum FormWidgetType
{
Auto, // inferred from property type
TextBox,
TextArea,
RichText,
Dropdown,
DatePicker,
DateRangePicker,
MediaPicker,
RelationPicker,
Toggle,
ColorPicker,
JsonEditor
}
public enum FilterType { Auto, Equals, Contains, Range, MultiSelect }AdminAction
Batch operations on selected records, linking to existing [Command] definitions:
[MetaConcept("AdminAction")]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class AdminActionAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("Command", "Command", Multiplicity = Multiplicity.One)]
public string Command { get; set; }
[MetaProperty("Icon")]
public string? Icon { get; set; }
[MetaProperty("RequiresConfirmation")]
public bool RequiresConfirmation { get; set; } = true;
[MetaProperty("IsBatch")]
public bool IsBatch { get; set; } = true;
public AdminActionAttribute(string name) => Name = name;
}[MetaConcept("AdminAction")]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class AdminActionAttribute : Attribute
{
[MetaProperty("Name", Required = true)]
public string Name { get; }
[MetaReference("Command", "Command", Multiplicity = Multiplicity.One)]
public string Command { get; set; }
[MetaProperty("Icon")]
public string? Icon { get; set; }
[MetaProperty("RequiresConfirmation")]
public bool RequiresConfirmation { get; set; } = true;
[MetaProperty("IsBatch")]
public bool IsBatch { get; set; } = true;
public AdminActionAttribute(string name) => Name = name;
}Complete Admin Module Example
[AdminModule("Products", Aggregate = "Product",
Icon = "package", MenuGroup = "Catalog", MenuOrder = 1)]
[AdminAction("Publish", Command = "PublishProduct", Icon = "globe")]
[AdminAction("Unpublish", Command = "UnpublishProduct", Icon = "eye-off")]
[AdminAction("Delete", Command = "DeleteProduct", Icon = "trash",
RequiresConfirmation = true)]
public partial class ProductAdminModule
{
[AdminField("Name", ListVisible = true, ListSortable = true,
FormGroup = "General", FormOrder = 1)]
[AdminFilter(FilterType = FilterType.Contains)]
public partial string Name { get; }
[AdminField("Sku", ListVisible = true, ListSortable = true,
FormGroup = "General", FormOrder = 2)]
public partial string Sku { get; }
[AdminField("Price", ListVisible = true, ListSortable = true,
FormGroup = "Pricing", FormOrder = 1,
FormWidget = FormWidgetType.Auto)]
public partial Money Price { get; }
[AdminField("Category", ListVisible = true,
FormGroup = "Classification", FormOrder = 1,
FormWidget = FormWidgetType.RelationPicker)]
[AdminFilter(FilterType = FilterType.MultiSelect)]
public partial CategoryId CategoryId { get; }
[AdminField("Description", ListVisible = false,
FormGroup = "Content", FormOrder = 1,
FormWidget = FormWidgetType.RichText)]
public partial string Description { get; }
[AdminField("MainImage", ListVisible = false,
FormGroup = "Media", FormOrder = 1,
FormWidget = FormWidgetType.MediaPicker)]
public partial MediaRef MainImage { get; }
}[AdminModule("Products", Aggregate = "Product",
Icon = "package", MenuGroup = "Catalog", MenuOrder = 1)]
[AdminAction("Publish", Command = "PublishProduct", Icon = "globe")]
[AdminAction("Unpublish", Command = "UnpublishProduct", Icon = "eye-off")]
[AdminAction("Delete", Command = "DeleteProduct", Icon = "trash",
RequiresConfirmation = true)]
public partial class ProductAdminModule
{
[AdminField("Name", ListVisible = true, ListSortable = true,
FormGroup = "General", FormOrder = 1)]
[AdminFilter(FilterType = FilterType.Contains)]
public partial string Name { get; }
[AdminField("Sku", ListVisible = true, ListSortable = true,
FormGroup = "General", FormOrder = 2)]
public partial string Sku { get; }
[AdminField("Price", ListVisible = true, ListSortable = true,
FormGroup = "Pricing", FormOrder = 1,
FormWidget = FormWidgetType.Auto)]
public partial Money Price { get; }
[AdminField("Category", ListVisible = true,
FormGroup = "Classification", FormOrder = 1,
FormWidget = FormWidgetType.RelationPicker)]
[AdminFilter(FilterType = FilterType.MultiSelect)]
public partial CategoryId CategoryId { get; }
[AdminField("Description", ListVisible = false,
FormGroup = "Content", FormOrder = 1,
FormWidget = FormWidgetType.RichText)]
public partial string Description { get; }
[AdminField("MainImage", ListVisible = false,
FormGroup = "Media", FormOrder = 1,
FormWidget = FormWidgetType.MediaPicker)]
public partial MediaRef MainImage { get; }
}The generator produces three Blazor components: a list view with sortable columns and filters, a detail form with grouped fields and appropriate input widgets, and a toolbar with batch action buttons.
Diem vs CMF: Side-by-Side
What was generator.yml in Diem becomes type-safe C# attributes:
# Diem generator.yml (YAML, runtime interpretation)
generator:
class: sfDoctrineGenerator
param:
model_class: Product
theme: admin
config:
list:
display: [name, sku, price, category]
sort: [name, asc]
filter:
display: [name, category]
form:
display:
General: [name, sku]
Pricing: [price]
Classification: [category]
Content: [description]# Diem generator.yml (YAML, runtime interpretation)
generator:
class: sfDoctrineGenerator
param:
model_class: Product
theme: admin
config:
list:
display: [name, sku, price, category]
sort: [name, asc]
filter:
display: [name, category]
form:
display:
General: [name, sku]
Pricing: [price]
Classification: [category]
Content: [description]// CMF (C# attributes, compile-time validation)
[AdminModule("Products", Aggregate = "Product")]
public partial class ProductAdminModule
{
[AdminField("Name", ListSortable = true)]
[AdminFilter(FilterType = FilterType.Contains)]
public partial string Name { get; }
// ...
}// CMF (C# attributes, compile-time validation)
[AdminModule("Products", Aggregate = "Product")]
public partial class ProductAdminModule
{
[AdminField("Name", ListSortable = true)]
[AdminFilter(FilterType = FilterType.Contains)]
public partial string Name { get; }
// ...
}The CMF version is type-checked at compile time: if Product does not have a Name property, the generator reports a compiler error. Diem's YAML would fail silently at runtime.