Template authoring guide
Build, share, and version your own templates.
Template format
A template is a directory of .cs files that share a single declared C# namespace. That namespace is the NamespaceToken replaced at application time.
my-custom-template/
MyClass.cs
MyInterface.csNamespace convention
Built-in templates use:
namespace Socotra.Templates.Shared;At application time this becomes:
{TargetNamespace}.{PascalCaseTemplateName}Example: applying base-entity to the Orders.Domain layer with target namespace MyApp.Orders.Domain yields:
namespace MyApp.Orders.Domain.BaseEntity;Content tokens
namespace Socotra.Templates.Shared;
public class {{Name}}
{
public {{Namespace}} Context { get; set; }
}Path tokens (CustomOutputPath)
outputPath: "{{ModuleName}}/{{EntityName}}/Generated"Guard header
Every file written by the template system receives a guard header:
// <socotra-managed version="1.0" template="base-entity" source="built-in" file="BaseEntity.cs" />apply runs. Files without it are considered user-written and are never overwritten.Template sources
Built-in — simple name with no prefix (e.g. base-entity). Embedded in the tool binary as assembly resources under Socotra.TemplateRegistry.BuiltInTemplates.*. Hyphens are reversed at runtime.
GitHub — github:owner/repo/path. Uses the GitHub Contents API. Retries handled by Polly with exponential backoff: 3 retries on HttpRequestException, TaskCanceledException, 429, or 503; respects Retry-After; base delay 2s. Subdirectories traversed recursively; .cs only.
LocalPath — ./path, ../path, /abs/path, or C:\\path. Copies .cs files top-level only (non-recursive) into the local cache. Single .cs files also accepted.
Create a custom template
Step 1 — create the directory:
my-templates/
audit-entity/
AuditEntry.cs
AuditConfiguration.csStep 2 — declare a common namespace:
namespace MyCompany.Templates.Auditing;
public class AuditEntry { /* ... */ }Step 3 — use tokens (optional):
namespace MyCompany.Templates.Auditing;
public class {{Name}}Configuration
{
public string Namespace => "{{Namespace}}";
}Step 4 — add the template:
socotra template add ./my-templates/audit-entityWithout --global, this copies files to .socotra/templates/audit-entity/. With --global, it uses ~/.socotra/registry/audit-entity/.
Step 5 — reference in socotra.yaml:
modules:
- name: "Orders"
layers:
- name: "Domain"
templates: ["audit-entity"]How template application works
TemplateSourceParser.Parse()
│
▼
TemplateResolver.ResolveAsync()
│
├── In Project (.socotra/templates/)? → use project-local files
├── In Global (~/.socotra/registry/)? → use global cache
├── Built-in (embedded)? → extract from assembly
│
└── Not found → TemplateFetcherDispatcher.FetchAsync()
│
├── BuiltIn → BuiltInTemplateFetcher
├── GitHub → GitHubTemplateFetcher (Polly retry)
└── LocalPath → LocalPathTemplateFetcher
│
▼
LocalRegistryStore.SaveAsync()
│
▼
NamespaceDetector.Detect()
│
▼
TemplateApplier applies:
1. Resolve output folder
2. Build target namespace
3. Build content tokens
4. For each file:
a. NormalizedFileWriter.WriteAsync()
b. NamespaceNormalizer.Normalize()
c. Substitute {{Name}} / {{Namespace}}
d. Prepend guard header
e. Atomic write via .tmp fileNamespace normalization
The NamespaceNormalizer handles four kinds of replacements:
- File-scoped namespace:
namespace Foo.Bar;→namespace Target.Ns; - Block-scoped namespace:
namespace Foo.Bar {→namespace Target.Ns { - Using directives:
using Foo.Bar;→using Target.Ns; - Qualified references:
Foo.Bar.SomeType→Target.Ns.SomeType
Comments and string literals containing the namespace token are not modified (heuristic check for " and @ before the token on the line). If the namespace token equals the target namespace, no work is done. An exception is thrown if the target namespace is empty.
meta.json
{
"name": "base-entity",
"source": { "kind": "BuiltIn", "value": "base-entity" },
"fetchedAt": "2026-06-17",
"version": "built-in",
"files": ["BaseEntity.cs"],
"namespaceToken": "Socotra.Templates.Shared"
}