Socotra
Templates

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.cs

Namespace 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

TokenResolved to
{{Name}}PascalCase of the template name (e.g. base-entityBaseEntity)
{{Namespace}}Full target namespace: {TargetNamespace}.{PascalCaseName}
MyClass.cs
namespace Socotra.Templates.Shared;

public class {{Name}}
{
    public {{Namespace}} Context { get; set; }
}

Path tokens (CustomOutputPath)

TokenResolved to
{{ModuleName}}Module name (e.g. Orders)
{{LayerName}}Layer name (e.g. Orders.Domain)
{{Namespace}}Target namespace
{{LayerPath}}Target project path on disk
{{SolutionName}}First segment of ProjectName split by .
{{EntityName}}Last segment of LayerName split by .
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" />
Never-overwrite protection
Files with this header may be overwritten on subsequent 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.

GitHubgithub: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.cs

Step 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-entity

Without --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 file

Namespace normalization

The NamespaceNormalizer handles four kinds of replacements:

  1. File-scoped namespace: namespace Foo.Bar;namespace Target.Ns;
  2. Block-scoped namespace: namespace Foo.Bar {namespace Target.Ns {
  3. Using directives: using Foo.Bar;using Target.Ns;
  4. Qualified references: Foo.Bar.SomeTypeTarget.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

.socotra/templates/base-entity/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"
}
Schema 1.1 · CLI 1.0