Last active
September 1, 2021 17:43
-
-
Save daiplusplus/f395a419e9d70c4a718284fdfeef007b to your computer and use it in GitHub Desktop.
Dependency Injection Constructor Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <#@ 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