Creating Templates with External Module Dependencies
When creating your own modules, your module may be dependent on another module or have interplay with another module. This article covers various topics around this theme.
Automating the Installation of Dependent Modules
When talking about module dependencies, they come in one of the following forms:
- Hard: This is a required dependency; your module will not work without the dependent module being installed.
- Soft: This is an optional dependency; your module will run without the dependency but will have additional or different functionality if the dependent module is installed.
Automatically Installing Hard Dependency Modules
Assume you have a RepositoryModule
module and you want to write a new module RepositoryExtensionsModule
, which extends the functionality of the RepositoryModule
. In this case, the RepositoryExtensionsModule
has a hard dependency on the RepositoryModule
, as its functionality relies on the presence of the RepositoryModule
.
In this scenario, you can simply add a dependency
configuration in your RepositoryExtensionsModule
's Module Installation file.
<?xml version="1.0" encoding="utf-8"?>
<package>
<id>RepositoryExtensionsModule</id>
<version>1.0.0</version>
...
<dependencies>
<dependency id="RepositoryModule" version="2.0.0" />
...
</dependencies>
...
</package>
This will ensure that when your module RepositoryExtensionsModule
is installed, the RepositoryModule
is installed and is at least version 2.0.0
. (Module Installation)
Ensuring Minimum Module Dependency Versions for Soft Dependencies
Assume you have a RepositoryModule
module and you have a soft dependency on our CosmosDB module, namely Intent.CosmosDB
. If the Intent.CosmosDB
module is installed, your RepositoryModule
will have additional functionality. In this case, your module is built against a specific version of the Intent.CosmosDB
module, and you would want to ensure that the module is installed with at least that minimum version.
In this scenario, you can modify your RepositoryModule
's Module Installation file as follows:
<?xml version="1.0" encoding="utf-8"?>
<package>
<id>RepositoryModule</id>
<version>2.0.0</version>
...
<interoperability>
<detect id="Intent.CosmosDB">
<install>
<package id="Intent.CosmosDB" version="1.2.1" />
</install>
</detect>
...
</interoperability>
...
</package>
The above configuration reads as follows: if the Intent.CosmosDB
module is installed, ensure it is at least version 1.2.1
.
Configuring Conditionally Dependent Modules
Assume you have a RepositoryModule
, and you may also have an AspNetCore.RepositoryModule
module that you would like to conditionally install if the application has the Intent.AspNetCore
module installed.
In this scenario, you can modify your RepositoryModule
's Module Installation file as follows:
<?xml version="1.0" encoding="utf-8"?>
<package>
<id>RepositoryModule</id>
<version>2.0.0</version>
...
<interoperability>
<detect id="Intent.AspNetCore">
<install>
<package id="AspNetCore.RepositoryModule" version="1.0.0" />
</install>
</detect>
...
</interoperability>
...
</package>
The above configuration reads as follows: if the Intent.AspNetCore
module is installed, ensure the AspNetCore.RepositoryModule
module is installed, and at least version 1.0.0
.
Conditionally Running a Template Based on the Presence of a Module
If your module has a soft dependency on another module, you may have templates that you want to conditionally run if the dependent module is installed.
For example:
Let's say we have a template in our RepositoryModule
which has a CosmosInterfaceTemplate
. Now we only want to generate the CosmosInterface if the Intent.CosmosDB
module is installed.
We can achieve this scenario by overriding the template's CanRunTemplate
method.
...
public partial class CosmosInterfaceTemplate : CSharpTemplateBase<object>, ICSharpFileBuilderTemplate
{
...
public override bool CanRunTemplate()
{
return base.CanRunTemplate() && ExecutionContext.InstalledModules.Any(p => p.ModuleId == "Intent.CosmosDB");
}
...
}
In this code, you can see that the CanRunTemplate
method will return false
if the Intent.CosmosDB
module is not installed, causing the template not to execute, i.e., not generating any output.
Another common scenario may be to test for the presence of a file being generated by another module, commonly referred to as a template instance.
...
public partial class MyInterfaceImplementationTemplate : CSharpTemplateBase<object>, ICSharpFileBuilderTemplate
{
...
public override bool CanRunTemplate()
{
return ExecutionContext.FindTemplateInstances("MyInterfaceModule.MyInterface").Any();
}
...
}
In this code, you can see that the CanRunTemplate
method will return false
if there are no template instances of the template with the TemplateId MyInterfaceModule.MyInterface
. Put more simply, Intent will not generate a MyInterfaceImplementation
file if it is not generating a MyInterface
file.
Here are several other common examples of CanRunTemplate
implementations:
// If we are generating any files for `MyInterface.TemplateId`
return ExecutionContext.FindTemplateInstances(MyInterface.TemplateId).Any();
// If we are generating any files for `MyInterface.TemplateId`
return TryGetTypeName(MyInterface.TemplateId, out var interfaceTypeName);
// If we are generating any files for `MyInterface.TemplateId`
return ExecutionContext.FindTemplateInstance<IClassProvider>("MyInterfaceModule.MyInterface") != null;
// Based on a Module setting
return ExecutionContext.Settings.GetDatabaseSettings().DatabaseProvider().AsEnum() == DatabaseSettingsExtensions.DatabaseProviderOptionsEnum.Cosmos;
// Based on the existence of Designer elements
return ExecutionContext.MetadataManager.Services(ExecutionContext.GetApplicationConfig().Id).GetElementsOfType("Command");
// If we are generating a file for `Domain.Entity` for a specific designer model, passing in the `Model` parameter.
// e.g. If we have modeled a `Customer` class in the domain designer, this is checking are we generating a `Customer.cs` file specifically
return ExecutionContext.FindTemplateInstance<IClassProvider>("Domain.Entity", Model) != null;
// If we are generating a file for `Domain.Entity` for a specific designer Model
return TryGetTemplate<ICSharpFileBuilderTemplate>("Domain.Entity", Model, out var entityTemplate);
Referencing / Working with Template Instances from Other Modules
When building your own modules which have dependencies on other modules, it is often useful to be able to ask questions about that module and the code it is generating. Here are some common scenarios module builders run into.
Note
Most of these scenarios center around the ability to look up/discover other template instances during template execution. When searching for a template instance, it can be done by the TemplateId
or the Template's Role
. TemplateId
s should generally be unique and hence very specific. It is also possible to use Template Roles
, which means different templates, from potentially different modules, could produce the generated file. This is a way to decouple implementation dependencies or have different modules which could fulfill the Template Role
. For example, you could have a generic Repository
template role which could be fulfilled by CosmosDB.Repository.TemplateId
or EntityFrameworkCode.Repository.TemplateId
.
Is the Following File Being Generated?
Sometimes the code you wish to generate may be conditional on what other modules are installed and/or what they are doing. A fairly typical way to do this is to query and see if there is a template instance of a template generating a specific file.
// Get the `Domain.Entity` template instance for a specific ClassModel (this would be a `File Per Model` output template)
if (TryGetTemplate<ICSharpFileBuilderTemplate>("Domain.Entity", Model, out var entityTemplate))
{
}
// Get the `MyInterfaceModule.MyInterface` template instance (this would be a `Single File` output template)
if (TryGetTemplate<ICSharpFileBuilderTemplate>("MyInterfaceModule.MyInterface", out var myInterfaceTemplate))
{
}
// Get all the `Domain.Entity` template instances for all the ClassModels
var templateInstances = ExecutionContext.FindTemplateInstances("Domain.Entity");
The above are examples for discovering other template instances from within your own template.
Note
It is often fine to simply check for the presence of a template instance to know if a file is being generated. However, if the template you are looking up implements the CanRunTemplate
method, you should also check that this method returns true
.
What is the Type Information of a Class Generated by a Template?
If you have a template generating a file, you may need to know the type information of that type during the execution of your template.
For example:
A template with Id Domain.Entity
, may have a template instance for the Customer
ClassModel which outputs the following type MyApplication.Domain.Entity.Customer
;
var typeName = GetTypeName("Domain.Entity", Model);
if (TryGetTypeName("Domain.Entity", Model, out var typeName))
{
}
In the above example, assuming the Model
passed in was the Customer
one, typeName
would be set to MyApplication.Domain.Entity.Customer
.
Interrogate the Code That Is Actually Being Generated
It can be useful to have access to what code is being produced by another template to make decisions about what your template needs to do.
var template = GetTemplate<ICSharpFileBuilderTemplate>("Domain.Entity", Model);
// Access the to-be-generated code constructs through the builder pattern
var @class = entityTemplate.CSharpFile.Classes.First();
foreach (var property in @class.Properties)
{
}
In the above example, you are able to access and traverse the source being generated by the template through the builder. In this case, we are simply finding the first class
in the file and iterating over its properties.
Note
This example will only work for templates that implement a Builder
pattern, which allows for the interrogation of the generated code.