Framework for template processing and code generation
$ dotnet add package pauldeen79.TemplateFramework.Core.CodeGenerationTemplate and code generation framework for C#
If you want to create templates in any .NET language (C#, VB.Net, F#) and run them using a dotnet global tool, this framework is for you!
We currently target .NET 10.0, but the code can easily be ported back to older .NET versions.
A code generation provider is a class that provides a template instance, along with optional model and additional parameters. This is typically the level you want to use, if you want to scaffold multiple files using a single command.
If you want to use the template abstraction level, then you have to make sure the template class has a public parameterless constructor.
You have to write a class in a .NET 10.0 project (class library project is good enough), and compile this project. Then you can either use the command line tool 'tf' (Template Framework) or write your own host and reference the Core and TemplateProviders.CompiledTemplateProvider packages.
There are multiple types of templates supported out of the box:
Important: If you are not using a POCO template, make sure you reference the same package version of TemplateFramework.Abstractions as the host! So if you install version x.y of TemplateFramework.Console, then also reference version x.y of the TemplateFramework.Abstractions package from your template or code generation assembly.
To create a StringBuilder template, implement this interface from the TemplateFramework.Abstractions package:
public interface IStringBuilderTemplate
{
Task RenderAsync(StringBuilder builder, CancellationToken token);
}
To create a Text Transform template, implement this interface from the TemplateFramework.Abstractions package:
public interface ITextTransformTemplate
{
Task<string> TransformTextAsync(CancellationToken token);
}
To create a Multiple Content Builder template, implement this interface from the TemplateFramework.Abstractions package:
public interface IMultipleContentBuilderTemplate
{
Task RenderAsync(IMultipleContentBuilder builder, CancellationToken token);
}
The template assembly is loaded by the command tool. If you want to add external references to your template assembly, then you have to take some additional steps. There are more options to choose from.
The first option is to write a custom host. Add the references to the Core and TemplateProviders.CompiledTemplateProvider packages.
The second option is to add the following property to your template assembly, so your build output directory contains all referenced assemblies from package references:
<PropertyGroup>
...
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
...
</PropertyGroup>
The third option is to publish your template assembly, and use the publishing output directory.
Note that the following assemblies will be loaded from the host (Console) command tool, so make sure you use the same versions referenced from there:
Right now, the all TemplateFramework assemblies are built in one build pipeline within one GitHub repository, so all version numbers of the TemplateFramework assemblies are the same. This means, that if you install version x.y of TemplateFramework.Console, then your template assemblies should also use version x.y of TemplateFramework package references. (most likely TemplateFramework.Abstractions)
If you want to render child templates from your main (root) template, then you have to implement this interfaces from the TemplateFramework.Abstractions package: ITemplateContextContainer.
public interface ITemplateContextContainer
{
ITemplateContext Context { get; set; }
}
Then, in your template, call the Render method on the TemplateEngine instance. (Engine property of the Context) As context, create a child context using the CreateChildContext method on the TemplateContext instance.
There is also an integration test in the TemplateFramework.TemplateProviders.ChildTemplateProvider test project to demonstrate this.
In order to register child templates, so that they can be resolved from the (root) template that's being rendered, you have to create a class that implements the following interface, from the TemplateFramework.Abstractions package:
public interface ITemplateProviderComponent
{
Task Initialize(ITemplateProvider provider, CancellationToken token);
}
Then, from the command line, you have to specify the class name of this class, using the --templateproviderplugin or -t argument. Note that the current version expects this class to be in the same assembly as the template assembly.
If you use one or more code generation providers, then each code generation provider (ICodeGenerationProvider implementation) also can implement this ITemplateProviderPlugin interface, to register additional child templates. Note that if you don't supply a filter on the command line, then all code generation providers will be checked for this interace. If you have conflicting child template names or model types within the same assembly, you have to use a filter to run just one code generation provider instead of all types from the assembly.
If you are using text-based templates, you can register custom components to process placeholders or function results.
FormattableStringTemplates: CrossCutting.Utilities.Parsers.Contracts.IPlaceholderProcessor (from CrossCutting.Utilities.Parsers package) ExpressionStringTemplate: CrossCutting.Utilities.Parsers.Contracts.IPlaceholderProcessor (from CrossCutting.Utilities.Parsers package)
To register this dynamically, you need to create a class that implements this interface, from TemplateFramework.Abstractions:
public interface ITemplateComponentRegistryPlugin
{
Task Initialize(ITemplateComponentRegistry registry, CancellationToken token);
}
Create a constructor to get the ComponentRegistrationContext instance. Then, in the Initialize method, register instances.
public sealed class MyTemplateComponentRegistryPlugin : ITemplateComponentRegistryPlugin
{
public ComponentRegistrationContext ComponentRegistrationContext { get; }
public TestTemplateComponentRegistryPlugin(ComponentRegistrationContext componentRegistrationContext)
{
Guard.IsNotNull(componentRegistrationContext);
ComponentRegistrationContext = componentRegistrationContext;
}
public Task Initialize(ITemplateComponentRegistry registry, CancellationToken token)
{
var processorProcessor = new MyPlaceholderProcessor();
var functionResultParser = new MyFunctionResultParser();
ComponentRegistrationContext.PlaceholderProcessors.Add(processorProcessor);
ComponentRegistrationContext.FunctionResultParsers.Add(functionResultParser);
return Task.CompletedTask;
}
}
In this example, the MyPlaceholderProcessor and MyFunctionResultParser classes are the implementations that you need to provide. If you need additional dependencies, you need to add those to the constructor or your TemplateComponentRegistryPlugin class, and then use them on construction of your placeholder processors or function result parsers.
Finally, on the command line, use the assembly name and class name (and probably also the directory name where the assembly is stored) to this class. Probably something like this:
tf template --formattablestring template.txt --dryrun --default myfile.txt --interactive --templateproviderplugin MyAssembly.MyTemplateComponentRegistryPlugin --assembly MyAssembly --directory D:\\somewhere\\MyAssembly\\bin\\debug\\net8.0
There is also an example in launchSettings.json of the TemplateFramework.Console project, that uses a template provider plug-in of a unit test project.
In version 2.0, there are three breaking changes:
//ICodeGenerationProvider:
Task<object?> CreateAdditionalParameters();
Task<object?> CreateModel();
//ISessionAwareComponent:
Task StartSession(CancellationToken token);
has changed to
//ICodeGenerationProvider:
Task<Result<object?>> CreateAdditionalParameters(CancellationToken token);
Task<Result<object?>> CreateModel(CancellationToken token);
//ISessionAwareComponent:
Task<Result> StartSession(CancellationToken token);
This enables you to return error messages from model creation, instead of throwing exceptions. And while we are already breaking compatibility, the CancellationToken argument is also added to CreateAdditionalParameters and CreateModel.