Skip to content

Instantly share code, notes, and snippets.

@baronfel
Last active March 27, 2024 18:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baronfel/8d334750037c774d9cefeb7b14c59ada to your computer and use it in GitHub Desktop.
Save baronfel/8d334750037c774d9cefeb7b14c59ada to your computer and use it in GitHub Desktop.
.NET SDK Compound Templates

Core Idea

Allow templates to easily orchestrate the invocation of other templates by name/id, making multi-project and multi-item templates easier to keep up to date, and factoring out subsets of currently-giant templates into maintainable chunks.

Motivating Examples

  • A JS API with a corresponding .NET Backend
  • A Library project, a console project, and a test project
  • An Aspire AppHost with a .NET Backend

Proposal

Create a new template type

The template engine already has a known set of supported types - we should add a new one: compound templates. These templates would have additional syntax in the form of a subtemplates array which describes the set of child templates the parent compound template would orchestrate. The subtemplates array would consist of items that:

  • uniquely identify the child template (for allowing sharing of symbols)
  • specify the child template to invoke (either by shortname alone or by a combination of PackageName/shortName for specificity)
  • specify how the template engine should invoke the child template, namely:
  • key input parameters like name, output, etc
  • allowing specifying concrete values for the symbols of the child template

In addition, the parent template's symbols would be able to re-use specific symbols from the child templates, referencing them by identifiers and encouraging re-use and delegation.

An example of a compound template in the new system might look something like this:

{
 "name": "My compound template",
 "shortName": "compound-template",
 "type": "compound",
 "subtemplates": [
   {
     // the id is how we refer to this subtemplate in the rest of this file
     "id": "console-app",
     "template": "console", // this can be a shortname, or for great specificity a Package::shortname disambiguation syntax can be used
     "name": "ref:parent:console-app-name", // name and output path can reference the parent's name and output path, specific parent symbols, or be hard-coded strings
     "condition": "{favoriteAnimal} == 'Chicken'"
     "symbols": [
       "langVersion": "ref:parent:langVersion", // symbols can be set to hard-coded values or reference symbols defined on the parent (which themselves can come from children)
       "framework": "ref:parent:framework"
     ]
   },
   {
     "id": "library",
     "template": "classlib"
   }
 ],
 "symbols": {
   "langVersion": "ref:console-app:langVersion", // the parent template can pull out specific symbols and reuse them
   "framework": "ref:console-app:langVersion",
   "favoriteAnimal": {
     "type": "parameter",
     "description": "What's your favorite barn animal?",
     "datatype": "choice",
     "choices": [
       { "choice": "Pig" },
       { "choice": "Cow" },
       { "choice": "Chicken" }
     ]
   },
 }
}

Specifying a child template

Child templates need to be locateable to be installed or invoked. They can be referenced by one of two ways:

  • "<shortName>" - if the shortName is unique and the shortName is in the package search cache, then it's enough to specify the shortName to identify the template to invoke
  • "<PackageId>[@<PackageVersion>]:<shortName>" - this can be used to explicitly specify the nuget package containing the template, optionally its version, and the name of the template in the package. The version is optional, if not specified install will grab the latest version of the template.

Note

Only one level of template nesting will be supported. Specifically only templates of type project or item can be used as subtemplates.

Reusing symbols

We propose a new syntax for referencing symbols from another template: ref:<target>:<symbolName>. This allows a 'parent' template to point to symbol definitions in the child templates, and child template mappings to reference symbols from the parent. You can see an example of what this referencing might look like in the example above.

Hard-coding symbols

As all other templates can, compound templates can define their own symbols. These symbols can be used by the parent template's content during rendering, or be passed to child templates as shown above.

How the engine processes the new template

Installation

For the .NET CLI, installing a compound template will consist of two phases:

  • installing the initial template
  • installing all subtemplates referenced in the parent template

This should be treated as an atomic action - the user should be presented with a yes/no dialog and a list of the child templates that will be installed after the compound template is downloaded, and that list should direct the user to details pages/etc where they can learn more about each template before accepting or denying the install request. If the request is accepted, all child templates are installed. If the request is denied, the parent template is uninstalled. If any errors occur during installation, all installed templates are uninstalled.

Invocation

The compound template is the entrypoint of the invocation operation. The symbols of the compound template will be interrogated and used to bind any user inputs as symbols are for other templates today - the primary change being that the definitions for those symbols may be resolved from child templates.

Invocation of a compound template will proceed as follows:

  • parent symbols are resolved
  • each child template will be invoked in turn:
    • child template symbols will be defined, taking into account any inputs that come from the parent template's mappings
      • this includes name/output symbols
    • child template content is rendered
    • child template post-actions are run
  • parent template content is rendered (since it may rely on that of the child templates)
  • parent template postactions are run (since they may rely on that of the child templates)

References

dotnet/templating#2771

@joeloff
Copy link

joeloff commented Mar 21, 2024

Some questions:

  1. Can sub-templates reference templates that have sub-templates?
  2. Do we need to or are we already detecting potential cyclical references? For example, TemplateA is a compound template that depends on TemplateB, TemplateB is compound template that depends on TemplateC and TemplateC is a compound template that depends on TemplateA. Especially if these are coming from different packages that belong to multiple authors.
  3. Do we need to be concerned where multiple dependencies in a compound template are compound templates with the same dependency? So A is a compound template that include B and C. B and C are both compound templates that include D. They use the package syntax, but B and C come from different package versions and depend on different versions of D.

@baronfel
Copy link
Author

baronfel commented Mar 21, 2024

  1. I think for v1 we should limit to a single level of 'depth'.
  2. This is sidestepped if we only allow a depth of 1.
  3. Same answer?

I updated the spec with this constraint (which we had verbally decided in a previous meeting)

@phenning
Copy link

specify the child template to invoke (either by shortname alone or by a combination of PackageName/shortName for specificity)

  • Is there a reason for shortName here rather than groupIdentity / identity?
  • If parent has specific language - we'd use the same language for child templates if applicable, or if child template is not lang specific, just use it?
  • If parent does NOT have specific language, but listed child template does - what is the logic? Or is that an authoring error, or do we match default language?

For invocation flow, you mention that each child template is run, then its post actions are run. This infers that each host will be responsible for implementing this flow. This seems like there will be a bunch of duplicate effort here.

It would be great if we could isolate the Invocation logic into the template engine and have it call back into the host for running the host post action implementations as it went, rather than each host having to parse through the template metadata and loop through the templates. This is especially important for the cyclical questions which Jacques raises above.

Ideally, we'd have minimal changes to the CLI invocation path other than calling the new API and having the callbacks in place to invoke the post actions post child/parent-template creation.

Same applies to listing parameters, ideally, listing the symbols for the parent template via the API should just automatically map to the child templates without the host needing to manually query the child template's metadata.

@baronfel
Copy link
Author

@phenning broadly I agree with your points and actually intended for that to be the case - I'll see if I can make that more clear. The orchestration of child template invocation, hydration/shuttling around of symbols, etc should all be done by the engine. In the ideal world the only host-specific part of this would be the postAction registration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment