Provides extended code-generation capabilities that encapsulates Handlebars as the underlying code generator, or more specifically Handlebars.Net.
This is intended to provide the base for code-generation tooling enabling a rich and orchestrated code-generation experience in addition to the templating and generation enabled using native Handlebars.
The included change log details all key changes per published version.
Code generation can be used to greatly accelerate application development where standard coding patterns can be determined and the opportunity to automate exists.
Code generation can bring many or all of the following benefits:
- Acceleration of development;
- Consistency of approach;
- Simplification of implementation;
- Reusability of logic;
- Evolution of approach over time;
- Richness of capabilities with limited effort.
There are generally two types of code generation:
- Gen-many - the ability to consistently generate a code artefact multiple times over its lifetime without unintended breaking side-effects. Considered non-maintainable by the likes of developers as contents may change at any time; however, can offer extensible hooks to enable custom code injection where applicable. This approach offers the greatest long-term benefits.
- Gen-once - the ability to generate a code artefact once to effectively start the development process. Considered maintainable, and should not be re-generated as this would override custom coded changes.
The code-generation capabilities within OnRamp support both of the above two types.
OnRamp has been created to provide a rich and standardized foundation for orchestrating Handlebars-based code-generation.
The OnRamp code-generation tooling is composed of the following:
- Configuration - data used as the input to drive the code-generation via the underlying templates.
- Templates - Handlebars templates that define a specific artefact's content.
- Scripts - orchestrates one or more templates that are used to generate artefacts for a given configuration input.
The code-generation is driven by a configuration data source, in this case a YAML or JSON file. This acts as a type of DSL (Domain Specific Language) to define the key characteristics / properties that will be used to generate the required artefacts.
Configuration consists of the following, which each ultimately inherit from ConfigBase
for their base capabilities:
- Root node - inherits from
ConfigRootBase
to enable additional runtime parameters (IRootConfig
). - Child nodes - zero or more child nodes (hierarchical) that inherit from
ConfigBase<TRoot, TParent>
that specify theRoot
andParent
hierarchy.
The advantage of using a .NET typed class for the configuration is that additional properties (computed at runtime) can be added to aid the code-generation process. The underlying Prepare
method provides a consistent means to implement this logic at runtime.
The following attributes should be used when defining the .NET Types, as they enable validation, and corresponding schema/documentation generation (where required):
Attribute | Description |
---|---|
CodeGenClassAttribute |
Defines the schema/documentation details for the .NET class. |
CodeGenCategoryAttribute |
Defines one or more documentation categories for a .NET class. |
CodeGenPropertyAttribute |
Defines validation (IsMandatory , IsUnique and Options ) and documentation for a property (non-collection). |
CodeGenPropertyCollectionAttribute |
Defines validation (IsMandatory ) and documentation for a collection property. |
The configuration must also use the System.Text.Json
serializer attributes as System.Text.Json
is used internally to perform all JSON deserialization.
An example is as follows:
[CodeGenClass("Entity", Title = "'Entity' object.", Description = "The `Entity` object.", Markdown = "This is a _sample_ markdown.", ExampleMarkdown = "This is an `example` markdown.")]
[CodeGenCategory("Key", Title = "Provides the _Key_ configuration.")]
[CodeGenCategory("Collection", Title = "Provides related child (hierarchical) configuration.")]
public class EntityConfig : ConfigRootBase<EntityConfig>
{
[JsonPropertyName("name")]
[CodeGenProperty("Key", Title = "The entity name.", IsMandatory = true)]
public string? Name { get; set; }
[JsonPropertyName("properties")]
[CodeGenPropertyCollection("Collection", Title = "The `Property` collection.", IsImportant = true)]
public List<PropertyConfig>? Properties { get; set; }
protected override void Prepare()
{
Properties = PrepareCollection(Properties);
}
}
[CodeGenClass("Property", Title = "'Property' object.", Description = "The `Property` object.")]
[CodeGenCategory("Key", Title = "Provides the _Key_ configuration.")]
public class PropertyConfig : ConfigBase<EntityConfig, EntityConfig>
{
public override string QualifiedKeyName => BuildQualifiedKeyName("Property", Name);
[JsonPropertyName("name")]
[CodeGenProperty("Key", Title = "The property name.", IsMandatory = true, IsUnique = true)]
public string? Name { get; set; }
[JsonPropertyName("type")]
[CodeGenProperty("Key", Title = "The property type.", Description = "This is a more detailed description for the property type.", IsImportant = true, Options = new string[] { "string", "int", "decimal" })]
public string? Type { get; set; }
[JsonPropertyName("isNullable")]
[CodeGenProperty("Key", Title = "Indicates whether the property is nullable.")]
public bool? IsNullable { get; set; }
protected override void Prepare()
{
Type = DefaultWhereNull(Type, () => "string");
}
}
A corresponding configuration YAML example is as follows:
name: Person
properties:
- { name: Name }
- { name: Age, type: int }
- { name: Salary, type: decimal, isNullable: true }
Once the code-gen configuration data source has been defined, one or more templates will be required to define the artefact output. These templates are defined using Handlebars syntax. Template files can either be added as an embedded resource within a folder named Templates
(primary), or referenced directly on the file system (secondary), to enable runtime access.
Additionally, Handlebars has been extended to add additional capabilities beyond what is available natively to further enable the required generated output. Note that where any of the following documented functions denote String.Format
will result in the usage of the .NET String.Format
where the first argument is the format, and the remainder are considered the arguments referenced by the format.
The following functions represent additional Handlebars conditions:
Function | Description |
---|---|
ifeq |
Checks that the first argument equals at least one of the subsequent arguments. |
ifne |
Checks that the first argument does not equal any of the subsequent arguments. |
ifle |
Checks that the first argument is less than or equal to the subsequent arguments. |
ifge |
Checks that the first argument is greater than or equal to the subsequent arguments. |
ifval |
Checks that all of the arguments have a non-null value. |
ifnull |
Checks that all of the arguments have a null value. |
ifor |
Checks that any of the arguments have a true value where bool ; otherwise, non-null value. |
The following functions perform the specified string manipulation (generally using StringConverter
) writing the output to the code-generated aretfact:
Function | Description |
---|---|
format |
Writes the arguments using String.Format. |
lower |
Converts and writes a value as lower case. |
upper |
Converts and writes a value as upper case. |
camel |
Converts and writes a value as camel case. |
pascal |
Converts and writes a value as pascal case. |
private |
Converts and writes a value as private case. |
sentence |
Converts and writes a value as sentence case. |
past-tense |
Converts and writes a value as past tense. |
pluralize |
Converts and writes a singularized value as the plural. |
singularize |
Converts and writes a pluralized value as the single. |
see-comments |
Converts and writes a value as a C# <see cref="value"/> comments equivalent. |
The following functions perform miscellanous operations:
Function | Description |
---|---|
indent |
Inserts indent spaces based on the passed count value. |
add |
Adds all the arguments and writes the sum. |
set-value |
Sets the named property value on the underlying data context. Only accepts two arguments, the first being the property name as a string, and the second being the value. Nothing is output. |
add-value |
Adds all the arguments to the named property value on the underlying data context. Accepts at least a single argument, the first being the property name as a string. Where no further arguments are specified will add 1 by default. Nothing is output. |
As there is no native integrated means to set a breakpoint and debug the template directly, logging and debugger functions have been added to aid troubleshooting.
Function | Description |
---|---|
log-error |
Logs (ILogger.LogInformation ) the arguments using String.Format . |
log-warning |
Logs (ILogger.LogWarning ) the arguments using String.Format . |
log-error |
Logs Logs (ILogger.LogError ) the arguments using String.Format . |
log-debug |
Logs (Debug.WriteLine ) the arguments using String.Format . |
debug |
Logs (Debug.WriteLine ) the arguments using String.Format ; then invokes Debugger.Break . |
Any functions that denote String.Format
will result in the usage of the .NET String.Format
where the first argument is the format, and the remainder are the arguments.
Example usage is as follows:
To orchestrate the code generation, in terms of the Templates to be used, a YAML-based script-like file is used. Script files can either be added as an embedded resource within a folder named Scripts
(primary), or referenced directly on the file system (secondary), to enable runtime access.
The following are the root CodeGenScript
properties:
Property | Description |
---|---|
configType |
The expected .NET Configuration root node Type name (as used by Type.GetType ). This ensures that the code generator will only be executed with the specified configuration source. |
inherits |
A script file can inherit the script configuration from one or more parent script files specified by a script name array. This is intended to simplify/standardize the addition of additional artefact generation without the need to repeat. |
editorType |
The .NET Type name (as used by Type.GetType ) that provides an opportunity to modify the loaded configuration. The Type must implement IConfigEditor . This enables runtime changes to configuration where access to the underlying source code for the configuration is unavailable; see Personalization. |
generators |
A collection of none of more scripted generators. |
The following are the generators
collection CodeGenScriptItem
properties:
Attribute | Description |
---|---|
type |
The .NET Type name (as used by Type.GetType ) that will perform the underlying configuration data selection, where the corresponding Template will be invoked per selected item. This Type must inherit from either CodeGeneratorBase<TRootConfig> or CodeGeneratorBase<TRootConfig, TGenConfig> . The inherited SelectGenConfig method should be overridden where applicable to perform the actual selection. |
template |
The name of the Handlebars template that should be used. |
file |
The name of the file (artefact) that will be generated; this also supports Handlebars syntax to enable runtime computation. |
directory |
This is the sub-directory (path) where the file (artefact) will be generated; this also supports Handlebars syntax to enable runtime computation. |
genOnce |
This boolean (true /false ) indicates whether the file is to be only generated once; i.e. only created where it does not already exist (optional). |
genOncePattern |
The file name pattern to search, including wildcards, to validate if the file (artefact) already exists (where genOnce is true ). This is optional and where not specified will default to file . This is useful in scenarios where the file name is not fixed; for example, contains date and time. |
text |
The text written to the log / console to enable additional context (optional). |
Any other YAML properties specified will be automatically passed in as runtime parameters (name/value pairs); see IRootConfig.RuntimeParameters
.
An example of a Script YAML file is as follows:
configType: OnRamp.Test.Config.EntityConfig, OnRamp.Test
generators:
- { type: 'OnRamp.Test.Generators.EntityGenerator, OnRamp.Test', template: EntityExample.hbs, directory: "{{lookup RuntimeParameters 'Directory'}}", file: '{{Name}}.txt', Company: Xxx, AppName: Yyy }
- { type: 'OnRamp.Test.Generators.PropertyGenerator, OnRamo.Test', template: PropertyExample.hbs, directory: "{{lookup Root.RuntimeParameters 'Directory'}}", file: '{{Name}}.txt' }
A CodeGeneratorBase<TRootConfig, TGenConfig>
example is as follows:
// A generator that is targeted at the Root (EntityConfig); pre-selects automatically.
public class EntityGenerator : CodeGeneratorBase<EntityConfig> { }
// A generator that targets a Child (PropertyConfig); requires selection from the Root config (EntityConfig).
public class PropertyGenerator : CodeGeneratorBase<EntityConfig, PropertyConfig>
{
protected override IEnumerable<PropertyConfig> SelectGenConfig(EntityConfig config) => config.Properties!;
}
To enable code-generation the CodeGenerator
is used. The constructor for this class takes a CodeGeneratorArgs
that specifies the key input, such as the script file name. The additional argument properties enable additional capabilities within. The CodeGenerator
constructor will ensure that the underlying script configuration is valid before continuing.
To perform the code-generation the Generate
method is invoked passing the configuration file input. The configuration is initially parsed and validated, then each of the generators described within the corresponding script file are instantiated, the configuration data selected and iterated, then passed into the HandlebarsCodeGenerator
to perform the code-generation proper against the scripted template. A CodeGenStatistics
is returned providing basic statistics from the code-generation execution.
The CodeGenerator
can be inherited to further customize; with the OnBeforeScript
, OnCodeGenerated
, and OnAfterScript
methods available for override.
An example is as follows:
var cg = new CodeGenerator(new CodeGeneratorArgs("Script.yaml") { Assemblies = new Assembly[] { typeof(Program).Assembly } });
var stats = cg.Generate("Configuration.yaml");
OnRamp
has been optimized so that a new console application can reference and inherit the underlying capabilities.
Where executing directly the default command-line options are as follows.
Xxx Code Generator.
Usage: Xxx [options]
Options:
-?|-h|--help Show help information.
-s|--script Script orchestration file/resource name.
-c|--config Configuration data file name.
-o|--output Output directory path.
-a|--assembly Assembly containing embedded resources (multiple can be specified in probing order).
-p|--param Parameter expressed as a 'Name=Value' pair (multiple can be specified).
-cs|--connection-string Database connection string.
-cv|--connection-varname Database connection string environment variable name.
-enc|--expect-no-changes Indicates to expect _no_ changes in the artefact output (e.g. error within build pipeline).
-sim|--simulation Indicates whether the code-generation is a simulation (i.e. does not create/update any artefacts).
The recommended approach is to invoke the OnRamp capabilities directly and include the required Scripts and Templates as embedded resources to be easily referenced at runtime. Otherwise, the capabilities can be inherited and overridden to enhance the experience.
Where the out-of-the-box capabiltity of OnRamp is acceptable, then simply invoking the CodeGenConsole
will perform the code-generation using the embedded resources. The command-line arguments need to be passed through to support the standard options. Additional methods exist to specify defaults or change behaviour as required. An example Program.cs
is as follows.
using OnRamp;
using System.Threading.Tasks;
namespace My.Application
{
public class Program
{
static Task<int> Main(string[] args) => CodeGenConsole.Create<Program>().RunAsync(args);
}
}
The CodeGenConsoleBase
is designed to be inherited, with opportunities within to tailor the console application experience via the following key overrideable methods:
Method | Description |
---|---|
OnBeforeExecute |
Invoked before the underlying console execution occurs. This provides an opportunity to remove or add to the command-line commands and options. The command-line capabilities are enabled using CommandLineUtils . |
OnValidation |
Invoked after command parsing is complete and before the underlying code-generation. This provides an opportunity to perform additional validation on the command-line commands and options. |
OnCodeGeneration |
Invoked to instantiate and run a CodeGenerator using the CodeGeneratorArgs returning the corresponding CodeGenStatistics . This provides an opportunity to fully manage / orchestrate the code-generation. |
For an example of advanced usage, see Beef.CodeGen.Core
that uses this capability as the basis for its code-generation.
To enable consumers of a code-generator to personalize, and/or override, the published code-generation behaviour the Scripts and Templates can be overridden. This will avoid the need for a consumer to clone the solution and update unless absolutely neccessary. This is achieved by passing in Assembly
references which contain embedded resources of the same name; the underlying CodeGenerator
will use these and fall back to the original version where not overridden.
There is no means to extend the underlying configuration .NET types directly. However, as all the configuration types inherit from ConfigBase
the ExtraProperties
hash table is populated with any additional configurations during the deserialization process. These values can then be referenced direcly within the Templates as required. To perform further changes to the configuration at runtime an IConfigEditor
can be added and then referenced from within the corresponding Scripts
file; it will then be invoked during code generation enabling further changes to occur. The ConfigBase.CustomProperties
hash table is further provided to enable custom properties to be set and referenced in a consistent manner.
Some additional utility capabilites have been provided:
Class | Description |
---|---|
JsonSchemaGenerator |
Provides the capability to generate a JSON Schema from the configuration. This can then be published to the likes of the JSON Schema Store so that it can be used in Visual Studio and Visual Studio Code (or other editor of choice) to provide editor intellisense and basic validation. |
MarkdownDocumentationGenerator |
Provides the capability to generate markdown documentation files from the configuration. These can then be published within the owning source code repository or to a wiki to provide corresponding documentation. |
StringConverter |
Provides additional string conversions that are useful where generating code; for example: ToCamelCase , ToPascalCase , ToPrivateCase , ToSentenceCase , ToSnakeCase , ToKebabCase , ToPastTense , ToPlural , ToSingle , ToComments and ToSeeComments . |
These other Avanade repositories leverage OnRamp to provide code-generation capabilities:
- DbEx - Database and DBUP extensions.
- NTangle - Change Data Capture (CDC) code generation tool and runtime.
- Beef - Business Entity Execution Framework to enable industralisation of API development.
OnRamp is open source under the MIT license and is free for commercial use.
One of the easiest ways to contribute is to participate in discussions on GitHub issues. You can also contribute by submitting pull requests (PR) with code changes. Contributions are welcome. See information on contributing, as well as our code of conduct.
See our security disclosure policy.
Avanade is the leading provider of innovative digital and cloud services, business solutions and design-led experiences on the Microsoft ecosystem, and the power behind the Accenture Microsoft Business Group.