Skip to content
/ OnRamp Public

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

License

Notifications You must be signed in to change notification settings

Avanade/OnRamp

Repository files navigation


Logo


Introduction

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.


Status

CI NuGet version

The included change log details all key changes per published version.


Overview

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.


Capabilities

OnRamp has been created to provide a rich and standardized foundation for orchestrating Handlebars-based code-generation.


Composition

The OnRamp code-generation tooling is composed of the following:

  1. Configuration - data used as the input to drive the code-generation via the underlying templates.
  2. Templates - Handlebars templates that define a specific artefact's content.
  3. Scripts - orchestrates one or more templates that are used to generate artefacts for a given configuration input.

Configuration

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:

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.


Example

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 }

Templates

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.


Conditional functions

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.

String manipulation functions

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.

Miscellaneous functions

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.

Troubleshooting functions

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

Example usage is as follows:

{{#ifeq Type 'int' 'decimal'}}Is a number.{{/ifeq}}
{{format '{0:yyyy-MM-dd HH:mm:ss}' Order.Date}}
{{camel Name}}
{{log-warn 'The name {0} is not valid.' Name}}
{{set-value 'Check' true}}

Scripts

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.


Root

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.

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.


Example

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!;
}

Code-generation

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");

Console application

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.


Invoke

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);
    }
}

Inherit

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.


Personalization

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.


Utility

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.

Other repos

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.

License

OnRamp is open source under the MIT license and is free for commercial use.


Contributing

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.


Security

See our security disclosure policy.


Who is Avanade?

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.

About

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

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks