Search Results for

    Show / Hide Table of Contents

    Resolving type names

    Intent Architect has mechanisms and infrastructure to make it easy to resolve type names and generate any additional code to be able to use that type (EG: project references, using directives, imports, etc).

    Note

    We strongly recommend against using strategies like conventions to work out type names of classes which are generated by templates. There are many edge cases you will have to solve for yourself including handling collections or ensuring that appropriate using or import directives are also generated. Managing such complexities yourself invariably leads to more complicated templates which might not be able to handle all scenarios correctly.

    The GetTypeName(...) method

    The purpose of the GetTypeName system is to resolve dependencies. As part of that it also gets class names, configures any required project relationships (such as project references in .csproj files) and adds any required using directives/import statements as appropriate for the particular language being used.

    By routing all these concerns through a single method, it makes templates simpler which vastly improves maintainability of them and your modules. To demonstrate, the .tt file to generate a POCO is as simple as:

    <#@ template language="C#" inherits="CSharpTemplateBase<Intent.Modelers.Services.Api.DTOModel>" #>
    <#@ import namespace="Intent.Modules.Common.Templates" #>
    <#@ import namespace="Intent.Modules.Common.CSharp.Templates" #>
    
    [assembly: DefaultIntentManaged(Mode.Fully)]
    
    namespace <#= Namespace #>
    {
        public class <#= ClassName #>
        {
    <#  foreach (var field in Model.Fields) { #>
            <#= GetTypeName(field) #> <#= field.Name.ToPascalCase() #> { get; set; }
    <#  } #>
        }
    }
    

    Depending on the GetTypeName overload used and the type trying to be resolved, it may internally resolve an appropriate template instance. From this resolved template instance, information such as namespaces, package names, output locations and dependency requirements can be determined.

    TypeResolvers

    It is normal for some overloads of GetTypeName or if it is used with certain types, that there is no template instance to be resolved, for example for elements which reference Type Definitions which are for "basic" types such as a string or an int.

    In such cases language specific infrastructure, known as TypeResolvers, are used to determine how the requested type should be resolved. For example, when resolving an int in C# it should be resolved as int, but in TypeScript it should be resolved as a number. Base template types (such as CSharpTemplateBase or TypeScriptTemplateBase) have been pre-configured with language appropriate TypeResolvers.

    These language specific TypeResolvers have been configured to look at Stereotypes applied on Type Definition which can be used to specify language specific information about that type. For example, the TypeResolver used on CSharpTemplateBase looks for a C# Stereotype, and if found uses the properties on it, such as Namespace, when resolving the type.

    The AddTypeSource(...) method

    The GetTypeName system requires some additional information to know which template instances to match referenced types against, otherwise it falls back to outputting only the "name" of the element of the referenced type and doesn't know of any references/imports/using directives which may have been needed.

    For any of the referenced types which are generated by templates (even the same template type), you will need to use AddTypeSource(...) in the partial to let the GetTypeName system know which template instances to check against. For the above POCO example, in the template partial's constructor you would need to use AddTypeSource for each of the TemplateIds which are used, for example:

    AddTypeSource(TemplateId);
    AddTypeSource(EnumTemplate.TemplateId);
    

    Making the entire file look something like:

    using Intent.Engine;
    using Intent.Modelers.Services.Api;
    using Intent.Modules.Common;
    using Intent.Modules.Common.CSharp.Templates;
    using Intent.RoslynWeaver.Attributes;
    
    [assembly: DefaultIntentManaged(Mode.Fully)]
    [assembly: IntentTemplate("Intent.ModuleBuilder.CSharp.Templates.CSharpTemplatePartial", Version = "1.0")]
    
    namespace MyModule.Templates.Dto
    {
        [IntentManaged(Mode.Fully, Body = Mode.Merge)]
        partial class DtoTemplate : CSharpTemplateBase<DTOModel>
        {
            public const string TemplateId = "NewModule.Dto";
    
            [IntentManaged(Mode.Fully, Body = Mode.Ignore)]
            public DtoTemplate(IOutputTarget outputTarget, DTOModel model) : base(TemplateId, outputTarget, model)
            {
                AddTypeSource(TemplateId);
                AddTypeSource(EnumTemplate.TemplateId);
            }
    
            [IntentManaged(Mode.Fully, Body = Mode.Ignore)]
            protected override CSharpFileConfig DefineFileConfig()
            {
                return new CSharpFileConfig(
                    className: $"{Model.Name}",
                    @namespace: $"{this.GetNamespace()}",
                    relativeLocation: $"{this.GetFolderPath()}");
            }
        }
    }
    

    Resolving type names for templates fulfilling a role

    One of the mechanisms which can be used to decouple modules from each other is to have templates which fulfill roles and then other templates which need to refer to that template's type refer to it by the template's role name. By using a template role name instead of a particular TemplateId, it allows any module made at any time to fulfill that role without the "referencing" module being aware of it.

    An example of where this is used is with the Intent.EntityFrameworkCore module. Due to the non-trivial effort of making templates to generate entity type configurations, many Intent Architect module builders choose to use Intent Architect's highly mature Intent.EntityFrameworkCore module, but in many cases they also want to have their own custom template for generating entities.

    With Intent Architect's support for template fulfilling roles, this is possible. The builder of the module simply needs to configure that their entity template fulfills the same template role that Intent Architect's Intent.EntityFrameworkCore module's EntityTypeConfigurationTemplate is looking for.

    The EntityTypeConfigurationTemplate has the following method to resolve the entity type:

    private string GetEntityName()
    {
        return GetTypeName("Domain.Entity", Model);
    }
    

    And then a template fulfills a role by having it configured with the same value for Role in its Template Settings in the Module Builder:

    Template settings

    Collection formatting

    The final key concept to discuss is how the GetTypeName system knows that stringField should use List<T> as its collection type, for this you can configure it with collection format strings.

    Setting the default collection format

    The SetDefaultTypeCollectionFormat(...) method can be called in the partial's constructor to set the default collection format for types being resolved by GetTypeName in that template, for example:

    SetDefaultTypeCollectionFormat("System.Collections.Generic.List<{0}>");
    

    For these formats, {0} is what is substituted with the actual type.

    Setting the collection format for types resolved from a particular template

    The AddTypeSource(...) method has an overload with a collectionFormat parameter which takes in a formatted string as above, for example:

    AddTypeSource(EnumTemplate.TemplateId, "{0}[]");
    

    Setting the collection format for a particular usage of GetTypeName

    The GetTypeName(...) method has an overload with a collectionFormat parameter which takes in a formatted string as above, for example:

    return GetTypeName(field, "System.Collections.ObjectModel.Collection<{0}>");
    

    Forcing GetTypeName to resolve the non-collection version of a type

    Simply use "{0}" for the collectionFormat parameter, for example:

    return GetTypeName(field, "{0}");
    

    Forcing GetTypeName to resolve the collection version of a type

    GetTypeName doesn't have an overload for doing this directly, but you can just manually output the collection type inline with the non-collection version of the type, for example:

    return $"List<{GetTypeName(field, "{0}")}>";
    

    Module Builder generated Get<Template>Name methods

    For each module, the Module Builder generates a TemplateExtensions file within it, and for each template in that module utility methods are generated which can be used to more conveniently get type names for the templates. These utility methods ultimately call GetTypeName(...) so using them has all of the same benefits.

    Single File templates

    For cases where the template is configured as Single File in the Module Builder, a method is generated which looks similar to:

    public static string GetBaseRepositoryName<T>(this IntentTemplateBase<T> template)
    {
        return template.GetTypeName(BaseRepositoryTemplate.TemplateId);
    }
    

    Methods like the above are useful for scenarios like having to specify a base type, for example you have a Repository template which is generated per Class in the domain, but the generated class needs to derive from a BaseRepository which is generated by a different template. In such case you can simply use <#= this.GetBaseRepositoryName() #> in the templates .tt file and it will correctly resolve the type name.

    Note

    The this. is required because the generated GetBaseRepositoryName(...) method is an extension method.

    File Per Model templates

    For cases where the template is configured as File Per Model in the Module Builder, two methods are generated which look similar to:

    public static string GetRepositoryInterfaceName<T>(this IntentTemplateBase<T> template) where T : Intent.Modelers.Domain.Api.ClassModel
    {
        return template.GetTypeName(RepositoryInterfaceTemplate.TemplateId, template.Model);
    }
    
    public static string GetRepositoryInterfaceName(this IntentTemplateBase template, Intent.Modelers.Domain.Api.ClassModel model)
    {
        return template.GetTypeName(RepositoryInterfaceTemplate.TemplateId, model);
    }
    

    Because there are potentially multiple instances of the template, GetTypeName(...) needs to know which particular instance it should resolve the type from and in this case it will match using the model parameter.

    The first method can be useful in scenarios like having a repository for a particular Entity derive from an interface for that same particular Entity, for example you want to generate the following:

    public class MyEntityRepository : IMyEntityRepository
    {
    }
    

    As the model of the template you want to resolve has the same model for the template you are resolving from, you can simply call the first extension method in the .tt file:

    public class <#= Class #> : <#= this.GetRepositoryInterfaceName() #>
    {
    }
    

    The second method is generally only used for advanced scenarios where you know the template which generates the type you wish to resolve, but you don't want to use the AddTypeSource(...) method.

    The UseType(...) method

    For cases where you want Intent Architect to conditionally add import/using directives for a fully qualified type which isn't template generated (perhaps from a library), use the UseType(...) method, for example:

    private string GetDerivedFromType(ClassModel model)
    {
        if (!model.IsAggregateRoot())
        {
            return string.Empty;
        }
    
        return $" : {UseType("Library.IAggregateRoot")}";
    }
    

    For the above example, UseType(...) will ensure that using Library; is in the generated template output and return IAggregateRoot, but the using directive won't be added if IsAggregateRoot() returns false.

    The GetTypeInfo method

    For cases where you want to be able to reason about a resolved type, for example to know whether or not it is a collection, use the GetTypeInfo method.

    The GetTypeInfo method is actually used by the GetTypeName method to initially resolve the type which it then passes to the UseType method to resolve the string which it returns.

    Examples

    Scenario: the same template for the type

    You want to create a template which generates POCOs for DTOs defined in the Services Designer and where the DTOs may have fields whose type is another DTO, for example:

    Example of DTOs modelled in the Services Designer where one of the DTO's fields references another DTO

    We want our template to output the following for the Owner DTO:

    public class OwnerDto
    {
        public string Name { get; set; }
        public IEnumerable<PetDto> Pets { get; set; }
    }
    

    Notice that we intend to have the template automatically suffix the type names with Dto.

    The template

    The .cs partial file for the template would be:

    partial class DtoTemplate : CSharpTemplateBase<DTOModel>
    {
        public const string TemplateId = "MyModule.Dto";
    
        public DtoTemplate(IOutputTarget outputTarget, DTOModel model) :
            base(TemplateId, outputTarget, model)
        {
            AddTypeSource(DtoTemplate.TemplateId);
        }
    
        protected override CSharpFileConfig DefineFileConfig()
        {
            return new CSharpFileConfig(
                className: $"{Model.Name}Dto",
                @namespace: $"{this.GetNamespace()}",
                relativeLocation: $"{this.GetFolderPath()}");
        }
    }
    

    And the .tt file would be:

    [assembly: DefaultIntentManaged(Mode.Fully)]
    
    namespace <#= Namespace #>
    {
        public class <#= ClassName #>
        {
    <# foreach(var field in Model.Fields) { #>
            public <#= GetTypeName(field) #> <#= field.Name.ToPascalCase() #> { get; set; }
    <# } #>
        }
    }
    

    Note the <#= GetTypeName(field) #> part, this will instruct the template to automatically "get" the correct name of the type by finding a template instance using the passed in field, reading the ClassName property from it and it will also wrap it in IEnumerable<> if the Is Collection property has been selected for the Field in the Services Designer.

    Also note the AddTypeSource(DtoTemplate.TemplateId); line in the constructor, this instructs the GetTypeName system to also search for template instances with the provided TemplateId when resolving the type name.

    Scenario: customizing the collection type to generate

    This scenario is identical to the above scenario except that we want the collection type to be a List<> rather than an IEnumerable<> which would change our template output to:

    public class OwnerDto
    {
        public string Name { get; set; }
        public List<PetDto> Pets { get; set; }
    }
    

    The typical way to achieve this is to add the following to the constructor of the template's partial class:

    SetDefaultTypeCollectionFormat("List<{0}>");
    

    This makes it so that any time GetTypeName(...) is used within that template it will by default use the entered format.

    Tip

    The {0} within the composite string will be replaced by the resolved type.

    If you only want to change the collection format for a single GetTypeName(...) call, then you can do so using the overload which takes in string collectionFormat. In the .tt file change <#= GetTypeName(field) #> to instead be <#= GetTypeName(field, "List<{0}>") #> which will override the default collection format for just this call.

    Scenario: different template for the type

    You want to create two templates, one which generates an interface for each service and another which generates an implementation of that interface.

    For the interface template, we want it to output the following:

    public interface IOwnerService
    {
        // ...
    }
    

    For the implementation template, we want it to output the following:

    public class OwnerService : IOwnerService
    {
        // ...
    }
    

    The templates

    The partial file for the interface template would be:

    partial class ServiceInterfaceTemplate : CSharpTemplateBase<ServiceModel>
    {
        public const string TemplateId = "MyModule.ServiceInterface";
    
        public ServiceInterfaceTemplate(IOutputTarget outputTarget, ServiceModel model) :
            base(TemplateId, outputTarget, model)
        {
        }
    
        protected override CSharpFileConfig DefineFileConfig()
        {
            return new CSharpFileConfig(
                className: $"I{Model.Name}Service",
                @namespace: $"{this.GetNamespace()}",
                relativeLocation: $"{this.GetFolderPath()}");
        }
    }
    

    The partial file for the implementation template would be:

    partial class ServiceTemplate : CSharpTemplateBase<ServiceModel>
    {
        public const string TemplateId = "MyModule.Service";
    
        public ServiceTemplate(IOutputTarget outputTarget, ServiceModel model) :
            base(TemplateId, outputTarget, model)
        {
        }
    
        protected override CSharpFileConfig DefineFileConfig()
        {
            return new CSharpFileConfig(
                className: $"{Model.Name}Service",
                @namespace: $"{this.GetNamespace()}",
                relativeLocation: $"{this.GetFolderPath()}");
        }
    }
    

    The .tt file for the interface template would be:

    [assembly: DefaultIntentManaged(Mode.Fully)]
    
    namespace <#= Namespace #>
    {
        public interface <#= ClassName #>
        {
            // ...
        }
    }
    

    The .tt file for the implementation template would be:

    [assembly: DefaultIntentManaged(Mode.Fully)]
    
    namespace <#= Namespace #>
    {
        public class <#= ClassName #> : <#= this.GetServiceInterfaceName() #>
        {
            // ...
        }
    }
    

    In this case the <#= this.GetServiceInterfaceName() #> part is a call to the following extension method which the module builder automatically generated:

    public static string GetServiceInterfaceName<T>(this IntentTemplateBase<T> template)
        where T : ServiceModel
    {
        return template.GetTypeName(ServiceInterfaceTemplate.TemplateId, template.Model);
    }
    

    By using the extension method and not putting the logic directly in the template, it makes the template simpler. As with the same template for the type scenario, GetTypeName(...) is ultimately getting called, but in this case we're using an overload which takes in a template ID and from this Intent Architect knows to get the ClassName value from the template with that ID.

    Scenario: Resolving type name for a referenced type where the generating template may be unknown

    You're creating a template which generates the members for a type but the template to use varies depending on the type selected within an Intent Architect designer. For example a template for a DTO where a C# property is generated for each defined field, sometimes a field references a type from the Intent.Common.Types module (such as a string), other times it references another DTO generated by the same template, but a different instance of it.

    You will need use the GetTypeName(...) overload which takes in an IHasTypeReference parameter and then add AddTypeSource(<TemplateId of the generating template>) entries to the constructor, once for each template generated type which can possibly be referenced (which in this case is our DTO template's TemplateId).

    The .cs partial file for the template would be:

    partial class DtoTemplate : CSharpTemplateBase<DTOModel>
    {
        public const string TemplateId = "MyModule.Dto";
    
        public DtoTemplate(IOutputTarget outputTarget, DTOModel model) :
            base(TemplateId, outputTarget, model)
        {
            AddTypeSource(DtoTemplate.TemplateId);
        }
    
        protected override CSharpFileConfig DefineFileConfig()
        {
            return new CSharpFileConfig(
                className: $"{Model.Name}Dto",
                @namespace: $"{this.GetNamespace()}",
                relativeLocation: $"{this.GetFolderPath()}");
        }
    }
    

    And the .tt file would be:

    [assembly: DefaultIntentManaged(Mode.Fully)]
    
    namespace <#= Namespace #>
    {
        public class <#= ClassName #>
        {
    <# foreach(var field in Model.Fields) { #>
            public <#= GetTypeName(field) #> <#= field.Name.ToPascalCase() #> { get; set; }
    <# } #>
        }
    }
    
    • Edit this page
    ☀
    ☾
    In this article
    Back to top Copyright © 2017-, Intent Software Pte Ltd.