Skip to content

Instantly share code, notes, and snippets.

@daiplusplus
Last active September 1, 2021 17:43
Show Gist options
  • Save daiplusplus/f395a419e9d70c4a718284fdfeef007b to your computer and use it in GitHub Desktop.
Save daiplusplus/f395a419e9d70c4a718284fdfeef007b to your computer and use it in GitHub Desktop.
Dependency Injection Constructor Generator
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Globalization" #>
<#
//////////////////////////////
// Dependency Injection Constructor Generator - By Jehoel on GitHub - https://gist.github.com/Jehoel/
// Licensed in the Public Domain.
//////////////////////////////
// Q: What is this?
// A: This is a single T4 file that has a list of type names and their dependencies. It then generates beautifully formatted public constructors with null parameter validation.
// Q: Why would I want this?
// A: This is useful in .NET Core and ASP.NET Core projects that make appropriate use of DI, as you'll have many classes with long lists of injected dependencies.
// If you're working in a fast-moving project where dependencies change often then keeping the constructors well-formed (with validation) and in-sync with class fields becomes a pain.
// I note that using this file will be overkill if your types have only a couple of dependencies each (i.e. stick with hand-written constructors).
// Q: What if I have custom logic in my constructors?
// A: Short answer: you shouldn't have any custom logic in your constructors!
// A: Long answer: If you really need custom constructor logic, simply add a "p" suffix to each injectable and this T4 will then generate a separate scaffold private constructor (in a commented region you'd just copy+paste into your partial class definition) with those selected injectables passed into it.
// Remember that these private constructors are executed before the body of the T4-generated constructor, so you won't be able to use any of the generated private fields yet, so you might need to select more dependencies to pass-in.
// Q: What about Microsoft.Extensions.Logging?
// A: This T4 handles ILoggerFactory specially and creates an ILogger field and populates it with a typed logger from the ILoggerFactory. Feel free to customize this logic.
// Q: How are field names generated? What if I want a custom field name?
// A: Scroll down and look for the field '_typeNameToFieldNameMap'. This allows you to set a custom field name (also used for the parameter name) for the injected dependency.
// Have fun, no warranty!
// INSTRUCTIONS: List your classes that receive injected dependencies as public constructor parameters:
// For example, this T4 currently generates a two ASP.NET MVC controllers' constructors.
// The first one has a single injected dependency.
// The second one has a logger and also a private constructor which the IConfiguration object is passed into.
// NOTE: The _typeNameToFieldNameMap must be populated first (i.e. right here) before you define your types below.
_typeNameToFieldNameMap = new Dictionary<String,String>() {
{ "ILoggerFactory" , null }, // <-- 'null' means don't store as a field. If a injectable's line ends with " p" then it will be passed to the private constructor.
{ "RuntimeConfiguration" , "cfg" },
{ "IMyDbContext" , "db" },
{ "IIdentityServerInteractionService", "interactionService" },
};
_typeNamePrefixesToRemoveFromFieldNames = new[] { "Foobar" }; // This removes any common type-name prefixes from field-names if they're too verbose. e.g. If your project is named `Foobar` and use that as a class name prefix then any injected `MyProject.FoobarDatabaseService` will be stored in a field named `databaseService`.
_defaultInjectableNamespace = "MyCompany.MyProject"; // If an injected dependecy is not fully qualified, this namespace is used (does not apply to _typeNameToFieldNameMap's keys, though).
// LIST YOUR CLASSES AND THEIR DEPENDENCIES HERE:
// NOTE: If a injectable's line ends with " p" then it will be passed to the private constructor.
List<Klass> klasses = new List<Klass>() {
new Klass
(
"YourProject.Mvc.SimpleController",
@"
YourProject.IYourDbContext"
),
new Klass
(
"YourProject.Mvc.AdvancedController",
@"
Microsoft.Extensions.Logging.ILoggerFactory
YourProject.IYourDbContext
YourProject.IConfiguration p" // This parameter will be passed to a private constructor and a scaffolded constructor signature will be generated for you in a C# comment.
),
// Add more types here...
};
#>
using System;
<# foreach( Klass klass in klasses ) { #>
namespace <#= klass.Namespace #>
{
<# foreach( String injectableNamespace in klass.InjectablesNamespaces ) { #>
using <#= injectableNamespace #>;
<# } #>
public partial class <#= klass.TypeName #>
{
<# if( klass.HasLog ) { #>
private readonly <#= "ILogger".PadRight( klass.LongestInjectableFieldTypeName ) #> log;
<# } #>
<# foreach( Injectable inj in klass.Injectables.Where( i => i.IsField ) ) { #>
private readonly <#= inj.FieldTypeNamePad #> <#= inj.FieldName #>;
<# } #>
public <#= klass.TypeName #>
(
<# foreach( Injectable inj in klass.Injectables ) { #>
<#= inj.ParamTypeNamePad #> <#= inj.ParamName #><#= inj == klass.Injectables.Last() ? "" : "," #>
<# } #>
)
<# if( klass.PrivateCtor ) { #>
: this( <#= String.Join( ", ", klass.PrivateCtorInjectables.Select( i => i.ParamName ) ) #> )
<# } #>
{
<# if( klass.HasLog ) { #>
if( loggerFactory == null ) throw new ArgumentNullException( nameof(loggerFactory) );
this.log = loggerFactory.CreateLogger<<#= klass.TypeName #>>();
<# } #>
<# foreach( Injectable inj in klass.Injectables.Where( i => i.IsField ) ) { #>
this.<#= inj.FieldNamePad #> = <#= inj.ParamNamePad #> ?? throw new ArgumentNullException( nameof(<#= inj.ParamName #>) );
<# } #>
}
<# if( klass.PrivateCtor ) { #>
/* Generated constructor scaffold (copy and paste this into your class' main definition):
private <#= klass.TypeName #>( <#= String.Join( ", ", klass.PrivateCtorInjectables.Select( i => i.TypeName + " " + i.FieldName ) ) #> )
{
}
*/
<# } #>
}
}
<# } #>
<#+
static String _defaultInjectableNamespace;
static Dictionary<String,String> _typeNameToFieldNameMap;
static IReadOnlyList<String> _typeNamePrefixesToRemoveFromFieldNames;
class Klass {
public readonly String Namespace;
public readonly String TypeName;
public readonly Boolean PrivateCtor;
public readonly List<Injectable> Injectables;
public readonly List<String> InjectablesNamespaces;
public readonly List<Injectable> PrivateCtorInjectables;
public readonly Int32 LongestInjectableParamTypeName;
public readonly Int32 LongestInjectableFieldTypeName;
public readonly Int32 LongestInjectableFieldName;
public readonly Int32 LongestInjectableParamName;
// public Injectable LastInjectable => this.Injectables.Last();
public readonly Boolean HasLog;
public Klass( String fullyQualifiedName, String injectables ) {
Int32 dotIdx = fullyQualifiedName.LastIndexOf('.');
if( dotIdx == -1 ) {
this.Namespace = _defaultInjectableNamespace;
this.TypeName = fullyQualifiedName;
}
else {
this.Namespace = fullyQualifiedName.Substring( 0, dotIdx );
this.TypeName = fullyQualifiedName.Substring( dotIdx + 1 );
}
//
this.Injectables = injectables
.Split( new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries )
.Where( line => !String.IsNullOrWhiteSpace( line ) )
.Select( line => new Injectable( line.Trim() ) )
.ToList();
this.InjectablesNamespaces = this.Injectables.Select( i => i.Namespace ).Distinct().OrderBy( s => s ).ToList();
this.LongestInjectableParamName = this.Injectables .Max( i => i.ParamName.Length );
this.LongestInjectableParamTypeName = this.Injectables .Max( i => i.TypeName .Length );
this.LongestInjectableFieldName = this.Injectables.Where( i => i.IsField ).Max( i => i.FieldName.Length );
this.LongestInjectableFieldTypeName = this.Injectables.Where( i => i.IsField ).Max( i => i.TypeName .Length );
foreach( Injectable inj in this.Injectables ) {
inj.ParamNamePad = inj.ParamName.PadRight( this.LongestInjectableParamName );
inj.ParamTypeNamePad = inj.TypeName .PadRight( this.LongestInjectableParamTypeName );
inj.FieldNamePad = inj.FieldName.PadRight( this.LongestInjectableFieldName );
inj.FieldTypeNamePad = inj.TypeName .PadRight( this.LongestInjectableFieldTypeName );
}
this.HasLog = this.Injectables.Any( i => i.TypeName == "ILoggerFactory" );
this.PrivateCtor = this.Injectables.Any( i => i.PrivateCtor );
this.PrivateCtorInjectables = this.Injectables.Where( i => i.PrivateCtor ).ToList();
}
}
class Injectable {
public readonly String Namespace;
public readonly String TypeName;
public readonly String ParamName; // This is only used for the public constructor. The generated private constructor scaffold uses the fieldName (presumably, for brevity).
public readonly String FieldName;
public readonly Boolean IsField;
public readonly Boolean PrivateCtor;
public String ParamNamePad;
public String ParamTypeNamePad;
public String FieldNamePad;
public String FieldTypeNamePad;
public Injectable( String line ) {
line = line.Trim();
if( line.EndsWith( " p" ) ) {
this.PrivateCtor = true;
line = line.Substring( 0, line.Length - 2 ).Trim();
}
//
Int32 dotIdx = line.LastIndexOf('.');
if( dotIdx == -1 ) {
this.Namespace = _defaultInjectableNamespace;
this.TypeName = line;
}
else {
this.Namespace = line.Substring( 0, dotIdx );
this.TypeName = line.Substring( dotIdx + 1 );
}
if( _typeNameToFieldNameMap.TryGetValue( this.TypeName, out String fieldName ) ) {
if( fieldName == null ) {
this.IsField = false;
this.FieldName = GetFieldName( this.TypeName, removePrefixes: true );
}
else {
this.IsField = true;
this.FieldName = fieldName;
}
}
else {
this.IsField = true;
this.FieldName = GetFieldName( this.TypeName, removePrefixes: true );
}
this.ParamName = GetFieldName( this.TypeName, removePrefixes: false );
}
private static String GetFieldName( String typeName, Boolean removePrefixes ) {
if( typeName.Length <= 2 ) return typeName.ToLowerInvariant();
Boolean isInterface = typeName[0] == 'I' && typeName.Length >= 2 && Char.IsUpper( typeName[1] ); // If the TypeName looks like an interface name (E.g. "IFoo" but not "Internet" ) then omit the I from the fieldname.
if( isInterface ) typeName = typeName.Substring( 1 );
if( removePrefixes && _typeNamePrefixesToRemoveFromFieldNames != null && _typeNamePrefixesToRemoveFromFieldNames.Count > 0 ) {
// Remove the longest matching prefix, if any:
foreach( String typeNamePrefix in _typeNamePrefixesToRemoveFromFieldNames.OrderByDescending( prefix => prefix.Length ) ) {
if( typeName.StartsWith( typeNamePrefix, StringComparison.Ordinal ) ) {
typeName = typeName.Substring( typeNamePrefix.Length );
break;
}
}
}
return Char.ToLower( typeName[0] ) + typeName.Substring( 1 );
}
public Injectable( String @namespace, String name ) {
this.Namespace = @namespace;
this.TypeName = name;
this.FieldName = Char.ToLower( this.TypeName[0] ) + this.TypeName.Substring( 1 );
}
}
#>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment