How do I invoke (and cast) an introduced generically typed field?

Bear with me - there's a lot going on here. Also Discord is funky about styling my code, so I'm handwriting it (pardon any typos). At a high level, let me explain where I'm trying to go and then I'll get into the issue. I want to attach an attribute to a target type that defines a type (TEntity) and another type (TEntityId) so I can use these to introduce some advice later on. I can retrieve those no problem - each is saved as an INamedType on the BuildAspect method. I then want to iterate through most of the properties of the TEntity type itself and introduce at least one field for several of them, but the type of the field depends on the type of the property itself. Again, this is set up and works fine. I save all the references to the introductions in a dictionary keyed by a reproducible mechanism to link the properties with the introduced types. Again, this works fine. My target type implements a base type which has a bunch of event callbacks in place. What I want to do is replace the last methods in those event callback chains with a method that can do operations against some or all of these introduced fields and this is where I'm having some difficulty. I can pass along the produced IIntroductionAdviceResult<IField> via tags, but my problem is that I can't figure out how to assign the appropriate type to it. Now, typically I'd do something like this:
[Template]
private void DoSomething<[CompileTime]TMyType>(IMethod myMethod)
{
var thisIsMyMethod = (TMyType) myMethod.Invoke()
//Use it as though it's cast as a TMyType now
//...
}
[Template]
private void DoSomething<[CompileTime]TMyType>(IMethod myMethod)
{
var thisIsMyMethod = (TMyType) myMethod.Invoke()
//Use it as though it's cast as a TMyType now
//...
}
But the issue here is that the type is only known by the field itself and only as part of this compilation pipeline. As a result, I cannot do the following (or can I?):
[Template]
internal void RebuildNotificationAsyncCallback(
IIntroductionAdviceResult<IFIeld> adviceField
)
{
//I can get the type of the field off the type declaration itself...
var fieldType = adviceField.Declaration.Type; //...but then I cannot then use this with an 'as' or 'is' nor can I cast with a (fieldType)whatever.Value;
//So when I then get my value for it, it's stuck cast as a `dynamic?` and it's unclear how to proceed
var myField = adviceField.Declaration.Value; //Type = dynamic?
}
[Template]
internal void RebuildNotificationAsyncCallback(
IIntroductionAdviceResult<IFIeld> adviceField
)
{
//I can get the type of the field off the type declaration itself...
var fieldType = adviceField.Declaration.Type; //...but then I cannot then use this with an 'as' or 'is' nor can I cast with a (fieldType)whatever.Value;
//So when I then get my value for it, it's stuck cast as a `dynamic?` and it's unclear how to proceed
var myField = adviceField.Declaration.Value; //Type = dynamic?
}
I could try using meta since this is a template, but it's a similar problem - the only thing that knows what type it is is the method advice itself and I can't cast with that. Is there a helper method I'm missing somewhere to allow invocation of arbitrary typed introduced fields? Or how else might I go about using the adviceField in this template? Thank you!
59 Replies
Gael Fraiteur
Gael Fraiteur•2y ago
I'm not sure I understand the whole dissertation, but in var myField = adviceField.Declaration.Value, myField will be of type adviceField.Declaration.Type. It's dynamic only in the template, but the template compiler replaces this by the actual run-time type. And there is always meta.Cast if you need it, but in this case it seems redundant.
Whit
Whit•2y ago
A follow-up question for you: Is there a cast-like method somewhere which I can use in a [Template] that allows me to specify a dynamic? value and an INamedType or an IType that will yield an instance that's typed to that at compile time? Reason being, it's insufficient for me to simply pass, say, a field into an introduced method because I usually need to pass it into something else as an argument. It's great that at compile-time, getting adviceField.Declaration.Value will type the field correctly, but it's not going to like it if, at design-time, I attempt to pass a dynamic into something expecting a more specific type. Rather, since I know what type it's going to be (per the IType from adviceField.Declaration.Type), it'd be great if there were even just some syntactic sugar I could use that is ignored at compile time but just allows me to use the type as it will eventually be here at dev-time. Some additional background on where I'm coming from: In my use case, I'm introducing a pile of fields to a class and I'm using the TypeFactory to build their types dynamically based on the Type constructor arguments of another attribute attached to the class. I want to introduce additional methods that can perform operations against all (or some) of these fields, but operate against all the fields at one time. Individually, I can get this to work:
[Template]
private void ClearData<
[CompileTime] TDataKey,
[CompileTime] TDataValue>
(DataType dataType, IIntroductionAdviceResult<IField> fieldAdvice)
where TDataKey : IComparable, IComparable<TDataKey>
where TDataValue : IComparable, IComparable<TDataValue>, IEquatable<TDataValue>
{
if (dataType == DataType.Dictionary)
{
var data = (Dictionary<TDataKey, IEnumerable<TDataValue>>)fieldAdvice.Declaration.Value;
var dataOperator = new DataOperator<TDataKey, TDataValue>(ref data);
dataOperator.Clear();
}
}
[Template]
private void ClearData<
[CompileTime] TDataKey,
[CompileTime] TDataValue>
(DataType dataType, IIntroductionAdviceResult<IField> fieldAdvice)
where TDataKey : IComparable, IComparable<TDataKey>
where TDataValue : IComparable, IComparable<TDataValue>, IEquatable<TDataValue>
{
if (dataType == DataType.Dictionary)
{
var data = (Dictionary<TDataKey, IEnumerable<TDataValue>>)fieldAdvice.Declaration.Value;
var dataOperator = new DataOperator<TDataKey, TDataValue>(ref data);
dataOperator.Clear();
}
}
and..
[Template]
private void ClearAllData([CompileTime] List<IIntroductionAdviceResult<IMethod> introducedClearMethods)
{
foreach (var method in introducedClearMethods)
{
method.Declaration.Invoke();
}
}
[Template]
private void ClearAllData([CompileTime] List<IIntroductionAdviceResult<IMethod> introducedClearMethods)
{
foreach (var method in introducedClearMethods)
{
method.Declaration.Invoke();
}
}
Then I can loop through all the introduced fields and introduce a ClearData method for each field (ensuring they have unique names via the IMethodBuilder), save those introduced advices and pass them into an introduced method for the second template where it invokes each of them. The fields themselves get strongly typed at design-time because I can infer their type based on the enum value, so I can pass them around to other methods and instances freely, then invoke all of them in the ClearAllData method. The downside here is that I wind up with a bunch of private methods that should only be exposed to the respective *AllData methods - rather I'd like to reference each of the fields into a single introduced ClearAllData method, but that's where question #1 comes in.. Each field has a different TDataKey type associated with it. meaning that unlike my first template above, I cannot simply pass in the generic types to the template because they differ on a per-field basis. I'd like to just pass the list of all the IIntroducedAdviceResult<IField> into the method, iterate through the fields, cast each to their compile-time types (but at design-time so I can pass it into various arguments) and then invoke the Clear method on each of their DataOperator instances, but I cannot do that without some means of casting out from dynamic? to their respective compile-time types. Thanks!
Petr Onderka
Petr Onderka•2y ago
I don't think there is anything in C# that would support that kind of generics. Are the private methods a problem because of IntelliSense? If so, would hiding them using [EditorBrowsable(EditorBrowsableState.Never)] help? Or, as an alternative, you can generate any code you want using IStatements. But that tends to require fairly ugly string manipulation.
Whit
Whit•2y ago
It's more that since I intend to mark the class as partial, I would prefer not to pollute the possible methods by having umpteen different methods (number of utility methods times the number of introduced properties) and would instead rather have only a single method for each action. Marking them with that attribute might work - can you point me to an example of how I might append that to an introduced method? IStatements are a possibility.. I'd have to figure out how to pass around the right types, but that might be a good route too. I'll give that a shot later today. Whew.. you weren't kidding. These statement blocks are wordy
var myType = ((INamedType) TypeFactory.GetType(typeof(Dictionary<,>))).WithTypeArguments(entityPropertyType,
entityIdType);
var myType = ((INamedType) TypeFactory.GetType(typeof(Dictionary<,>))).WithTypeArguments(entityPropertyType,
entityIdType);
See - something like that is so neat that I can trivially construct a type with generic type arguments using variables. There are so many places I can drop an INamedType or an IType into to introduce things that it's just such a pity that there's not a mechanism to take a dynamic and type it at design time to whatever sort of IType or INamedType I want so I might just keep on using it as the type I know it to be introduced as, even if it's just some sort of sugar that tricks the IDE locally (since it would all type out correctly at compile time once the dynamics were subbed out) As an aside - if I apply an Indent in the statement builder and then insert a new line, do I have to re-indent or does it maintain the indented level between newlines until I unindent?
Gael Fraiteur
Gael Fraiteur•2y ago
Well T# is essentially duck typing so you can always use an interface that has the members you want. The target type does not even need to implement the interface. It's voiding the warranty of course but since there's no warranty 😉 I mean here you can use IDictionary
Petr Onderka
Petr Onderka•2y ago
Actually, marking them with EditorBrowsable won't help, because that only works for members in other assemblies.
Whit
Whit•2y ago
I fear I've done a poor job of explaining the issue I'm experiencing and why I'm having the problem I am here: I'm trying to build an aspect that will automatically build and maintain secondary indices for any specified type. I have an attribute attached to the target type with two Types provided: one that identifies a specific model (which I intend to build the indexes for) and another that clarifies what the identifier property type is in that model. The target type implements a base class with all the functionality and I'm looking to augment the methods that handle the end result of event chains with all these cross-index updates. I iterate through the properties on the specified type and, for those types which I intend to support a secondary index for, I then introduce a field creating a new instance of whatever that index type is (again, varies by property type). As such, each of the introduced fields is a different base type with at least one (sometimes two) generic constraints: the type of the property of the model to serve as the key in the secondary index and the type of the identifier (passed in via the attribute). At the moment I have three different kinds of secondary indexes - a dictionary is just the easiest one to use in these demos. Up to this point, things work fine. Given a Vehicle with a boolean value for the property "IsEnabled", a field is created of Dictionary<bool, List<Guid>> on my target type:
[BuildIndexes]
[Index(typeof(Vehicle), typeof(Guid))]
internal sealed class MyStore
{
private Dictionary<bool, List<Guid>> Index_I_IsEnabled = new global::System.Collections.Generic.Dictionary<global::System.Boolean, global::System.Collections.Generic.List<global::System.Guid>>();
}
[BuildIndexes]
[Index(typeof(Vehicle), typeof(Guid))]
internal sealed class MyStore
{
private Dictionary<bool, List<Guid>> Index_I_IsEnabled = new global::System.Collections.Generic.Dictionary<global::System.Boolean, global::System.Collections.Generic.List<global::System.Guid>>();
}
The issue comes in passing multiple fields to an introduced method. If I pass a single field, I can pass it to something like my ClearData method in the example above and it produces something like the following (names updated):
private void ClearSecondaryIndex_Index_I_IsEnabled()
{
var invertedIndex = (Dictionary<bool, IEnumerable<Guid>>)this.Index_I_IsEnabled;
var invertedOperator = new InvertedSecondaryIndexOperator<bool, Guid>(ref invertedIndex);
invertedOperator.Clear();

}
private void ClearSecondaryIndex_Index_I_IsEnabled()
{
var invertedIndex = (Dictionary<bool, IEnumerable<Guid>>)this.Index_I_IsEnabled;
var invertedOperator = new InvertedSecondaryIndexOperator<bool, Guid>(ref invertedIndex);
invertedOperator.Clear();

}
That's great for a single field because I can pass the generic constraints over and pass them into the (Dictionary<TKeyEntityType, TKeyEntityIdType>) I'm doing to the field value, get that typed result and use it downstream in the InvertedSecondaryIndexOperator. What I cannot figure out how to do is pass multiple such fields into an introduced method to do the same thing. For example:
[Template]
private void ClearAllSecondaryIndex
(List<IndexType> indexTypes, List<IIntroductionAdviceResult<IField>> fields)
{
for (var a = 0; a < indexTypes.Count; a++)
{
var indexType = indexTypes[a];
var field = fields[a];

var fieldType = field.Declaration.Type;
var fieldValue = field.Declaration.Value; //Returns dynamic?

var typedFieldValue = field.Declaration.Value as typeof(field.Delcaration.Type.ToType()); //Can't do this as I can't use 'as' with a variable
var typedFieldValue = (field.Declaration.Type.ToType())field.Declaration.Value; //Same problem

//What I'd like to see..

var castFieldValue = TypeFactory.CastTo(field.Declaration.Value, field.Declaration.Type.ToType()); //Where `castFieldValue` is now whatever type will replace the dynamic at compile time, so I can use it right here right now
//or alternatively as a static extension method..
var castFieldValue = field.DeclarationValue.Value.To(field.DeclarationType); //Take an INamedType, IType or Type and return the value cast as that type
}
[Template]
private void ClearAllSecondaryIndex
(List<IndexType> indexTypes, List<IIntroductionAdviceResult<IField>> fields)
{
for (var a = 0; a < indexTypes.Count; a++)
{
var indexType = indexTypes[a];
var field = fields[a];

var fieldType = field.Declaration.Type;
var fieldValue = field.Declaration.Value; //Returns dynamic?

var typedFieldValue = field.Declaration.Value as typeof(field.Delcaration.Type.ToType()); //Can't do this as I can't use 'as' with a variable
var typedFieldValue = (field.Declaration.Type.ToType())field.Declaration.Value; //Same problem

//What I'd like to see..

var castFieldValue = TypeFactory.CastTo(field.Declaration.Value, field.Declaration.Type.ToType()); //Where `castFieldValue` is now whatever type will replace the dynamic at compile time, so I can use it right here right now
//or alternatively as a static extension method..
var castFieldValue = field.DeclarationValue.Value.To(field.DeclarationType); //Take an INamedType, IType or Type and return the value cast as that type
}
Is there anything at all like that last line possible? To your suggestion, I cannot cast it as a Dictionary<> and just use it because I don't know the generic constraints to apply and one cannot use variables in is, as or () casts that I'm aware of.
Gael Fraiteur
Gael Fraiteur•2y ago
The TypeFactory.CastTo thing you want is probably meta.Cast. There is also the extension method IExpression.CastTo so you can do field.CastTo(someOtherType).Value
Whit
Whit•2y ago
meta.Cast returns a dynamic - without casting to a constant type, I don't know that this moves the needle along much. But as every IField is an IExpression, perhaps I can use it this way alongside the ExpressionBuilder or StatementBuilders.. more experimentation needed
Petr Onderka
Petr Onderka•2y ago
Well, what do you want it to return? I can't be T, since the type of a variable can't change with every iteration.
Gael Fraiteur
Gael Fraiteur•2y ago
Note that it's dynamic only in your template code. In your generated code, it won't be dynamic but strongly and statically typed.
Whit
Whit•2y ago
I guess that's the issue I'm having here conceptualizing how to deal with this. I would like for it to return T, even if it's just syntactical sugaring here in the template code, so I can write the logic I intend to be copied through to the generated code - that's the point of a [Template] right? Or is the idea that if I write whatever methods I'm passing the value into, so long as it's compatible at generation time, it'll work because at least it'll be substituted in then?
Gael Fraiteur
Gael Fraiteur•2y ago
Yes that's it
Whit
Whit•2y ago
I just haven't done a whole lot with dynamic before - sorry for the slow learning curve on this one
Gael Fraiteur
Gael Fraiteur•2y ago
The template is mostly a syntax generator i.e. almost a text generator No problem, it's a totally innovative use of dynamic. Never seen before 🙂
Whit
Whit•2y ago
So really, if I'm just passing in an IField an an argument and trying to get it's value, if I know the type I can cast it as such. But if I don't know the type, I can just as easily just pass the value into something that would be expecting precisely that type and it'll fill in the blank down the road without the cast?
Gael Fraiteur
Gael Fraiteur•2y ago
And we need to learn how to improve the doc btw
Whit
Whit•2y ago
Because the blank down the road is the type I specified when I set up the IField?
Gael Fraiteur
Gael Fraiteur•2y ago
Yeah exactly. As long as the generated code compiles, you are fine.
Whit
Whit•2y ago
I have no idea where you'd start to improve the documentation on that, but if I develop any insight on that, I'll definitely reach out 🙂 It's a trippy topic
Gael Fraiteur
Gael Fraiteur•2y ago
It should be documented here, https://doc.metalama.net/conceptual/aspects/templates, but obviously it can be improved
Whit
Whit•2y ago
Ok, so say the downstream thing I want to pass the value into expects to be generically typed based on whatever it is. Is that where I should generally fall back to using an StatementBuilder or ExpressionBuilder so I can piece together the anticipated type (with its generic types) from what I'm able to know at compile-time?
Gael Fraiteur
Gael Fraiteur•2y ago
Not necessarily. If the field type is compatible with the method argument name so there's nothing special to do. But it would be easier if you write us the code that you want to be generated, it may be quicker than to try to explain in theory.
Whit
Whit•2y ago
What I'm trying to do is something like:
var fieldValue = field.Declaration.Value;
return new DictionaryOperator<TIndexKey, TIndexValue>(ref fieldValue);
var fieldValue = field.Declaration.Value;
return new DictionaryOperator<TIndexKey, TIndexValue>(ref fieldValue);
TIndexKey is whatever the field's own first generic type is and TIndexValue is a Type argument I'm passing into this method
Gael Fraiteur
Gael Fraiteur•2y ago
Yes for this you need to use ExpressionBuilder or StatementBuilder because we don't have an API to dynamically call constructors
Whit
Whit•2y ago
Now I could just go back to however I introduce the field and save the types I create it from and pass them in here, giving me that TIndexKey and the TIndexValue, then just pass in the fieldValue just like that. But I guess I'm trying to see if I can't just pick the generic type out of the field's type itself.
Gael Fraiteur
Gael Fraiteur•2y ago
Or you create a factory method for DictionaryOperator and you can use method invokers. But we don't have constructor invokers.
Whit
Whit•2y ago
Excellent. That at least clarifies that. One of these days I'll have this thing working end to end and have a marvelous example of a truly complicated aspect.
Whit
Whit•2y ago
Thankfully, I found that while taking my detour through making all these private methods with a single type and then calling them all with one unifying method.. but the number of methods that introduced on my test class was just sily The TypeFactory is a really well-done API coupled with INamedType - I haven't found a type I couldn't express yet with that And the idea is that when I craft a statement then, I'll execute it with meta.InsertStatement within a template?
Gael Fraiteur
Gael Fraiteur•2y ago
yes
Whit
Whit•2y ago
Excellent. I'll give this a whirl then and see how far it takes me. Thank you
Gael Fraiteur
Gael Fraiteur•2y ago
Good 🙂
Whit
Whit•2y ago
If I specify a type in the StatementBuilder.AppendType does Metalama automatically import it in the output class?
Gael Fraiteur
Gael Fraiteur•2y ago
It will fully qualify it. I'm not sure we're doing the import+simplify thing with StatementBuilder yet.
Whit
Whit•2y ago
Fully qualified is fine for my purposes
Gael Fraiteur
Gael Fraiteur•2y ago
It would not be hard to do. You can file a feature request 🙂
Whit
Whit•2y ago
😄 I'll get to that later today For now, I'm going to tackle this method that's been taunting me all weekend
Gael Fraiteur
Gael Fraiteur•2y ago
So tell us, how longdid it take to go from level 100 to 400 in Metalama? ;D
Whit
Whit•2y ago
Haha, day two? I confess, I jumped into Metalama with two very specific use cases in mind: Telemetry capture + method profiling for Application Insights and automatic generation and maintenance of sets of secondary indexes for arbitrary types. The first one took me a day or two to figure out the ropes. The second one has been eating my lunch until the dynamic bit clicked just a little while ago.
Gael Fraiteur
Gael Fraiteur•2y ago
Maybe we should add another example
Whit
Whit•2y ago
I've only introduced fields, automatic properties and methods so far and all three have been straightforward. I think the arbitrary key/value pairs via.. I think you call it the tags in the documentation.. is excellent - very versatile. The challenge has just been in taking all those artifacts and then finally tying them together because simple "pass in the field and write its name out to the console" I think the docs could use a lot of additional examples, even if just to really clarify how to use the API. The API documentation is helpful.. if you're familiar enough with the interfaces and how they work together. But I would personally enjoy more examples to climb that learning curve. As it is, I've got a silly number of test projects just trying out different ideas to see what's in the generated output.
Gael Fraiteur
Gael Fraiteur•2y ago
Examples as in https://doc.metalama.net/examples (implementation journeys) or like in https://doc.metalama.net/conceptual ?
Whit
Whit•2y ago
/examples is more end-to-end practical aspects. I think there's room for more toy examples in /conceptual
Gael Fraiteur
Gael Fraiteur•2y ago
The problem in /conceptual is not to rely on too many concepts
Whit
Whit•2y ago
Take the current puzzle I'm working on - I think there's only one example that actually references an introduced method and I think it's to demonstrate specifying the callback method for an event handler. I think that page could be lengthened to include an example like what I'm working on right now. Start with three fields introduced with different generic types. Pass the introduced fields into another introduced method that does some type-specific operation with each one (but within the same method, e.g. each produces a numeric output that you then sum and display). You could show this using the approach I started with (each field gets its own method and another method calls all these methods), then show a more optimized approach that relies on the dynamic nature to simply use them as-is relying on the generated code to be accurate, but using them all in a single method.
Gael Fraiteur
Gael Fraiteur•2y ago
How could we generalize your case into an example that would make sense at large?
Whit
Whit•2y ago
My own project is a little too elaborate for a conceptual demo, but perhaps something along those lines - it'd demonstrate some of the sheer potential for Metalama beyond just decorating method boundaries I'll think about it once I actually get this working 🙂 Ok, another question - how do I assign the field to a variable within a template, but then reference that variable within the statement builder? If I introduce as an expression, it's just going to evaluate the field value, right? But if I specify the variable name, how will Metalama know said variable isn't just compile-time?
Gael Fraiteur
Gael Fraiteur•2y ago
AppendExpression
Whit
Whit•2y ago
that doesn't just evaluate the expression at compile time?
Gael Fraiteur
Gael Fraiteur•2y ago
no run-time expressions are never evaluated at compile time, this is impossible
Whit
Whit•2y ago
Ah, that's right Oh, another great example to add to /conceptual - a demonstration of how to add an attribute via an IMethodBuilder
Gael Fraiteur
Gael Fraiteur•2y ago
where?
Whit
Whit•2y ago
I think I'd add a new page for it "Adding attributes" under "Advising code" (300 and split it up like in "Adding initializers" (assuming there's a non-programmatic way of doing it) I mean, I just figured it out because I correctly guessed that I could get an IConstructor from an INamedType, but I don't know that'd be an immediate leap for a new user
var attr = AttributeConstruction.Create(
indexAttr.Constructor,
new List<TypedConstant>
{
TypedConstant.Default(typeof(int)),
TypedConstant.Default(typeof(int))
},
new List<KeyValuePair<string, TypedConstant>>());
var attr = AttributeConstruction.Create(
indexAttr.Constructor,
new List<TypedConstant>
{
TypedConstant.Default(typeof(int)),
TypedConstant.Default(typeof(int))
},
new List<KeyValuePair<string, TypedConstant>>());
That takes some familiarity to piece together what it's talking about And then to know that you attach it on IMethodBuilder via b => b.AddAttribute() and not just specifying it in a list like another IType
Gael Fraiteur
Gael Fraiteur•2y ago
GitHub
postsharp Metalama Ideas · Discussions
Explore the GitHub Discussions forum for postsharp Metalama in the Ideas category.
Whit
Whit•2y ago
GitHub
Docs improvement: Add page describing how to add attributes · posts...
I might recommend this be placed in a new page under "Advising code" in the conceptual documentation and organized like the "Adding initializers" page (assuming there's a no...
Gael Fraiteur
Gael Fraiteur•2y ago
Thanks!
Whit
Whit•2y ago
GitHub
Simplify imports for types introduced via StatementBuilder · postsh...
Today, if I introduce a type via a StatementBuilder, it uses the fully-qualified namespace for it. Please instead import the type's namespace and opt to use the simpler type inline to improve r...
Gael Fraiteur
Gael Fraiteur•2y ago
Thanks
Want results from more Discord servers?
Add your server