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:
Decorator Base Class - Defines extension points in the target template
Decorator Implementation - Your custom decorator logic
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:
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;" );
});
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:
[ 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)
Handle missing elements gracefully
Always check if elements exist before modifying: var method = @class . FindMethod ( "Handle" );
if ( method != null )
{
method . InsertStatement ( 0 , "// My code" );
}
Document decorator behavior
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