Skip to main content
Template decorators allow you to modify the output of existing templates without changing the template’s source code. This is essential for creating composable, reusable modules.

What are Decorators?

Decorators are classes that:
  • Hook into existing template output
  • Add, modify, or remove code sections
  • Execute before the template writes its output
  • Chain together for complex modifications
  • Maintain separation of concerns between modules

When to Use Decorators

Use decorators when you need to:
  • Add functionality to templates from another module
  • Inject cross-cutting concerns (logging, validation, etc.)
  • Customize generated code based on stereotypes
  • Avoid modifying or duplicating existing templates
  • Keep module dependencies clean and maintainable

Decorator Architecture

A decorator consists of:
  1. Decorator Contract Interface: Defines hooks that templates expose
  2. Decorator Implementation: Your code that modifies the template
  3. Decorator Registration: Registers the decorator with the Software Factory

Creating Your First Decorator

1
Create Decorator in Designer
2
In Module Builder designer:
3
  • Right-click your module
  • Select New Decorator > Template Decorator
  • Configure:
    • Name: MyDecorator
    • Decorator Contract: Select or create interface
  • 4
    Define the Decorator Contract
    5
    The contract interface defines what the decorator can modify:
    6
    using Intent.Modules.Common.CSharp.Builder;
    using Intent.Modules.Common.Templates;
    
    public interface IServiceDecorator : IHasDecorators<IServiceDecorator>
    {
        CSharpClass Class { get; }
        CSharpConstructor Constructor { get; }
    }
    
    7
    Run Software Factory
    8
    Generate decorator infrastructure:
    9
    intent run-software-factory
    
    10
    This creates:
    11
  • Decorators/MyDecorator.cs (your implementation)
  • Decorators/MyDecoratorRegistration.cs (generated)
  • 12
    Implement the Decorator
    13
    Edit the decorator class:
    14
    using Intent.Engine;
    using Intent.Modules.Common.CSharp.Builder;
    using Intent.Modules.Common.Templates;
    
    namespace MyModule.Decorators
    {
        public class MyDecorator : IServiceDecorator
        {
            public const string DecoratorId = "MyModule.MyDecorator";
    
            private readonly IServiceDecorator _template;
            private readonly IApplication _application;
    
            public MyDecorator(
                IServiceDecorator template, 
                IApplication application)
            {
                _template = template;
                _application = application;
                
                // Priority determines execution order (lower = earlier)
                Priority = 100;
            }
    
            public int Priority { get; }
    
            public void BeforeTemplateExecution()
            {
                // Add logging to the service
                _template.Class.AddField(
                    "ILogger", 
                    "_logger", 
                    field => field.PrivateReadOnly());
    
                _template.Constructor.AddParameter(
                    "ILogger",
                    "logger",
                    param => param.IntroduceField((field, p) =>
                    {
                        field.PrivateReadOnly();
                    }));
    
                // Add logging to all methods
                foreach (var method in _template.Class.Methods)
                {
                    method.AddStatement(
                        $"_logger.LogInformation(\"Executing {method.Name}\");",
                        stmt => stmt.BeforeCurrentStatement());
                }
            }
    
            public IServiceDecorator GetDecorator<T>() where T : IServiceDecorator
            {
                return _template.GetDecorator<T>();
            }
    
            public IEnumerable<IServiceDecorator> GetDecorators()
            {
                return _template.GetDecorators();
            }
        }
    }
    
    15
    Make Templates Decorator-Aware
    16
    Update target template to support decoration:
    17
    using Intent.Modules.Common.CSharp.Builder;
    using Intent.Modules.Common.Templates;
    
    public partial class ServiceTemplate : CSharpTemplateBase<ClassModel>, 
        IServiceDecorator
    {
        private CSharpClass _class;
        private CSharpConstructor _constructor;
    
        public CSharpClass Class => _class;
        public CSharpConstructor Constructor => _constructor;
    
        public override void BeforeTemplateExecution()
        {
            base.BeforeTemplateExecution();
            
            // Build the class structure
            _class = new CSharpClass(ClassName);
            _constructor = _class.AddConstructor();
            
            // Allow decorators to modify
            ExecuteDecorators();
        }
    
        public override string TransformText()
        {
            return _class.ToString();
        }
    }
    

    Decorator Patterns

    Adding Fields and Dependencies

    public void BeforeTemplateExecution()
    {
        // Add a field
        _template.Class.AddField(
            "IEventBus",
            "_eventBus",
            field => field.PrivateReadOnly());
    
        // Add constructor parameter and assign to field
        _template.Constructor.AddParameter(
            "IEventBus",
            "eventBus",
            param => param.IntroduceField((field, p) =>
            {
                field.PrivateReadOnly();
            }));
    }
    

    Modifying Methods

    public void BeforeTemplateExecution()
    {
        foreach (var method in _template.Class.Methods)
        {
            // Skip private methods
            if (method.AccessModifier == "private")
                continue;
    
            // Add parameter validation
            method.AddStatement(
                "ArgumentNullException.ThrowIfNull(request);",
                stmt => stmt.BeforeCurrentStatement());
    
            // Wrap existing code in try-catch
            var existingStatements = method.Statements.ToList();
            method.Statements.Clear();
            
            method.AddStatement("try");
            method.AddStatement("{");
            foreach (var statement in existingStatements)
            {
                method.AddStatement($"    {statement}");
            }
            method.AddStatement("}");
            method.AddStatement("catch (Exception ex)");
            method.AddStatement("{");
            method.AddStatement("    _logger.LogError(ex, \"Error in method\");");
            method.AddStatement("    throw;");
            method.AddStatement("}");
        }
    }
    

    Conditional Decoration

    Apply decorations based on metadata:
    public void BeforeTemplateExecution()
    {
        // Only decorate if model has specific stereotype
        if (!_template.Model.HasStereotype("Auditable"))
            return;
    
        // Add auditing fields
        _template.Class.AddProperty(
            "DateTime",
            "CreatedAt",
            prop => prop.WithAccessors("get; private set;"));
    
        _template.Class.AddProperty(
            "string",
            "CreatedBy",
            prop => prop.WithAccessors("get; private set;"));
    
        // Add auditing method
        _template.Class.AddMethod("void", "SetAuditInfo", method =>
        {
            method.AddParameter("string", "userId");
            method.AddStatement("CreatedAt = DateTime.UtcNow;");
            method.AddStatement("CreatedBy = userId;");
        });
    }
    

    Adding Attributes

    public void BeforeTemplateExecution()
    {
        // Add class-level attribute
        _template.Class.AddAttribute(
            "[GeneratedCode(\"Intent.Architect\", \"1.0\")]");
    
        // Add method attributes
        foreach (var method in _template.Class.Methods
            .Where(m => m.ReturnType.StartsWith("Task")))
        {
            method.AddAttribute("[Authorize]");
        }
    
        // Add property attributes based on metadata
        foreach (var property in _template.Class.Properties)
        {
            var modelProperty = _template.Model.Properties
                .FirstOrDefault(p => p.Name == property.Name);
                
            if (modelProperty?.HasStereotype("Required") == true)
            {
                property.AddAttribute("[Required]");
            }
        }
    }
    

    Implementing Interfaces

    public void BeforeTemplateExecution()
    {
        // Add interface implementation
        _template.Class.ImplementsInterface("INotifyPropertyChanged");
    
        // Add interface event
        _template.Class.AddProperty(
            "PropertyChangedEventHandler",
            "PropertyChanged",
            prop => prop.WithAccessors("add; remove;"));
    
        // Add helper method
        _template.Class.AddMethod("void", "OnPropertyChanged", method =>
        {
            method.Protected();
            method.AddParameter("string", "propertyName");
            method.AddStatement(
                "PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));");
        });
    
        // Modify property setters to call OnPropertyChanged
        foreach (var property in _template.Class.Properties)
        {
            if (property.Setter != null)
            {
                property.AddStatement(
                    $"OnPropertyChanged(nameof({property.Name}));",
                    stmt => stmt.AfterCurrentStatement());
            }
        }
    }
    

    Advanced Decorator Techniques

    Chaining Decorators

    Decorators can call other decorators:
    public class ValidationDecorator : IServiceDecorator
    {
        public int Priority => 50; // Lower priority = executes first
    
        public void BeforeTemplateExecution()
        {
            // This executes first
            AddValidationLogic();
        }
    }
    
    public class LoggingDecorator : IServiceDecorator
    {
        public int Priority => 100; // Higher priority = executes later
    
        public void BeforeTemplateExecution()
        {
            // This executes after ValidationDecorator
            // Can see changes made by ValidationDecorator
            AddLoggingAroundValidation();
        }
    }
    

    Accessing Other Templates

    public void BeforeTemplateExecution()
    {
        // Find related templates
        var dtoTemplate = _application.FindTemplateInstance<IDtoProvider>(
            TemplateDependency.OnModel("DtoTemplate", _template.Model));
    
        if (dtoTemplate != null)
        {
            var dtoTypeName = dtoTemplate.ClassName;
            
            // Add mapper using DTO
            _template.Class.AddMethod($"{dtoTypeName}", "ToDto", method =>
            {
                method.AddStatement($"return new {dtoTypeName}");
                method.AddStatement("{");
                method.AddStatement("    // Mapping logic");
                method.AddStatement("}");
            });
        }
    }
    

    Using CSharp Builder API

    Leverage the full CSharp Builder for complex modifications:
    public void BeforeTemplateExecution()
    {
        // Add nested class
        _template.Class.AddNestedClass("Builder", builder =>
        {
            builder.Static();
            
            builder.AddMethod(_template.ClassName, "Create", method =>
            {
                foreach (var property in _template.Class.Properties)
                {
                    method.AddParameter(
                        property.Type,
                        property.Name.ToCamelCase());
                }
                
                method.AddStatement($"return new {_template.ClassName}");
                method.AddStatement("{");
                
                foreach (var property in _template.Class.Properties)
                {
                    method.AddStatement(
                        $"    {property.Name} = {property.Name.ToCamelCase()},");
                }
                
                method.AddStatement("};");
            });
        });
    }
    

    Accessing Metadata in Decorators

    public void BeforeTemplateExecution()
    {
        var model = _template.Model;
        
        // Check stereotypes
        if (model.HasStereotype("CachedEntity"))
        {
            var cacheDuration = model.GetStereotypeProperty<int>(
                "CachedEntity", "Duration", 300);
                
            AddCachingLogic(cacheDuration);
        }
    
        // Process associations
        foreach (var association in model.AssociatedClasses)
        {
            if (association.IsCollection)
            {
                AddCollectionNavigationProperty(association);
            }
        }
    
        // Access application settings
        var enableFeature = _application.Settings.GetSetting(
            "MyModule.EnableFeature", "true") == "true";
            
        if (enableFeature)
        {
            AddFeatureCode();
        }
    }
    

    Decorator Contract Interfaces

    Standard Contract Pattern

    using Intent.Modules.Common.CSharp.Builder;
    using Intent.Modules.Common.Templates;
    
    public interface IServiceDecorator : IHasDecorators<IServiceDecorator>
    {
        // Expose builder elements
        CSharpClass Class { get; }
        CSharpConstructor Constructor { get; }
        
        // Expose metadata
        ClassModel Model { get; }
        
        // Expose template properties
        string Namespace { get; }
        IOutputTarget OutputTarget { get; }
    }
    

    Extended Contract with Methods

    public interface IRepositoryDecorator : IHasDecorators<IRepositoryDecorator>
    {
        CSharpClass Class { get; }
        CSharpConstructor Constructor { get; }
        
        // Template helper methods
        void AddQueryMethod(string methodName, string returnType);
        void AddCommandMethod(string methodName);
        
        // Access to template dependencies
        string GetEntityTypeName();
        string GetDbContextTypeName();
    }
    

    Decorator Registration

    Generated registration class:
    Decorators/MyDecoratorRegistration.cs
    using Intent.Engine;
    using Intent.Modules.Common.Registrations;
    
    public class MyDecoratorRegistration : DecoratorRegistration<IServiceDecorator>
    {
        public override string DecoratorId => MyDecorator.DecoratorId;
    
        public override IServiceDecorator CreateDecoratorInstance(
            IServiceDecorator template, 
            IApplication application)
        {
            return new MyDecorator(template, application);
        }
    }
    

    Custom Registration Logic

    For conditional decorator application:
    public override IServiceDecorator CreateDecoratorInstance(
        IServiceDecorator template,
        IApplication application)
    {
        // Only apply decorator if model meets criteria
        if (template.Model.HasStereotype("RequiresLogging"))
        {
            return new MyDecorator(template, application);
        }
        
        return null; // Don't apply decorator
    }
    

    Testing Decorators

    Unit Testing

    [Fact]
    public void Decorator_AddsLoggingField()
    {
        // Arrange
        var mockTemplate = new Mock<IServiceDecorator>();
        var mockClass = new CSharpClass("TestClass");
        mockTemplate.Setup(t => t.Class).Returns(mockClass);
        
        var decorator = new LoggingDecorator(
            mockTemplate.Object, 
            mockApplication);
    
        // Act
        decorator.BeforeTemplateExecution();
    
        // Assert
        Assert.Contains(mockClass.Fields, 
            f => f.Name == "_logger" && f.Type == "ILogger");
    }
    

    Integration Testing

    Create test application and verify decorated output:
    [Fact]
    public async Task Decorator_GeneratesCorrectOutput()
    {
        // Arrange
        var application = CreateTestApplication();
        application.InstallModule("MyModule.WithDecorators");
    
        // Act
        var result = await application.RunSoftwareFactory();
    
        // Assert
        var serviceFile = result.GetGeneratedFile("Services/MyService.cs");
        Assert.Contains("private readonly ILogger _logger;", serviceFile.Content);
        Assert.Contains("_logger.LogInformation", serviceFile.Content);
    }
    

    Best Practices

    Keep Decorators Focused

    // Good: Single concern
    public class LoggingDecorator { }
    public class ValidationDecorator { }
    public class CachingDecorator { }
    
    // Bad: Multiple concerns
    public class EverythingDecorator { }
    

    Use Appropriate Priorities

    // Lower numbers execute first
    public class ValidationDecorator
    {
        public int Priority => 10; // Validate first
    }
    
    public class BusinessLogicDecorator
    {
        public int Priority => 50; // Business logic
    }
    
    public class LoggingDecorator
    {
        public int Priority => 100; // Log after everything
    }
    

    Handle Nulls Gracefully

    public void BeforeTemplateExecution()
    {
        if (_template?.Class == null)
            return;
    
        if (_template.Model?.Properties == null)
            return;
    
        // Safe to proceed
        foreach (var property in _template.Model.Properties)
        {
            // ...
        }
    }
    

    Document Decorator Behavior

    /// <summary>
    /// Adds logging capabilities to service templates.
    /// Requires ILogger dependency and adds logging statements
    /// at the beginning of each public method.
    /// </summary>
    public class LoggingDecorator : IServiceDecorator
    {
        // Implementation
    }
    

    Common Use Cases

    Adding Logging

    Inject logging infrastructure and statements into templates

    Validation

    Add input validation to methods and properties

    Caching

    Implement caching around expensive operations

    Security

    Add authorization checks and attributes

    Auditing

    Track creation and modification metadata

    Error Handling

    Wrap methods in try-catch blocks

    Troubleshooting

    • Verify template implements decorator contract interface
    • Check decorator is registered in .imodspec
    • Ensure template calls ExecuteDecorators()
    • Verify BeforeTemplateExecution() is called
    • Check decorator priority order
    • Verify modifications are made before TransformText()
    • Ensure builder objects are being modified, not recreated
    • Check for exceptions in decorator code
    • Review decorator priorities
    • Check for overlapping modifications
    • Use decorator chaining appropriately
    • Consider consolidating related decorators

    Next Steps

    Factory Extensions

    Hook into Software Factory lifecycle

    Creating Templates

    Learn template development basics

    Testing Modules

    Test your decorators and modules

    Module Builder

    Back to Module Builder overview

    Build docs developers (and LLMs) love