Skip to main content

Templates

Templates are the core code generation mechanism in Intent Architect modules. They consume metadata from designers and produce source code files using C# classes and optional T4 text templates.

What is a Template?

A template is a C# class that implements the code generation logic for a specific output artifact. Templates can generate any text-based file, including:
  • C# classes and interfaces
  • Configuration files (JSON, XML, YAML)
  • Project files (.csproj)
  • Documentation (Markdown, HTML)
  • Infrastructure as Code (Terraform, Bicep)

Template Registration

Every template must be registered in the module’s .imodspec file:
<templates>
  <template id="Intent.Application.MediatR.CommandHandler" 
            externalReference="4ba1d060-8722-4b1d-97e1-e7f070b8be2e">
    <config>
      <add key="ClassName" description="Class name formula override (e.g. '${Model.Name}')" />
      <add key="Namespace" description="Class namespace formula override (e.g. '${Project.Name}'" />
    </config>
    <role>Application.Command.Handler</role>
    <location></location>
  </template>
</templates>
id
string
required
Unique identifier for the template (used for template lookups)
externalReference
string
required
GUID that uniquely identifies this template across versions
role
string
required
The template role - used for template resolution and type lookups
location
string
Default output location relative to the project root

Template Anatomy

A typical template consists of three files:
using Intent.Engine;
using Intent.Modules.Common.CSharp.Templates;
using Intent.Templates;

namespace Intent.Modules.Application.MediatR.Templates.CommandHandler
{
    public partial class CommandHandlerTemplate : CSharpTemplateBase<CommandModel, CommandHandlerDecorator>, 
        ICSharpFileBuilderTemplate
    {
        public const string TemplateId = "Intent.Application.MediatR.CommandHandler";

        public CommandHandlerTemplate(IOutputTarget outputTarget, CommandModel model) 
            : base(TemplateId, outputTarget, model)
        {
            CSharpFile = new CSharpFile($"{this.GetCommandNamespace()}", 
                                       $"{this.GetCommandFolderPath()}");
            Configure(this, model);
        }

        internal static void Configure(ICSharpFileBuilderTemplate template, CommandModel model)
        {
            template.AddNugetDependency(NugetPackages.MediatR(template.OutputTarget));
            
            template.CSharpFile
                .AddUsing("System")
                .AddUsing("System.Threading")
                .AddUsing("System.Threading.Tasks")
                .AddUsing("MediatR")
                .AddClass($"{model.Name}Handler", @class =>
                {
                    @class.ImplementsInterface(GetRequestHandlerInterface(template, model));
                    @class.AddConstructor(ctor =>
                    {
                        ctor.AddAttribute(CSharpIntentManagedAttribute.Merge());
                    });
                    @class.AddMethod(GetReturnType(template, model), "Handle", method =>
                    {
                        method.Async();
                        method.AddParameter(GetCommandModelName(template, model), "request");
                        method.AddParameter("CancellationToken", "cancellationToken");
                        method.AddStatement("throw new NotImplementedException();");
                    });
                });
        }

        public CSharpFile CSharpFile { get; }

        public override string TransformText()
        {
            return CSharpFile.ToString();
        }
    }
}

Template Types

1. File Per Model Templates

Generate one file for each model in the metadata:
public class CommandHandlerTemplateRegistration : FilePerModelTemplateRegistration<CommandModel>
{
    public override IEnumerable<CommandModel> GetModels(IApplication application)
    {
        return _metadataManager.Services(application).GetCommandModels();
    }
}
Used for generating entity classes, DTOs, handlers, controllers, etc. - one file per metadata element.

2. Single File Templates

Generate a single file per project:
public class DependencyInjectionTemplateRegistration : SingleFileListModelTemplateRegistration
{
    public override ITemplate CreateTemplateInstance(IOutputTarget outputTarget)
    {
        return new DependencyInjectionTemplate(outputTarget, null);
    }
}
Used for startup configuration, dependency injection setup, solution files, etc.

3. T4 Templates (Legacy)

Older template style using T4 text templates:
<#@ template language="C#" inherits="CSharpTemplateBase<object>" #>
<#@ import namespace="Intent.Modules.Common.CSharp.Templates" #>
using System;
using System.Threading;

namespace <#= Namespace #>;

