Testura.Code icon indicating copy to clipboard operation
Testura.Code copied to clipboard

Roadmap? Any plans to add Record types or 'File Scoped Namespaces'? Fluent Interfaces?

Open jeffward01 opened this issue 2 years ago • 8 comments

Hello!

@MilleBo I just wanted to thank you again for this awesome library! It is probably my favorite tool I have ever worked with. Before, I used Handlebars.Net which allowed me to build very quickly, however, it was not flexible at all. Now I am working with Testura.Code and love it! It took some time for me to get started, due to the many options which you provide (It's a good thing!).

I wanted to ask you a few questions:

A.) Roadmap Questions:

  1. Any plans to add 'Record Types'?

Personally, i very much dislike the 'Primary Constructor syntax' which Microsoft has introduced.

Primary Constructor Pattern for records:

public readonly record struct DailyTemperature(double HighTemp, double LowTemp);

^^^ In my opinion, it (Primary Constructor Pattern) is strange, and does not conform to the rest of the C# formatting patterns.

I believe records should only be formatted like:

My preferred way:

public record SomeRandomName
{
   public string FirstName { get; } 

   public string LastName { get; } 

   public string FullName => $"{this.FirstName} {this.LastName}";
}
  1. Any plans to allow File-scoped namespaces?

Originally I disliked file-scoped namespaces, now I have grown to love them. It helps force cleaner code. I think file scoped namespaces are a step in the correct direction, now only if Microsoft forced one class or record per file <3.

  1. What do you have planned next? A Roadmap? or a 'To-Do list? I understand this is not your full-time job <3, I just wanted to know that if I were to submit pull-requests, what is high-priority, and what is low-priority?

B.) Code Questions:

  1. Where in Testura.Code can I find the compilation code for 'Class' or 'Enum'? I would like to work on added Record Types if this is something you would like to add.

C.) Code I have developed for Testura.Code:

I have built some tools that help me work with Testura.Code easier and quicker, if you think these features are useful, i can share the code with you and you can review it closely to see if its helpful for Testura.Code. My thoughts are that Testura.Code is pure, so you want to be careful with introducing code that is 'extra'. The choice is yours!

  1. I have built fluent interfaces for most of the library:
 var built = SmartTesturaClassBuilder.Initialize()
            .WithClassName(className)
            .WithoutInheritance()
            .WithDestinationFile(fileDestination)
            .AsPublic()
            .WithUsingStatements("SomeNamespace")
            .WithAttributes(oneType)
            .WithSummary("SomeSummary")
            .WithoutProperties()
            .Build();

I have more examples, but this gives you the idea

  1. I have built an enhanced 'Custom Type Creator'. So that you can easily create types like this:
SmartEnum<MyEnumClass, int>
ICollection<MyCustomClass>
ICollection<MyOtherClass, MyThirdClass>

It is a small improvement on your class 'TypeGenerator'

I find that this is helpful for my use-case. For example, I am writing a code generator that 'reads' files, and then generates code. The issue is that I do not always have access to the .dll, so it makes reflection oriented generation difficult.

  1. I have a small program which scans the 'nuget cache' on the local computer then automatically inserts the correct using statements and namespace location of the file so that the developer does not need to think about this.

Currently, I compile everything as a 'string', then I do my extra work such as file-scoped namespaces, record types, and using statement injection, then I save it as a code file.

Your code is awesome and very easy to work with!!! Let me know if i can help or submit pull requests!

My coding pattern is not quite the same as your's, I also use some other libraries which I created (Like Common methods, etc) that I will need to remove, etc... If your interested I can show you the code and we can have a chat about what you would like and think about things.

I am at your service!

Thanks!

jeffward01 avatar Jan 12 '22 20:01 jeffward01

Hey!

Glad you like the framework and that you find the useful.

Any plans to add 'Record Types'?

I'm a bit behind so I haven't checked on a lot of the new C# futures yet but that sounds like a good thing to add! I think we should be able to support both types (normal and primary constructor) with a new RecordBuilder.

Any plans to allow File-scoped namespaces?

This one seems really easy to add and we can just have it as a setting in the builder. So I will fix it.

What do you have planned next? A Roadmap? or a 'To-Do list?

Right now I don't have anything special (except for your suggestions so I will start to work on those). As you may notice I actually took a break from this project but recently got motivated/time to start again.

