Closed ignatandrei closed 3 years ago
Could you give an example where to put template text for each member? Following approach does not seem right because there are two methods Print1
and Print2
. Polluting interface also does not look like right approach??
public interface IPrintable
{
void Print1();
void Print2();
}
public partial class Person : IPrintable
{
[BeaKona.AutoInterface(Body = "bla bla", BodyLanguage = "scriban")]
private readonly IPrintable aspect1 = new PersonPrinterV1();
}
A template will look like this
{ for method in ,,,} public {{interfaceName}} . {{method.Name}} ({{ method.Arguments }} ) { return this.{{method.Name}} ({{ method.Arguments }} ) } {end for}
Thanks! I was closed-minded and also had an error in logic. I could not see meaningfulness because body would be full of 'if-s' for each case defying it's purpose.
There are four kind of templates; for methods
, properties
, indexers
and events
.
What if we add support for multiple Attribute
? First Attribute
can customize some set of methods, other can customize all properties etc.. All other cases will use fallback C# source text generation (current implementation). Here is example:
public partial class Person : ITest
{
[BeaKona.AutoInterface]
[BeaKona.AutoInterfaceTemplate(Target=Targets.Method, Filter="MyMethod\d+" Language="scriban" Code=" body1 ")]
[BeaKona.AutoInterfaceTemplate(Target=Targets.Property | Targets.Indexer, Language="scriban" Code=" body2 ")]
private readonly ITest aspect1 = new Test();
//[BeaKona.AutoInterface]
//[BeaKona.AutoInterfaceTemplate(Target=Targets.Method, Code=" body3 ")]
//private readonly ITest aspect2 = new Test();
}
Corner cases that I am aware of:
AutoInterfaceTemplate
entries for target-interface ITest
.Here is 'templated' version of current/fallback generator for methods
:
{{~for method in interface.methods~}}
{{method.IsAsync?"async":""}} {{method.ReturnType}} {{interface.Name}}.{{method.Name}}({{method.Arguments}})
{
{{~if method.IsAsync~}}
{{~for element in elements~}}
var temp{{for.index}} = (({{interface.Name}})this.{element.Name}).{{method.Name}}({{method.Arguments}});
{{~endfor~}}
{{~for element in elements~}}
{{for.last&&method.ReturnExpected?"return":""}} await temp{{for.index}};
{{~endfor~}}
{{~else~}}
{{~for element in elements~}}
{{for.last&&method.ReturnExpected?"return":""}} (({{interface.Name}})this.{element.Name}).{{method.Name}}({{method.Arguments}});
{{~endfor~}}
{{~end~}}
}
{{~endfor~}}
What do you think?
Also, what about security? Someone can create public project using this source generator and write ?malicious? template. Template will be executed during compile time (interpreted string).
I think it will be great to have just one template instead of 4
Unfortunately, there are multiple topics in this thread. As I understood it is desirable to have one monolith template that specify all four sections: methods
, properties
, indexers
and events
?
If you 'miss' one section then code for that section will not be generated. For example, this interface may require code generation for methods
and events
:
interface ITest
{
void MyMethod();
event EventHandler Receive;
}
Notice how code for events
are different than one for methods
. I was proposing solution for gaining configurability yet not concerning for noisy parts. Here is full version of scriban template that should be pasted to every template in order to get default implementation: (it was never tried; treat it as pseudo example)
{{~for method in interface.methods~}}
{{method.IsAsync?"async":""}} {{method.ReturnType}} {{interface.Name}}.{{method.Name}}({{method.Arguments}})
{
{{~if method.IsAsync~}}
{{~for element in elements~}}
var temp{{for.index}} = (({{interface.Name}})this.{{element.Name}}).{{method.Name}}({{method.Arguments}});
{{~endfor~}}
{{~for element in elements~}}
{{for.last&&method.ReturnExpected?"return":""}} await temp{{for.index}};
{{~endfor~}}
{{~else~}}
{{~for element in elements~}}
{{for.last&&method.ReturnExpected?"return":""}} (({{interface.Name}})this.{{element.Name}}).{{method.Name}}({{method.Arguments}});
{{~endfor~}}
{{~end~}}
}
{{~endfor~}}
{{~for property in interface.properties~}}
{{property.Type}} {{interface.Name}}.{{property.Name}}
{
get
{
return (({{interface.Name}})this.{{elements[0].Name}}).{{property.Name}};
}
set
{
{{~for element in elements~}}
(({{interface.Name}})this.{{element.Name}}).{{property.Name}} = value;
{{~endfor~}}
}
}
{{~endfor~}}
{{~for indexer in interface.indexers~}}
{{indexer.Type}} {{interface.Name}} this[{{indexer.Arguments}}]
{
get
{
return (({{interface.Name}})this.{{elements[0].Name}})[{{method.Arguments}}];
}
set
{
{{~for element in elements~}}
(({{interface.Name}})this.{{element.Name}})[{{method.Arguments}}] = value;
{{~endfor~}}
}
}
{{~endfor~}}
{{~for event in interface.events~}}
{{event.Type}} {{interface.Name}}.{{event.Name}}
{
add
{
{{~for element in elements~}}
(({{interface.Name}})this.{{element.Name}}).{{event.Name}} += value;
{{~endfor~}}
}
remove
{
{{~for element in elements~}}
(({{interface.Name}})this.{{element.Name}}).{{event.Name}} -= value;
{{~endfor~}}
}
}
{{~endfor~}}
it is ok. Just have this template as a starting point and each developer can modify his. It is better rather than have 4 differents templates to look at.
So, when this will be ready ? ;-)
I've made first version. See if it meets your expectations/requirements..
trying
Did you punlish on Nuget ? ;-)
I was hoping you will download github repository directly..
Now I made nuget push but not sure whether nuget path internals are valid. I am writing this because source generator have to have reference to foreign libraries (scriban) that will be loaded/dependent at runtime (your compiletime), but not at execution (your runtime). I hope I set paths right...
1.0.9-preview is now listed as nuget preview package
It works . Thanks!
https://github.com/ignatandrei/RSCG_Examples/tree/main/DP_Decorator
I will also blog about it. Thanks ! ( and maybe put a 1.0.10? ;-) )
Ok, in next few minutes I will publish 1.0.10.. Thanks!
I've visited your blog. It's great and useful! I have also looked at your DP_Decorator solution. Here is outline of version that I was proposing:
public partial class MilkDecorator : ICoffee
{
[BeaKona.AutoInterface]
[BeaKona.AutoInterfaceTemplate(Target=Targets.Property, Filter="Description", Language="scriban", Code=DescriptionTemplate)]
[BeaKona.AutoInterfaceTemplate(Target=Targets.Property, Filter="Price", Language="scriban", Code=PriceTemplate)]
private readonly ICoffee coffee;
private const string DescriptionTemplate = @"
var name = this.GetType().Name.Replace(""Decorator"","""");
return {{expression}} + "" with "" + name;
";
private const string PriceTemplate = @"return {{expression}} + this.DecoratorPrice;";
public int DecoratorPrice { get; set; } = 1;
public MilkDecorator(ICoffee coffee)
{
this.coffee = coffee;
}
}
Yes, it is much simpler . I will put as a alternate solution ( and thanks for the kind words)
Also, here are SourceGenerator features Access Analyzer Config properties and Consume MSBuild properties and metadata that somehow may be used.
Yes. The approach with a template file is much easy. I have an example with https://github.com/ignatandrei/AOP_With_Roslyn/tree/master/SkinnyControllers or https://github.com/ignatandrei/AOP_With_Roslyn/tree/master/AOPMethods
Thanks. I will extend current version to take named parameter TemplateFileName that will search 'AdditionalFiles' and 'Assembly's ManifestResourceStream' (as fallback) for a template body.
Version 1.0.11 can load template from analyzer additional file and also we have partial templates.
Your pattern is great for a decorator pattern. Could you add templating ( Scriban ? ) to customize how the code is generated ?