Skip to main content
Decorators allow you to extend existing templates without modifying their source code. They’re essential for cross-cutting concerns and modular functionality.

What are Decorators?

Decorators are components that:
  • Modify the output of existing templates
  • Add attributes, methods, properties, or using statements
  • Implement cross-cutting concerns (logging, validation, etc.)
  • Integrate multiple modules together
Decorators follow the Open/Closed Principle - templates are open for extension but closed for modification.

Decorator Architecture

A decorator system consists of:
  1. Decorator Base Class - Defines extension points in the target template
  2. Decorator Implementation - Your custom decorator logic
  3. Decorator Registration - Registers the decorator with the template

Creating a Decorator

Step 1: Define the Decorator Base Class

The template owner defines a decorator base class:
CommandHandlerDecorator.cs
using Intent.RoslynWeaver.Attributes;
using Intent.Templates;

[assembly: DefaultIntentManaged(Mode.Ignore)]
[assembly: IntentTemplate("Intent.ModuleBuilder.Templates.TemplateDecoratorContract", Version = "1.0")]

namespace Intent.Modules.Application.MediatR.Templates.CommandHandler
{
    [IntentManaged(Mode.Merge)]
    public abstract class CommandHandlerDecorator : ITemplateDecorator
    {
        public int Priority { get; protected set; } = 0;
        
        // Extension points for decorators
        public virtual void BeforeHandleMethod() { }
        public virtual void AfterHandleMethod() { }
        public virtual IEnumerable<string> AdditionalUsings() => Enumerable.Empty<string>();
        public virtual IEnumerable<string> ConstructorParameters() => Enumerable.Empty<string>();
    }
}

Step 2: Implement the Decorator

Create your decorator implementation:
LoggingCommandHandlerDecorator.cs
using Intent.Engine;
using Intent.Modules.Application.MediatR.Templates.CommandHandler;
using Intent.RoslynWeaver.Attributes;
using Intent.Templates;

[assembly: DefaultIntentManaged(Mode.Merge)]
[assembly: IntentTemplate("Intent.ModuleBuilder.Templates.TemplateDecorator", Version = "1.0")]

namespace Intent.Modules.Application.Logging.Decorators
{
    [IntentManaged(Mode.Merge)]
    public class LoggingCommandHandlerDecorator : CommandHandlerDecorator, IDeclareUsings
    {
        public const string DecoratorId = "Intent.Application.Logging.CommandHandlerDecorator";

        private readonly CommandHandlerTemplate _template;
        private readonly IApplication _application;

        [IntentManaged(Mode.Merge, Body = Mode.Fully)]
        public LoggingCommandHandlerDecorator(CommandHandlerTemplate template, IApplication application)
        {
            _template = template;
            _application = application;
            Priority = 10; // Higher priority runs first
        }

        public override void BeforeHandleMethod()
        {
            _template.CSharpFile.OnBuild(file =>
            {
                var @class = file.Classes.First();
                var handleMethod = @class.FindMethod("Handle");
                
                handleMethod?.InsertStatement(0, 
                    $"_logger.LogInformation(\"Executing {_template.Model.Name}\");");
            });
        }

        public IEnumerable<string> DeclareUsings()
        {
            yield return "Microsoft.Extensions.Logging";
        }
    }
}

Step 3: Register the Decorator

Create a decorator registration:
LoggingCommandHandlerDecoratorRegistration.cs
using Intent.Engine;
using Intent.Modules.Application.MediatR.Templates.CommandHandler;
using Intent.Modules.Common.Registrations;
using Intent.RoslynWeaver.Attributes;
using System.ComponentModel;

[assembly: DefaultIntentManaged(Mode.Fully)]
[assembly: IntentTemplate("Intent.ModuleBuilder.Templates.TemplateDecoratorRegistration", Version = "1.0")]

namespace Intent.Modules.Application.Logging.Decorators
{
    [Description(LoggingCommandHandlerDecorator.DecoratorId)]
    public class LoggingCommandHandlerDecoratorRegistration : 
        DecoratorRegistration<CommandHandlerTemplate, CommandHandlerDecorator>
    {
        public override CommandHandlerDecorator CreateDecoratorInstance(
            CommandHandlerTemplate template, 
            IApplication application)
        {
            return new LoggingCommandHandlerDecorator(template, application);
        }

        public override string DecoratorId => LoggingCommandHandlerDecorator.DecoratorId;
    }
}

Decorator Patterns

Attribute Decorators

Add attributes to classes, properties, or methods:
DataContractAttributeDecorator.cs
public class DataContractDTOAttributeDecorator : DtoModelDecorator, IDeclareUsings
{
    private readonly DtoModelTemplate _template;

    public DataContractDTOAttributeDecorator(DtoModelTemplate template, IApplication application)
    {
        _template = template;
    }

    public override string ClassAttributes(DTOModel dto)
    {
        return $"[DataContract{GetDataContractProperties(dto)}]";
    }

