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:
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?):
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
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.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:
and..
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!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 IStatement
s. But that tends to require fairly ugly string
manipulation.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
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?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
Actually, marking them with
EditorBrowsable
won't help, because that only works for members in other assemblies.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:
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):
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:
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.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
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
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.Note that it's
dynamic
only in your template code. In your generated code, it won't be dynamic but strongly and statically typed.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?
Yes that's it
I just haven't done a whole lot with
dynamic
before - sorry for the slow learning curve on this oneThe 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 🙂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?
And we need to learn how to improve the doc btw
Because the blank down the road is the type I specified when I set up the IField?
Yeah exactly. As long as the generated code compiles, you are fine.
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
It should be documented here, https://doc.metalama.net/conceptual/aspects/templates, but obviously it can be improved
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?
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.
What I'm trying to do is something like:
TIndexKey is whatever the field's own first generic type is and TIndexValue is a Type argument I'm passing into this method
Yes for this you need to use ExpressionBuilder or StatementBuilder because we don't have an API to dynamically call constructors
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.
Or you create a factory method for DictionaryOperator and you can use method invokers. But we don't have constructor invokers.
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.
Heh
For methods it's IMethod.Invoke (https://doc.metalama.net/api/metalama_framework_validation_validatordelegate-1_invoke#Metalama_Framework_Validation_ValidatorDelegate_1_Invoke__0__)
And you can use for instance WithTypeArguments (https://doc.metalama.net/api/metalama_framework_code_genericextensions_withtypearguments#Metalama_Framework_Code_GenericExtensions_WithTypeArguments_Metalama_Framework_Code_IMethod_System_Type___System_Type___)
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?yes
Excellent. I'll give this a whirl then and see how far it takes me.
Thank you
Good 🙂
If I specify a type in the StatementBuilder.AppendType does Metalama automatically import it in the output class?
It will fully qualify it. I'm not sure we're doing the import+simplify thing with StatementBuilder yet.
Fully qualified is fine for my purposes
It would not be hard to do. You can file a feature request 🙂
😄 I'll get to that later today
For now, I'm going to tackle this method that's been taunting me all weekend
So tell us, how longdid it take to go from level 100 to 400 in Metalama? ;D
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.
Maybe we should add another example
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.
Examples as in https://doc.metalama.net/examples (implementation journeys) or like in https://doc.metalama.net/conceptual ?
/examples is more end-to-end practical aspects. I think there's room for more toy examples in /conceptual
The problem in /conceptual is not to rely on too many concepts
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.
How could we generalize your case into an example that would make sense at large?
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?
AppendExpression
that doesn't just evaluate the expression at compile time?
no run-time expressions are never evaluated at compile time, this is impossible
Ah, that's right
Oh, another great example to add to /conceptual - a demonstration of how to add an attribute via an IMethodBuilder
where?
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
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
Could you copy-paste this to https://github.com/postsharp/Metalama/discussions/categories/ideas?
GitHub
postsharp Metalama Ideas · Discussions
Explore the GitHub Discussions forum for postsharp Metalama in the Ideas category.
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...
Thanks!
Added feature request: https://github.com/postsharp/Metalama/discussions/141
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...
Thanks