So if you have anything more you want to add or wanna make a pull request yourself I'm open to look at it. I happy for all kinds of contributions so even maintenance or improvements is super nice.

B.) Code Questions:

You can find how we generate classes in the Testura.Code.Builders namespace. Enums can be generated by EnumGenerator in Testura.Code.Generators.Common

But I can take a look on the new record type for it will probably just be a new builder.

I have built fluent interfaces for most of the library:

That's really cool! I know many people lite fluent designs so that's maybe something we should have in the framework as an additional way of creating code. How much can you do with it? For example have you done fluent interfaces for methods too?

Feels like we should support all the different builders if we want to have it. But if you want to share the code/create a pull request that would be awesome.

I have built an enhanced 'Custom Type Creator'. So that you can easily create types like this:

Interesting. I know the problem with reflection and that you sometimes doesn't have the "real" type and that's why I created the CustomType class so you can use it togheter with the TypeGenerator.

But maybe your solution is better or something we should build into the TypeGenerator?

I will look into records and namespaces and should probably have something ready pretty soon. But I'm super interested in some of your own improvements/solution of things so if you want you can either share more of them here or try to work them into the framework and create a pull request that we can discuss.

MilleBo avatar Jan 13 '22 11:01 MilleBo

Awesome!!

I have a long reply, feel free to not reply to each thing <3

Small Rant and Praise

Thanks for getting back to me so quickly, I have looked at your code for a long time, and compared it to many other 'code generators' out there. your Syntax is the most verbose while also providing 'easy' configuration. I mean 'easy', because you still have control to do any-thing you wan programmatically (contrary to this one which uses string), and it is more lucid than this similar library for example.

Personally I dislike the CsCodeGenerator library because it takes the 'Service' approach which means you have many classes floating around that all need to work together, while Testura.Code is tight in a single 'macro' Fluent Interface Builder.

Personally i love code-generation because if you want to build a new API, a new project, a new app, you have so much boiler-plate. Then you say "I will use Templates! Hello Handlebars.NET - and its cool, it works... But then you want to make a tiny tiny change, you must manually edit all the template files, and it becomes a pain in the butt.

Sorry for rant - but this is the reasons why both Code Generation and Testura.Code are so cool!


Welcome back!!!

I have seen that you are more active on this! I discovered your code maybe 9 months ago and it was rather inactive (complete - but not recent commits which is fine <3 ) - I was very happy to see that your back!!

What interests you about code-generation? I saw you have some gaming repositories, are you lazy like me too and don't like writing boiler-plate code?

Im very happy to have you back! Really <3


I'm a bit behind so I haven't checked on a lot of the new C# futures yet but that sounds like a good thing to add! I think we should be able to support both types (normal and primary constructor) with a new RecordBuilder.

Very cool! I will look into this too!

This one seems really easy to add and we can just have it as a setting in the builder. So I will fix it.

I checked the Roslyn settings that you shared here and did not see an option for File-Scoped namespaces, you definitely know your library better than I do! I would be curious to know how you did the change so quickly, purely from a learning perspective!

1.) Response to the Attribute Answer

Thanks for pointing that out about Testura.Code.Builders, I see its practically stringified, and should be an easy enhancement to allow Sieve styled Attributes, very cool that its so simple!

2.) Fluent Interfaces