    public override string PropertyAttributes(DTOModel dto, DTOFieldModel field)
    {
        return "[DataMember]";
    }

    public IEnumerable<string> DeclareUsings()
    {
        yield return "System.Runtime.Serialization";
    }

    private string GetDataContractProperties(DTOModel dto)
    {
        var stereotype = dto.GetStereotype("DataContract");
        if (stereotype == null) return string.Empty;

        var properties = new List<string>();
        
        var ns = stereotype.GetProperty<string>("Namespace");
        if (!string.IsNullOrEmpty(ns))
        {
            properties.Add($"Namespace=\"{ns}\"");
        }

        var isReference = stereotype.GetProperty<bool>("IsReference");
        if (isReference)
        {
            properties.Add($"IsReference={isReference.ToString().ToLower()}");
        }

        return properties.Any() ? $"({string.Join(", ", properties)})" : string.Empty;
    }
}

Constructor Injection Decorators

Add dependencies to template constructors:
RepositoryInjectionDecorator.cs
public class RepositoryInjectionDecorator : ServiceImplementationDecorator
{
    private readonly ServiceImplementationTemplate _template;

    public RepositoryInjectionDecorator(ServiceImplementationTemplate template)
    {
        _template = template;
    }

    public override void OnConstructorBuilt(CSharpConstructor constructor)
    {
        if (_template.Model.RequiresRepository())
        {
            var repositoryType = $"I{_template.Model.GetEntityName()}Repository";
            
            constructor.AddParameter(
                _template.UseType(repositoryType),
                "repository",
                param => param.IntroduceReadonlyField());
        }
    }
}

Method Body Decorators

Modify method implementations:
ValidationDecorator.cs
public class FluentValidationDecorator : CommandHandlerDecorator
{
    private readonly CommandHandlerTemplate _template;

    public FluentValidationDecorator(CommandHandlerTemplate template)
    {
        _template = template;
        Priority = 5;
    }

    public override void BeforeHandleMethod()
    {
        _template.CSharpFile.OnBuild(file =>
        {
            var @class = file.Classes.First();
            var handleMethod = @class.FindMethod("Handle");
            
            if (handleMethod != null)
            {
                handleMethod.InsertStatement(0, new CSharpInvocationStatement(
                    "await _validator.ValidateAndThrowAsync(request, cancellationToken)"));
            }
        });
    }

    public override IEnumerable<string> ConstructorParameters()
    {
        yield return $"IValidator<{_template.GetCommandName()}> validator";
    }

    public override IEnumerable<string> AdditionalUsings()
    {
        yield return "FluentValidation";
    }
}

Configuration Decorators

Modify configuration files or startup code:
SerilogLoggingDecorator.cs
public class ConfigurationSettingsSerilogLoggingDecorator : AppSettingsDecorator
{
    private readonly AppSettingsTemplate _template;

    public ConfigurationSettingsSerilogLoggingDecorator(AppSettingsTemplate template)
    {
        _template = template;
        Priority = 0;
    }

    public override void Install()
    {
        _template.AddSetting("Serilog", new
        {
            Using = new[] { "Serilog.Sinks.Console", "Serilog.Sinks.File" },
            MinimumLevel = new
            {
                Default = "Information",
                Override = new Dictionary<string, string>
                {
                    ["Microsoft"] = "Warning",
                    ["System"] = "Warning"
                }
            },
            WriteTo = new object[]
            {
                new { Name = "Console" },
                new { Name = "File", Args = new { path = "logs/log-.txt", rollingInterval = "Day" } }
            }
        });
    }
}

Advanced Decorator Techniques

Using CSharpFile.AfterBuild

Modify the file after the template has built it:
_template.CSharpFile.AfterBuild(file =>
{
    var @class = file.Classes.First();
    
    // Add interface implementation
    @class.ImplementsInterface("IHaveAuditFields");
    
    // Add properties
    @class.AddProperty("DateTime", "CreatedDate", prop =>
    {
        prop.Getter();
        prop.Setter();
    });
    
    // Modify existing method
    var method = @class.FindMethod("Save");
    method?.InsertStatement(0, "CreatedDate = DateTime.UtcNow;");
});

Metadata-Driven Decorators

Use metadata to conditionally apply decorations:
public class MetadataBasedDecorator : DtoModelDecorator
{
    public override void BeforeTemplateExecution()
    {
        _template.CSharpFile.AfterBuild(file =>
        {
            var @class = file.Classes.First();
            var model = _template.Model;

            // Check for specific stereotype
            if (model.HasStereotype("Auditable"))
            {
                AddAuditFields(@class);
            }

            // Check for specific metadata tag
            if (model.HasTag("Serializable"))
            {
                @class.AddAttribute("[Serializable]");
            }
        });
    }

    private void AddAuditFields(CSharpClass @class)
    {
        @class.AddProperty("DateTime", "CreatedDate");
        @class.AddProperty("DateTime?", "ModifiedDate");
        @class.AddProperty("string", "CreatedBy");
        @class.AddProperty("string", "ModifiedBy");
    }
}

