Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

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;
}

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 }

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;
}

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; }
}

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]
// 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.

Diagram
A single [AdminModule] declaration unfolds into a full Razor back-office: sortable list, grouped form, read-only detail view and toolbar — all type-checked against the aggregate at compile time.
⬇ Download