Yea! I have it implemented for these areas below, I plan to add the rest very soon

  • Types with Generics
  • Classes
  • Attributes (Only have TypeArgument and ValueArgument implemented, I will do the others soon
  • ClassProperties (done)

Next on my to-do list is (in-order):

  • The Sieve Attribute enhancement
  • Methods
  • Finish Attributes
  • Constructors
  • Enums

My use-case is I am building a generator like Wrapt but much 'smarter' (no template files, it reads your codes then auto-scaffolds - I have to build some Mappers to map from DTO's to DBO's and back, so I will be doing this soon!

I implemented Fluent Interfaces because your code really gives the developer much control, which is cool, but its almost overwhelming too with the number of options you have in some cases. So personally, each time I would want to use Testura.Code I would have to 'read the code again', and see what goes where, and almost re-learn how to work with Testura.Code again each time.

With Fluent Interfaces, its pretty tight, and specific on what you can use, like:

Attributes

This generates the attributes below:


    private ISmartTesturaAttributeEntry _generateTypeAttribute(params TypeGroup[] valueList)
    {
        var attribute = SmartTesturaAttributeBuilder.Initialize()
            .WithAttributeName("SampleValueAttribute")
            .WithTypeParameters()
            .WithTypes(valueList)
            .Generate();

        return attribute;
    }

    private ISmartTesturaAttributeEntry _generateValueAttribute(params string[] valueList)
    {
        var attribute = SmartTesturaAttributeBuilder.Initialize()
            .WithAttributeName("SampleValueAttribute")
            .WithValueParameters()
            .WithArguments(valueList)
            .Generate();

        return attribute;
    }

Then you can inject the attributes into the SmartTesturaClassBuilder


    [Fact]
    public void CreateSimpleClass_WithOneAttributeValueTwoParams()
    {
        var className = CsSourceCodeFileName.WithFileName("WithOneAttributeValueTwoParams");
        var fileDestination = TesturaTestHelper.GetTestFile(className);
        fileDestination.AbsoluteFilename.DeleteFilesystemItemIfExists();

        var twoValueParamsOneAttribute = this._generateValueAttribute("first", "second");

     // The namespace is auto-generated based on folder hierarchy 
        var built = SmartTesturaClassBuilder.Initialize()
            .WithClassName(className.FileNameWithoutExtension)
            .WithoutUsingStatements()
            .WithoutInheritance()
            .WithDestinationFile(fileDestination)
            .AsPublic()
            
            .WithAttributes(twoValueParamsOneAttribute)
            .WithSummary(TesturaTestHelper.GetClassSummary(className))
            .WithoutProperties()
              
             // Compile options = Save to file or string
            .Compile(this.compileOptions);

        // This comments the code out so it doesnt mess with the compiler and give me build errors
        fileDestination.ToDisabledCodeFile();

        var lines = File.ReadAllLines(built.DestinationFile.AbsoluteFilename.AbsolutePath);
        var simpleValueFirstAttributeExists = lines.Any(_ =>
            _.Contains(this._valueAttributeNameFirstAttribute, StringComparison.Ordinal));

        var firstValueExists = lines.Any(_ => _.Contains("first", StringComparison.Ordinal));
        var secondValueExists = lines.Any(_ => _.Contains("second", StringComparison.Ordinal));
        Assert.True(simpleValueFirstAttributeExists);
        Assert.True(firstValueExists);
        Assert.True(secondValueExists);
    }

The final file will look



namespace SomeDestination.NamespaceWill.GoHere;

    [SampleValueAttribute("first", "second")]
    /// <summary>
    /// This is a generated summary for WithOneAttributeValueTwoParams.cs class.
    /// </summary>
    public class WithOneAttributeValueTwoParams
    {
    }

Here is an example with generic types:


    public void ShouldGenerateType_WithInheritedFromWithTwoGenericsAndSingle()
    {
        var className = CsSourceCodeFileName.WithFileName("ShouldBePublicWithDoubleInheritedWithAndSingleParams");
        var fileDestination = TesturaTestHelper.GetTestFile(className);
        fileDestination.AbsoluteFilename.DeleteFilesystemItemIfExists();
        var single = TypeGroup.Load("CustomClassThree");
        var loaded = TypeGroup.Load("SmartEnum", "TestEnumValue", "int");

        var built = SmartTesturaClassBuilder.Initialize()
            .WithClassName(className.FileNameWithoutExtension).WithoutUsingStatements()
            .WithInheritanceFrom(loaded, single)
            .WithDestinationFile(fileDestination)
            .AsPublic()
            
            .WithoutAttributes()
            .WithSummary(TesturaTestHelper.GetClassSummary(className))
            .WithoutProperties()
            .Compile(this.compileOptions);

        // This comments the code out so it doesnt mess with the compiler and give me build errors
        fileDestination.ToDisabledCodeFile();
        var lines = File.ReadAllLines(built.DestinationFile.AbsoluteFilename.AbsolutePath);
        var exists = lines.Any(_ =>
            _.Contains(
                $"public class {className.FileNameWithoutExtension} : SmartEnum<TestEnumValue, int>, CustomClassThree",
                StringComparison.Ordinal));
        Assert.True(exists);
    }

Result:


namespace SomeDestination.NamespaceWill.GoHere;

    /// <summary>
    /// This is a generated summary for ShouldBePublicWithDoubleInheritedWithAndSingleParams.cs class.
    /// </summary>
    public class ShouldBePublicWithDoubleInheritedWithAndSingleParams : SmartEnum<TestEnumValue, int>, CustomClassThree
    {
    }

The Generic Type Builder:

Yea I know what you mean 100%. I tried to implement this about 9 months ago when I first found your library, and I really struggled. Now I took another look at it and really just followed these and I think a stack overflow post:

https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection

https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-define-a-generic-type-with-reflection-emit

https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.generictypeparameterbuilder?view=net-6.0

https://stackoverflow.com/a/1151470 <-- very helpful

I would love to share my code with you in a pull request, i'll make a new comment for that, this has gotten long, sorry

jeffward01 avatar Jan 14 '22 06:01 jeffward01

Topic: Pull Requests and Code Exchange

So I would love to show you the code for everything and create some pull requests, the issue is currently its pretty tightly coupled to some interfaces that are used in my '(.cs) file reader) code. It will take me a few days to separate it properly and allow for a clean code-share. I also use some helper nuget packages I made for string helpers and what-not. All my code is on Azure Pipelines.

Sorry this is not cleaner, I will rip-out my custom nuget packages and tightly linked interfaces so that I can share the code properly with you.

Unless if you don't mind I invite you to my Azure Dev Ops and you can check the code out there before I clean it up.

Here is the Type Builder that I was talking about that works with Generics. Currently it only does 'level 1 depth' so this:

SmartEnum<OneType, int, AnotherType, AnotherDifferentType, ThisCanGoOnForever>

I have to add code to allow it to do:

SmartEnum<OneType, int, AnotherType<IOneLevelDeeper<AgainNested, WoahSomethingElse, ICollection<SomeWrapperResult<AnotherThing>>>, AnotherClass>

It's actually very easy to do this, I will make this enhancement very quickly.

Here is the code below for the Generics, let me know if you want an invite to the Repo before I clean it up for you!!


// This is what holds the names of the types
public record TypeGroup
{
    private TypeGroup(string typeName)
    {
       // This is SmartEnum from my Test class, and also CustomClassThree
        this.TypeName = typeName;


         // This below is based on my test class will hold two values: TestEnumValue, and int
        // TODO: Add sub-wrapped types, to allow a depth of n+ generic interfaces
        this.GenericTypes = new Collection<WrappedType>();
    }

    // TODO: Remove 'Wrapped Type' and use a nested 'type-group' to allow n+ depth
    private TypeGroup(string typeName, ICollection<string> genericParameters)
    {
        var wrappedTypes = new Collection<WrappedType>();
        this.TypeName = typeName;
        foreach (var genericParameter in genericParameters)
        {
            wrappedTypes.AddWrappedType(genericParameter);
        }

        this.GenericTypes = wrappedTypes;
    }

    private TypeGroup(string typeName, ICollection<WrappedType> genericParameters)
    {
        this.TypeName = typeName;
        this.GenericTypes = genericParameters;
    }

    public string TypeName { get; }

    public ICollection<WrappedType> GenericTypes { get; }

    public static TypeGroup Load(string typeName)
    {
        Guard.Against.NullOrWhiteSpace(typeName, nameof(typeName));

        return new TypeGroup(typeName);
    }

    public static TypeGroup Load(string typeName, ICollection<string> genericParameters)
    {
        Guard.Against.NullOrWhiteSpace(typeName, nameof(typeName));
        Guard.Against.NullOrEmpty(genericParameters, nameof(genericParameters));
        genericParameters.ForEach(_ => Guard.Against.NullOrWhiteSpace(_, nameof(genericParameters)));

        return new TypeGroup(typeName, genericParameters);
    }

    public static TypeGroup Load(string typeName, params string[] genericParameters)
    {
        Guard.Against.NullOrWhiteSpace(typeName, nameof(typeName));
        Guard.Against.NullOrEmpty(genericParameters, nameof(genericParameters));
        genericParameters.ForEach(_ => Guard.Against.NullOrWhiteSpace(_, nameof(genericParameters)));

        return new TypeGroup(typeName, genericParameters);
    }

    public static TypeGroup Load(string typeName, ICollection<WrappedType> genericParameters)
    {
        Guard.Against.NullOrWhiteSpace(typeName, nameof(typeName));
        Guard.Against.NullOrEmpty(genericParameters, nameof(genericParameters));
        genericParameters.ForEach(_ => Guard.Against.Null(_, nameof(genericParameters)));

        return new TypeGroup(typeName, genericParameters);
    }
}
  // TODO: Remove WrappedType entirely and replace with nested TypeGroup

public record WrappedType
{
    public WrappedType(int index, string typeName)
    {
        this.Index = index;
        this.TypeName = typeName;
    }

    public int Index { get; }

    public string TypeName { get; }
}

Here is the logic of the code, you'll see I have a region called "Private shadow methods" - this is copied from MIcrosoft Docs:

https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-examine-and-instantiate-generic-types-with-reflection

https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/how-to-define-a-generic-type-with-reflection-emit

https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.generictypeparameterbuilder?view=net-6.0

Basically its 'junk' code (The private shadow properties' - The compiler just needs to see that the type has properties, then it will create a generic. Idk where exactly I read this, but its for the compiler, the properties aren't actually created.


internal class SmartTesturaTypeHelper
{
    private readonly AssemblyName _assemblyName;
    private readonly string _moduleName = "MainModule";

    public SmartTesturaTypeHelper()
    {
        var assemblyNameString = Guid.NewGuid()
            .ToString();
        this._assemblyName = new AssemblyName(assemblyNameString);
    }


    public Type GenerateTypes(DotNetCollectionType collectionType, ICollection<WrappedType> wrappedType)
    {
        Guard.Against.Null(collectionType, nameof(collectionType));
        Guard.Against.NullOrEmpty(wrappedType, nameof(wrappedType));

        var genericTypeName = $"{collectionType.Name}`{wrappedType.Count}";
        var finalType = this._createNewType(genericTypeName, wrappedType);

        return finalType;
    }

    public Type GenerateTypes(string type)
    {
        Guard.Against.NullOrWhiteSpace(type, nameof(type));
        var single = CustomType.Create(type);

        return single;
    }

    public ICollection<Type> GenerateTypes(ICollection<string> types)
    {
        var resultList = new List<Type>();

        Guard.Against.NullOrEmpty(types, nameof(types));

        foreach (var type in types)
        {
            if (type.IsNotNullNotEmpty())
            {
                var single = CustomType.Create(type);
                resultList.Add(single);
            }
        }

        return resultList;
    }


    public ICollection<Type> GenerateTypes(ICollection<TypeGroup> typeWithGenericList)
    {
        var resultList = new List<Type>();
        Guard.Against.NullOrEmpty(typeWithGenericList, nameof(typeWithGenericList));
        foreach (var withGeneric in typeWithGenericList)
        {
            if (withGeneric.GenericTypes.Count == 0)
            {
                var single = CustomType.Create(withGeneric.TypeName);
                resultList.Add(single);

                return resultList;
            }

            var genericTypeName = $"{withGeneric.TypeName}`{withGeneric.GenericTypes.Count}";
            var typeCollection = this._createNewType(genericTypeName, withGeneric.GenericTypes);
            resultList.Add(typeCollection);
        }

        return resultList;
    }

    public Type GenerateTypes(TypeGroup typeWithGeneric)
    {
        Guard.Against.Null(typeWithGeneric, nameof(typeWithGeneric));
        Guard.Against.NullOrEmpty(typeWithGeneric.GenericTypes, nameof(typeWithGeneric));

        if (typeWithGeneric.GenericTypes.Count == 0)
        {
            return CustomType.Create(typeWithGeneric.TypeName);
        }

        var genericTypeName = $"{typeWithGeneric.TypeName}`{typeWithGeneric.GenericTypes.Count}";
        var finalType = this._createNewType(genericTypeName, typeWithGeneric.GenericTypes);

        return finalType;
    }


    public Type GenerateTypes(ICollection<WrappedType> wrappedType)
    {
        Guard.Against.NullOrEmpty(wrappedType, nameof(wrappedType));
        if (wrappedType.Count == 1)
        {
            return CustomType.Create(wrappedType.First()
                .TypeName);
        }

        var firstEntry = wrappedType.TakeItemByIndex(0);

        var genericTypeName = $"{firstEntry.TypeName}`{wrappedType.Count}";
        var finalType = this._createNewType(genericTypeName, wrappedType);

        return finalType;
    }


    private TypeInfo _compileCollectionTypeInfo(string primaryTypeName, ICollection<WrappedType> wrappedCustomType)
    {
        var tb = this._getTypeBuilder(primaryTypeName, wrappedCustomType);

        // This was from the sample code I used, Im not sure what I can use this for yet, so I left it here in-case if I need it later for some reason
        var constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName |
                                                      MethodAttributes.RTSpecialName);
         
        // These are the shadow properties that also do not get created
        var yourListOfFields = new List<FieldDescriptor>
        {
            new("YourProp1", typeof(string)),
            new("YourProp2", typeof(int))
        };
        foreach (var field in yourListOfFields)
        {
            this._createShadowProperty(tb, field.FieldName, field.FieldType);
        }

        var objectTypeInfo = tb.CreateTypeInfo();

        return objectTypeInfo;
    }

    private Type _createNewType(string primaryTypeName, ICollection<WrappedType> wrappedCustomType)
    {
        var myTypeInfo = this._compileCollectionTypeInfo(primaryTypeName, wrappedCustomType);
        //  var myType = myTypeInfo.AsType();

        return myTypeInfo;
    }

    // This is when the properties get generated, and they only get generated for the 'compiler' not in real life. It tricks the compiler somehow.
    // I saw this and copied it from someplace, I forget where.
    #region private shadow methods - do not need to see.

    private void _createShadowProperty(TypeBuilder tb, string propertyName, Type propertyType)
    {
        var fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

        var propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType,
            null);
        var getPropMthdBldr = tb.DefineMethod("get_" + propertyName,
            MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType,
            Type.EmptyTypes);
        var getIl = getPropMthdBldr.GetILGenerator();

        getIl.Emit(OpCodes.Ldarg_0);
        getIl.Emit(OpCodes.Ldfld, fieldBuilder);
        getIl.Emit(OpCodes.Ret);

        var setPropMthdBldr = tb.DefineMethod("set_" + propertyName,
            MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[]
            {
                propertyType
            });

        var setIl = setPropMthdBldr.GetILGenerator();
        var modifyProperty = setIl.DefineLabel();
        var exitSet = setIl.DefineLabel();

        setIl.MarkLabel(modifyProperty);
        setIl.Emit(OpCodes.Ldarg_0);
        setIl.Emit(OpCodes.Ldarg_1);
        setIl.Emit(OpCodes.Stfld, fieldBuilder);

        setIl.Emit(OpCodes.Nop);
        setIl.MarkLabel(exitSet);
        setIl.Emit(OpCodes.Ret);

        propertyBuilder.SetGetMethod(getPropMthdBldr);
        propertyBuilder.SetSetMethod(setPropMthdBldr);
    }

    #endregion


    private TypeBuilder _getTypeBuilder(string primaryTypeName, ICollection<WrappedType> wrappedCustomType)
    {
        var primaryTypeSignature = primaryTypeName;

        // I create a random assembly name for each 'run' so that all associated types can play nice when generating.
        var an = new AssemblyName(primaryTypeSignature);
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(this._assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule(this._moduleName);

        var tb = moduleBuilder.DefineType(primaryTypeSignature,
            TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass |
            TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout, null);

        var typeParamNames = wrappedCustomType.Select(_ => _.TypeName)
            .ToArray();

        // This is what actually injects the generic parameters
        var typeParams = tb.DefineGenericParameters(typeParamNames);

        // This is old code from the Microsoft Example, I should remove the code below
        //  var TFirst = typeParams[0];
        //  var TSecond = typeParams[1];

        return tb;
    }
}

That 'helper' class from above is then inhejected into a standard builder pattern whch allows you to do something like this (and also seen in my other comment above)


    [Fact]
    public void ShouldNotBeRuntimeType_ReturnICollection()
    {
        var collectionCustomClassType = SmartTesturaTypeBuilder.Initialize()
            .AsCollection()
            .WithCollectionType("ICollection")
            .WithGenericTypeName("CustomClass")
            .ToGenerate()
            .Build();

        Assert.NotEqual("RuntimeType", collectionCustomClassType.Name);
        Assert.True(collectionCustomClassType.IsGenericType);
        Assert.True(collectionCustomClassType.ContainsGenericParameters);
    }

The real logic that you'll be interested in is SmartTesturaTypeHelper


Give me a few days to perform a proper pull-request and code share! or I can invite you to my Azure Devop's repo, your choice!!


Sorry for long posts, I have been working with the code alot lately, you don't have to reply to everything <3

jeffward01 avatar Jan 14 '22 07:01 jeffward01

Thanks for getting back to me so quickly, I have looked at your code for a long time, and compared it to many other 'code generators' out there. your Syntax is the most verbose while also providing 'easy' configuration. I mean 'easy', because you still have control to do any-thing you wan programmatically (contrary to this one which uses string), and it is more lucid than this similar library for example.

Personally I dislike the CsCodeGenerator library because it takes the 'Service' approach which means you have many classes floating around that all need to work together, while Testura.Code is tight in a single 'macro' Fluent Interface Builder.

Personally i love code-generation because if you want to build a new API, a new project, a new app, you have so much boiler-plate. Then you say "I will use Templates! Hello Handlebars.NET - and its cool, it works... But then you want to make a tiny tiny change, you must manually edit all the template files, and it becomes a pain in the butt.

Sorry for rant - but this is the reasons why both Code Generation and Testura.Code are so cool!

No problem with the ranting, it's really cool to see other projects and how they do it! I haven't looked on a lot of other code generation projects so maybe I can find some insperation (but sounds like they work pretty different from this one so maybe hard).

I have seen that you are more active on this! I discovered your code maybe 9 months ago and it was rather inactive (complete - but not recent commits which is fine <3 ) - I was very happy to see that your back!!

Thanks and fun to work on it again. It helps when others find it useful too so a big thanks for your issues, feedback and requests as it really helps me getting started again.

What interests you about code-generation? I saw you have some gaming repositories, are you lazy like me too and don't like writing boiler-plate code?

Everything started when I worked on a proof of concept test tool for work. It was suppose to work by loading a dll/framework into the tool and then you could drag and drop methods to create tests (which I generated with this framework). It was a pretty cool idea but sadly we didn't work on it for very long.

But I started to like code generation after that and then I have been using it for multiple different things (page object generator tool, unit testing generators, etc). I also use it to generate some boiler-plate code as you mention (for example we like to use builds in our current work project so I created a VS extension that makes you right click on a class and then just select "generate builder" that are generated with this framework).

But I guess the sky is the limit for code generation and that's what makes it fun.

I checked out your example and I really like how clean it seems to be and how you built-in the saving/compiling to a single flow. So would be nice if we could find a way to get it into the framework. But I'm gonna look more at the code you posted tomorrow (a bit late here now ;-)).

Give me a few days to perform a proper pull-request and code share! or I can invite you to my Azure Devop's repo, your choice!!

Both works for me and no worry if it takes some time for you to clean-up. I rather see that we take our time and makes it fit nicely than we rush it.

MilleBo avatar Jan 15 '22 22:01 MilleBo

@MilleBo Hello!!! Sorry for the late reply, I will reply to all of your points above in the morning!

I just wanted to let you know that I am working on cleaning up the fluent interfaces and 'TypeBuilder enhancements (Generics Parameters)'.

I spent a while today cleaning up the code! I will have it ready to submit soon so that you can review it and we can make changes as you wish!

I will do my best to eliminate 3rd party libraries, right now the only two third party libraries I use are these:

Both of these will be very easy to remove!

I just wanted to let you know I am actively working on this and eager for you to check it out!

Talk to you soon and i'll reply to your comments in the AM!

jeffward01 avatar Jan 18 '22 10:01 jeffward01

Great to hear that and I'm excited to see what you come up with.

MilleBo avatar Jan 24 '22 16:01 MilleBo

Hello @MilleBo! I hope you are well!

I just wanted to write to you and let you know that I have not forgotten about this code! I use it all the time <3

I have some contributions I'd like to add around 'Type' Generation and fluent interfaces as I talked about. The code is done, I just have to 'groom' the code:

  • Remove it from my libraries
  • Clone your project
  • Insert my code into Testura.Code
  • Clean and test it
  • Document it
  • Add Pull Request

I just want to thank you again for this awesome library!!! I am excited to add my contributions! Thank you for your patience

jeffward01 avatar Sep 27 '22 18:09 jeffward01

I also wanted to thank you for using a .editorconfig its very nice to work with

jeffward01 avatar Sep 28 '22 03:09 jeffward01