Multi-Template Decorators

One decorator can modify multiple related templates:
public class AutoMapperProfileDecorator : FactoryExtensionBase
{
    protected override void OnAfterTemplateRegistrations(IApplication application)
    {
        // Modify DTO templates
        var dtoTemplates = application.FindTemplateInstances<DtoModelTemplate>();
        foreach (var template in dtoTemplates)
        {
            template.CSharpFile.AfterBuild(file =>
            {
                var @class = file.Classes.First();
                @class.ImplementsInterface($"IMapFrom<{GetEntityName(template.Model)}>");
                
                @class.AddMethod("void", "Mapping", method =>
                {
                    method.AddParameter("Profile", "profile");
                    method.AddStatement(
                        $"profile.CreateMap<{GetEntityName(template.Model)}, {template.ClassName}>()"
                        + ".ReverseMap();");
                });
            });
        }
    }
}

Decorator Priority

Control the order in which decorators run:
public class HighPriorityDecorator : SomeDecorator
{
    public HighPriorityDecorator()
    {
        Priority = 100; // Runs early
    }
}

public class LowPriorityDecorator : SomeDecorator
{
    public LowPriorityDecorator()
    {
        Priority = -100; // Runs late
    }
}
Higher priority values run first. Default priority is 0.

Implementing IDeclareUsings

Automatically add using statements:
public class MyDecorator : SomeDecorator, IDeclareUsings
{
    public IEnumerable<string> DeclareUsings()
    {
        yield return "System.ComponentModel.DataAnnotations";
        yield return "Microsoft.Extensions.Logging";
        yield return "Newtonsoft.Json";
    }
}

Conditional Decorator Application

Apply decorators based on settings or conditions:
public class ConditionalDecoratorRegistration : 
    DecoratorRegistration<MyTemplate, MyDecorator>
{
    public override MyDecorator CreateDecoratorInstance(
        MyTemplate template, 
        IApplication application)
    {
        // Only apply if setting is enabled
        if (!application.Settings.GetMySettings().EnableFeature())
        {
            return null; // Don't apply decorator
        }

        return new MyDecorator(template, application);
    }

    public override string DecoratorId => MyDecorator.DecoratorId;
}

Testing Decorators

Create tests to verify decorator behavior:
DecoratorTests.cs
[TestClass]
public class LoggingDecoratorTests
{
    [TestMethod]
    public void AddsLoggingStatements()
    {
        // Arrange
        var template = CreateTestTemplate();
        var decorator = new LoggingCommandHandlerDecorator(template, CreateTestApplication());
        
        // Act
        decorator.BeforeHandleMethod();
        var output = template.TransformText();
        
        // Assert
        Assert.IsTrue(output.Contains("_logger.LogInformation"));
    }

    [TestMethod]
    public void AddsRequiredUsings()
    {
        // Arrange
        var decorator = new LoggingCommandHandlerDecorator(/* ... */);
        
        // Act
        var usings = decorator.DeclareUsings().ToList();
        
        // Assert
        Assert.IsTrue(usings.Contains("Microsoft.Extensions.Logging"));
    }
}

Best Practices

Each decorator should have a single responsibility. Don’t create “god decorators” that do everything.
Set priority based on dependencies:
  • Validation: High priority (runs first)
  • Logging: Medium priority
  • Cleanup: Low priority (runs last)
Always check if elements exist before modifying:
var method = @class.FindMethod("Handle");
if (method != null)
{
    method.InsertStatement(0, "// My code");
}
Clearly document what your decorator does and any prerequisites:
/// <summary>
/// Adds DataContract and DataMember attributes to DTOs.
/// Requires the model to have a "DataContract" stereotype.
/// </summary>
public class DataContractDecorator : DtoModelDecorator
Don’t assume specific template implementation details. Use public APIs and metadata.

Common Use Cases

Cross-Cutting Concerns

  • Logging
  • Authentication/Authorization
  • Caching
  • Transaction management

Framework Integration

  • ORM attributes (EF Core)
  • Serialization attributes (JSON, XML)
  • Validation attributes (Data Annotations)
  • Dependency injection

Code Enhancement

  • Audit fields
  • Soft delete support
  • Versioning/concurrency
  • Multi-tenancy

Standards Enforcement

  • Naming conventions
  • Code style
  • Documentation requirements
  • Security policies

Real-World Examples

Explore these decorators in the source code:
  • DataContractDTOAttributeDecorator - Modules/Intent.Modules.Application.Dtos/Decorators/
  • CommandHandlerDecorator - Modules/Intent.Modules.Application.MediatR/Templates/CommandHandler/
  • ServiceImplementationDecorator - Modules/Intent.Modules.Application.ServiceImplementations/Templates/

Next Steps

Factory Extensions

Learn about lifecycle hooks and cross-template modifications

Template Development

Create templates that support decorators

Testing

Test your decorators comprehensively

Creating Modules

Package decorators into modules

Build docs developers (and LLMs) love