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•16mo 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•16mo 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•16mo 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•16mo 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•16mo 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•16mo ago
Actually, marking them with EditorBrowsable won't help, because that only works for members in other assemblies.
Whit
Whit•16mo 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•16mo 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•16mo 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•16mo 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•16mo 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•16mo 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•16mo ago
Yes that's it
Whit
Whit•16mo 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•16mo 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 🙂
Want results from more Discord servers?
Add your server