public interface <#= ClassName #>
{
    Task<Uri> GetAsync(string bucketName, string key, CancellationToken cancellationToken = default);
}
T4 templates are legacy. New modules should use the CSharpFile Builder API for better maintainability and tooling support.

CSharpFile Builder API

The modern approach uses a fluent API for building C# code:
template.CSharpFile
    .AddUsing("System")
    .AddUsing("System.Threading.Tasks")
    .AddUsing("MediatR")
    .AddClass($"{model.Name}Handler", @class =>
    {
        @class.AddAttribute("IntentManaged(Mode.Merge, Signature = Mode.Fully)");
        @class.ImplementsInterface($"IRequestHandler<{commandModel}>");
        
        @class.AddConstructor(ctor =>
        {
            ctor.AddParameter("ILogger<Handler>", "logger", param =>
            {
                param.IntroduceReadonlyField();
            });
        });
        
        @class.AddMethod("Task", "Handle", method =>
        {
            method.Async();
            method.AddParameter(commandModel, "request");
            method.AddParameter("CancellationToken", "cancellationToken");
            method.AddStatement("throw new NotImplementedException();");
        });
    });

Benefits

Type Safety

Compile-time validation of generated code structure

Refactoring Support

IDE can refactor template code directly

IntelliSense

Full IntelliSense support while writing templates

Testability

Easier to unit test template logic

Template Decorators

Decorators allow modules to extend templates from other modules without modifying them:
public class MyCommandHandlerDecorator : CommandHandlerDecorator
{
    public MyCommandHandlerDecorator()
    {
        Priority = 10; // Higher priority decorators execute last
    }

    public override void BeforeTemplateExecution()
    {
        // Add custom logic before template generates
    }
}
Decorators are powerful for:
  • Adding attributes to generated classes
  • Injecting additional dependencies
  • Modifying method signatures
  • Adding custom validation logic

Template Configuration

Templates can accept user configuration:
<template id="Intent.Application.MediatR.CommandHandler">
  <config>
    <add key="ClassName" description="Class name formula override" />
    <add key="Namespace" description="Class namespace formula override" />
  </config>
</template>
Access configuration in your template:
protected override CSharpFileConfig DefineFileConfig()
{
    var className = GetConfig("ClassName") ?? $"{Model.Name}Handler";
    var @namespace = GetConfig("Namespace") ?? GetNamespace();
    
    return new CSharpFileConfig(
        className: className,
        @namespace: @namespace);
}

Template Roles

Roles identify what a template generates and enable type resolution:
  • Application.Command
  • Application.Command.Handler
  • Application.Query
  • Application.Query.Handler
  • Application.Contract.Dto

Best Practices

The builder API provides type safety, better tooling support, and easier testing.
Each template should generate one type of artifact. Don’t combine multiple concerns.
Use decorators to extend templates instead of modifying them directly.
Apply IntentManaged attributes to control code merging behavior:
  • Mode.Fully - Completely managed by Intent
  • Mode.Merge - Merge with user code
  • Mode.Ignore - Never overwrite

Example: Complete Template

Here’s a real-world example from the codebase:
<#@ template language="C#" inherits="CSharpTemplateBase<object>" #>
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

[assembly: DefaultIntentManaged(Mode.Fully)]

namespace <#= Namespace #>;

public record BulkObjectItem(string Name, Stream DataStream);

public interface <#= ClassName #>
{
    Task<Uri> GetAsync(string bucketName, string key, CancellationToken cancellationToken = default);
    IAsyncEnumerable<Uri> ListAsync(string bucketName, CancellationToken cancellationToken = default);
    Task<Uri> UploadAsync(Uri cloudStorageLocation, Stream dataStream, CancellationToken cancellationToken = default);
    IAsyncEnumerable<Uri> BulkUploadAsync(string bucketName, IEnumerable<BulkObjectItem> objects, CancellationToken cancellationToken = default);
    Task<Stream> DownloadAsync(Uri cloudStorageLocation, CancellationToken cancellationToken = default);
    Task DeleteAsync(Uri cloudStorageLocation, CancellationToken cancellationToken = default);
}

Next Steps

Metadata

Learn how templates consume metadata

Dependencies

Understand how templates reference other templates

Build docs developers (and LLMs) love