-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: inverse [DynamicDependency] attribute for generated code trimming scenarios #50333
Comments
cc: @eerhardt |
An alternative thought would be to add a [assembly: DynamicDependency("ValidateAllProperties", typeof(__ObservableValidatorExtensions), Target = "ObservableValidator.ValidateAllProperties()")] This would state that the One issue with the original proposal is that the trimmer would have to look through all the code to find these "inversed DynamicDependency" attributes. But adding an @vitek-karas any thoughts here? |
Hi @eerhardt, thanks for taking a look! Just to clarify, in the proposed |
You're right that we don't have an attribute to do this today. There is a way to do the "target" behavior via linker attributes XML but it's not very user friendly (and I would recommend to NOT go that route). All that said I don't think there's a problem here though. The trimming happens on the entire application. The trimmer tool assumes that it sees all of the code in the application (and it's highly unlikely we will come up with a version which doesn't make this assumption anytime soon). As such the trimmer needs to see the source generated code as well. Once it does that all of the dependencies of the generated code will be automatically analyzed and preserved - just like any other code in the app. Given the code samples above I think the one missing piece is how the library gets to the source generated type. The reflection which looks for a specific namespece/type name in basically "all" assemblies is something trimming will have hard time dealing with. Ideally there would be no reflection in the code paths used by source generators. Reflection is obviously problematic for trimming, but it's also problematic for AOT (good AOT solutions try to avoid keeping metadata to save space and perf, which makes reflection hard). I don't know what other teams decided to do to solve this problem (runtime discoverability of source generated code). One approach I've seen used in the past is module constructors. Now C# can generate module initializers your source generator can generate a module init method which registers all of the helpers it generated - typically into a static dictionary of the library the helpers are for. At runtime there's no need for reflection then, just go over the dictionary if it contains a helper for the case at hand. (You would also be able to get rid of the delegate type creation and This would also solve the trimmer issue - module constructors are always preserved if the owning module is preserved, so all the code they refer to will also be preserved (in this case the generated helpers). Side note: The attribute as proposed has another problem which is that it applies to "all" assemblies (find me my friend in all assemblies) - trimmer really hates this - as it potentially greatly limits what it can do. If an application refers to assembly |
This pattern is not linker friendly. It keeps the helpers around even when the code that they are supporting is trimmed. |
Posting a small update on this issue about some recent changes I've pushed to the MVVM Toolkit v2, in case it's in any way relevant to the proposal. I've been talking with @GrabYourPitchforks about my current setup here, and following his suggestions I've reworked the code to avoid those Here's what the updated generator produces for a given user type (let's call it Source-generated code (click to expand):namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals
{
internal static partial class __ObservableValidatorExtensions
{
public static Action<object> CreateAllPropertiesValidator(PersonViewModel _)
{
static void ValidateAllProperties(object obj)
{
var instance = (PersonViewModel)obj;
__ObservableValidatorHelper.ValidateProperty(instance, instance.Name, nameof(instance.Name));
__ObservableValidatorHelper.ValidateProperty(instance, instance.Age, nameof(instance.Age));
}
return ValidateAllProperties;
}
}
}
And then the MVVM Toolkit does the following lookup at runtime, for the first invocation: MVVM Toolkit library code (click to expand):protected void ValidateAllProperties()
{
static Action<object> GetValidationAction(Type type)
{
if (type.Assembly.GetType("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType &&
extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo)
{
return (Action<object>)methodInfo.Invoke(null, new object?[] { null })!;
}
return GetValidationActionFallback(type);
}
EntityValidatorMap.GetValue(GetType(), static t => GetValidationAction(t))(this);
} So this change made the usage safer and also easier to manage, which is already a very welcome improvement 😄 I still have the issue of letting the trimmer know not to removing these methods though, since they're only referenced via reflection by the library. Currently I haven't added a static mapping of sorts since @jkotas mentioned that's not a linker-friendly solution. Looking forwards to hearing whether this proposal makes sense then and/or what a good solution for this issue will be 🙂 |
Maybe there's a way to do this without new features (not trying to avoid adding something which makes sense, just exploring options): I assume the source generator runs for some class defined in the app (typically) which derives from |
Ahah of course, if there's a workaround that works just the same and without requiring new features I'd be just as happy 😄
Unfortunately that's not possible (as far as I know). You'd have to either just have your source generator ignore that, and just end up with a build error due to duplicate type definitions, forcing the user to then mark their class as I can see how this workaround would effectively work with trimming, I mostly just have these reservations about it:
Also while we're at it I also have a more general question: regardless of this specific scenario I have in the MVVM Toolkit, do you feel that such a proposed feature (ie. ability to inform the trimmer of "backward" dynamic dependencies via an attribute) would generally be useful, especially considering source generators being more widespread in the future? As in, I was curious to know whether the team thought this proposal had merit on its own, not just to solve this specific issue I used as an example 😄 |
You may want to look at how this problem was solved for Json source generator: #45448. It has the same problem: The generator produces methods associated with types and it needs to look them up efficiently at runtime. |
Re methods showing up in intellisense: Re Re if this is useful:
@eerhardt proposal would probably fit all these. I'm a bit hesitant because I think that this is a design flaw in the source generators. Currently they don't seem to allow any way to "attach" the generated code to the code it belongs to using existing IL semantics (direct call, method on the same type, add an attribute to existing method, ...). So this attribute would basically try to workaround that limitation - just like the reflection calls at runtime are a workaround for that same problem. It's kind of weird that source generators which were partially designed to avoid reflection, force usage of reflection (and consequently cause trouble to trimming, AOT and so on). |
Source generators can only add new code, they can't change existing code. I only see 3 high-level options to get the new generated code included in your application:
Currently, both the JSON (#45448) and Logging (#36022) source generators take approach (1) above. |
Correct me if I'm wrong, from what I can see you then basically get the following drawbacks for each approach:
So it's mostly about choosing which of these 3 patterns to go for, and accept the drawbacks it will involve? As far as I can tell, among these 3 possible approaches, option 3 with the proposed attribute seems to be the only one that could be effectively fixed to result in a solution that has (effectively) no drawbacks in general? Because (1) will always require explicit action by consumers (ie. directly referencing some generated member), and option (2) is inherently not trimmer-friendly, whereas (3) could effectively be both transparent to consumers, as well as being trimmer-friendly with the right annotations. Again please correct me if I'm wrong, I'm just trying to properly understand all this and wrap my head around it 😄 |
The drawback of solution 3 is that it uses (relatively expensive) reflection at runtime. |
Right, I should've probably clarified that (at least in my case) I'm just assuming that option (3) would go together with some sort of caching, so that you'd amortize the reflection cost over multiple executions. What I'm doing in the MVVM Toolkit is to just cache the retrieved delegate in a I can cetainly agree that in cases where caching isn't an option and/or in cases where the generated code is meant to be used in a hot path (or in some path that is called more often, etc.), then doing lookup with reflection all the time might not be ideal 🙂 |
Even if you cache, you pay for the reflection in startup time and binary footprint of trimmed apps. For example, reducing startup time and size of trimmed apps was the priority for the JSON serializer source generator (#45448). |
I guess I don't see the difference between this and any of the proposed solutions to "3. Use reflection to scan for it". All the proposed solutions to telling the trimmer to keep the generated code can result in preserving code that's not actually used. |
@jkotas Oh, absolutely. I'm sorry if that came across the wrong way, I wasn't trying to say that approach (3) is just better than (1) nor that there's no valid use cases for (1), for sure. I can understand how that's the best solution for something like the JSON serializer and that it's overall a better solution with respect to things such as startup time, of course 🙂 My thinking here was that eg. with respect to the example use case in the MVVM Toolkit, where such a generated method would not be called at startup (you wouldn't try to validate a user form when opening an app), for me having the consumer experience be transparent (ie. not requiring users to alter their code just to opt-in into the faster code) is much more of a priority. As in, overall it's just a different use case scenario than something like those JSON APIs, from what I can see. Ideally I'd just want users to upgrade to the MVVM Toolkit v2 with the source generator, and just magically get faster code without requiring them to do anything.
@eerhardt Maybe I misunderstood here, I thought Jan meant to say that if the generated methods were annotated via an attribute, the linker could still technically remove them entirely if it detected that the code from the library looking them up with reflection was never used, whereas with the module initializer solution, they'd always be preserved no matter what. If I got that part wrong or if I'm missing something here, then please let me know 😄 |
There are other scenarios where unused code code would get kept unnecessarily. For example, if I had 2 classes |
Unfortunately, source generators do not have the right features to make the update seamless and transparent for existing patterns. The lean way to do transparent solutions like what you are looking for is by combining source generators to generate the bulk of the code and then combine them with something like Fody to hookup the generated code. |
Replying to @eerhardt:
Right, yeah I can see how only deciding what to keep based on the type can be limiting. I would say that given that the validation code is only generated if you specifically inherit from Replying to @jkotas: I see. Fody and IL weaving in general is really something we'd like to avoid in this case though 😥
As an example for the previous point, consider this case: public class ValidatorA : ObservableValidator
{
public void Foo()
{
ValidateAllProperties(ValidationContext.Default.ValidatorA);
}
}
public class ValidatorB : ValidatorA
{
}
new ValidatorB().Foo(); // Incorrect context is used 💣 Whereas the current approach (both in the shipped release of the MVVM Toolkit and in vNext with source generators) would have no problems with scenarios like this, as To summarize, the situation is as follows. Consider a user of the MVVM Toolkit defining a type such as: public class A : ObservableValidator { } The source generator will then create the following validation method: public static Action<object> CreateAllPropertiesValidator(A _) => throw null; So from what I can see we basically need a way to express the following for each such generated method:
...Which I guess just can't be fully determined statically by the trimmer though? 😟 |
That is the core of the problem - we want to come up with a design which would be statically analyzable. For example in System.Text.Json we would ideally want to be in a place that the trimmer can statically determine that all use cases have the source generated code available and thus the trimmer can remove the reflection based serialization from System.Text.Json (to reduce size). In order to do this, the callchain which leads to the serialization (or the usage in the general case) must be statically analyzable. My current thinking (very rough):
(This is very high-level and very hand wavy, but it feels like going in the right direction) Granted that this might not solve the validation issue you describe above - mainly because that API is not strongly typed in any way and the trimmer has very limited ways to statically analyze it. That said - we recently introduced a new mechanism to allow DAM on types (applies to all derived types in a way) - dotnet/linker#1929 - maybe that could be combined with the above to get it working. |
Thank you for the proposed solution idea @vitek-karas, I'm glad to see that at least there's some interest in solving this 😄
This bit in particular makes sense and it seems to be in line to what had been suggested before as well, with a "glue" attribute to externally link two separate methods potentially from different assemblies, and placed at the assembly level to make life easier for the linker that wouldn't have to crawl every single type and method to discover these attributes.
I don't think I'm entirely clear on why that wouldn't be possible for the requested solution. I mean I'm sure there's a reason but I'm struggling to fully understand it, so I'd appreciate if anyone could take a minute to explain that 🙂 To given an example, consider this scenario, which is basically equivalent to what I'm proposing: class A
{
public virtual void Foo() { }
}
class B : A
{
public override void Foo() { }
} Now, even though I think this is the main point that I'm struggling to understand - the way I see it this would at least in theory be doable? |
Sorry for the confusion in terminology. I should have been clearer about the "statically analyzable". It is definitely statically analyzable to determine what to keep (as you mention, it's no different from quite a few similar mechanics in the trimmer already). There's another aspect to "statically analyzable" though - verifiability. And that aspect is not about what is kept, but if what is kept is enough to make the app work. So then the question would be "How will the source generated code be discovered at runtime". That is, what mechanism will be used at runtime to find the Without the verifiability the tooling is not helpful really. We would have tools for library devs to hint the trimmer what to keep, but there would be no tooling to verify that it's good enough. For example, what if the library author made a mistake in how this new attribute is used. It might work in lot of cases, but could be broken in others. Now I used the library in my app, I turn on trimming and my app breaks. We really want to avoid that. We want to build trimmer features such that if the trimmer doesn't produce any warnings, it's almost guaranteed that the app will keep working the same even after trimming. |
Tagging subscribers to 'linkable-framework': @eerhardt, @vitek-karas, @LakshanF, @sbomer, @joperezr Issue DetailsBackground and MotivationI'm working on the new version of the The issue I'm having is that with this setup I don't know how to express the dynamic dependency between my code, to help with trimming. As in, the generated code will be in consuming projects, and it will be used via reflection by code in the shipped library. I've seen the
Basically I'm looking for a way to specify an inverse dependency, so that my generated code (which is not used directly) will be able to inform the linker that when a well known API (from a well known assembly) is included, then this generated code should be included as well, as it might be called via reflection by that code. I don't see a way to express that currently with existing APIs 🤔 Detailed use-case exampleThere's an protected void ValidateAllProperties()
{
// Creates a delegate with a compiled LINQ expression
static Action<object> GetValidationAction(Type type) { }
EntityValidatorMap.GetValue(GetType(), static t => GetValidationAction(t))(this);
} This will use a compiled LINQ expression to create and cache a function that will invoke namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals
{
[DebuggerNonUserCode]
[ExcludeFromCodeCoverage]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This type is not intended to be used directly by user code")]
internal static partial class __ObservableValidatorExtensions
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
[global::System.Obsolete("This method is not intended to be called directly by user code")]
public static global::System.Action<object> CreateAllPropertiesValidator(global::MyApp.PersonViewModel _)
{
static void ValidateAllProperties(object obj)
{
var instance = (global::MyApp.PersonViewModel)obj;
__ObservableValidatorHelper.ValidateProperty(instance, instance.Name, nameof(instance.Name));
__ObservableValidatorHelper.ValidateProperty(instance, instance.Age, nameof(instance.Age));
}
return ValidateAllProperties;
}
}
} Then the method in protected void ValidateAllProperties()
{
// Fast path that tries to create a delegate from a generated type-specific method. This
// is used to make this method more AOT-friendly and faster, as there is no dynamic code.
static Action<object> GetValidationAction(Type type)
{
if (type.Assembly.GetType("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType &&
extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo)
{
return (Action<object>)methodInfo.Invoke(null, new object?[] { null })!;
}
return GetValidationActionFallback(type);
}
// Fallback method to create the delegate with a compiled LINQ expression
static Action<object> GetValidationActionFallback(Type type) { }
EntityValidatorMap.GetValue(GetType(), static t => GetValidationAction(t))(this);
} Proposed APINot 100% sure on what the best API design would be for this. One possible solutions would be to add a new attribute that'd be the "inverse" of namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Event | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
public sealed class DynamicallyAccessedAttribute : Attribute
{
public DynamicDependencyAttribute(string memberSignature);
public DynamicDependencyAttribute(string memberSignature, Type type);
public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName);
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type);
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName);
public string? MemberSignature { get; }
public DynamicallyAccessedMemberTypes MemberTypes { get; }
public Type? Type { get; }
public string? TypeName { get; }
public string? AssemblyName { get; }
public string? Condition { get; set; }
}
} Usage exampleWith an attribute like this, I could essentially generate the following code: [DynamicallyAccessed("ValidateAllProperties()", typeof(ObservableValidator))]
public static void ValidateAllProperties(global::MyApp.MyViewModel instance)
{
} So that when Alternative Designsn/a RisksNone that I can see.
|
Tagging subscribers to this area: @tommcdon Issue DetailsBackground and MotivationI'm working on the new version of the The issue I'm having is that with this setup I don't know how to express the dynamic dependency between my code, to help with trimming. As in, the generated code will be in consuming projects, and it will be used via reflection by code in the shipped library. I've seen the
Basically I'm looking for a way to specify an inverse dependency, so that my generated code (which is not used directly) will be able to inform the linker that when a well known API (from a well known assembly) is included, then this generated code should be included as well, as it might be called via reflection by that code. I don't see a way to express that currently with existing APIs 🤔 Detailed use-case exampleThere's an protected void ValidateAllProperties()
{
// Creates a delegate with a compiled LINQ expression
static Action<object> GetValidationAction(Type type) { }
EntityValidatorMap.GetValue(GetType(), static t => GetValidationAction(t))(this);
} This will use a compiled LINQ expression to create and cache a function that will invoke namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals
{
[DebuggerNonUserCode]
[ExcludeFromCodeCoverage]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This type is not intended to be used directly by user code")]
internal static partial class __ObservableValidatorExtensions
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
[global::System.Obsolete("This method is not intended to be called directly by user code")]
public static global::System.Action<object> CreateAllPropertiesValidator(global::MyApp.PersonViewModel _)
{
static void ValidateAllProperties(object obj)
{
var instance = (global::MyApp.PersonViewModel)obj;
__ObservableValidatorHelper.ValidateProperty(instance, instance.Name, nameof(instance.Name));
__ObservableValidatorHelper.ValidateProperty(instance, instance.Age, nameof(instance.Age));
}
return ValidateAllProperties;
}
}
} Then the method in protected void ValidateAllProperties()
{
// Fast path that tries to create a delegate from a generated type-specific method. This
// is used to make this method more AOT-friendly and faster, as there is no dynamic code.
static Action<object> GetValidationAction(Type type)
{
if (type.Assembly.GetType("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType &&
extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo)
{
return (Action<object>)methodInfo.Invoke(null, new object?[] { null })!;
}
return GetValidationActionFallback(type);
}
// Fallback method to create the delegate with a compiled LINQ expression
static Action<object> GetValidationActionFallback(Type type) { }
EntityValidatorMap.GetValue(GetType(), static t => GetValidationAction(t))(this);
} Proposed APINot 100% sure on what the best API design would be for this. One possible solutions would be to add a new attribute that'd be the "inverse" of namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Event | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
public sealed class DynamicallyAccessedAttribute : Attribute
{
public DynamicDependencyAttribute(string memberSignature);
public DynamicDependencyAttribute(string memberSignature, Type type);
public DynamicDependencyAttribute(string memberSignature, string typeName, string assemblyName);
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, Type type);
public DynamicDependencyAttribute(DynamicallyAccessedMemberTypes memberTypes, string typeName, string assemblyName);
public string? MemberSignature { get; }
public DynamicallyAccessedMemberTypes MemberTypes { get; }
public Type? Type { get; }
public string? TypeName { get; }
public string? AssemblyName { get; }
public string? Condition { get; set; }
}
} Usage exampleWith an attribute like this, I could essentially generate the following code: [DynamicallyAccessed("ValidateAllProperties()", typeof(ObservableValidator))]
public static void ValidateAllProperties(global::MyApp.MyViewModel instance)
{
} So that when Alternative Designsn/a RisksNone that I can see.
|
+1 to this feature request. I have done the same thing (source generating attribute types) and am now running into this limitation with .NET 8 (first time I've enabled trimming). |
@hamarb123 could you please describe the scenario in which you think you need this capability? As noted above, the problem with This is generally why we're hesitant to add a new feature around this, unless we can figure out a way to make it verifiable - which is pretty diffcult (see discussion above). |
My scenario is that I have metadata attributes I stick on assemblies which track important information (versioning info, licenses, etc) - I need to ensure all of this info is kept. The way it's done is with I can see how it's not verifiable, but I believe it can still be quite useful to me, and probably other people. |
Attributes should be kept - and if they take a type as a parameter, that type will also be kept and so on. The trimming doesn't have a way to remove attributes currently. If possible, could you share an example of the shape of the code which you need to make work? I would be interested in both the information which is currently being trimmed as well as the rough shape of the code which uses reflection to access it. |
Also, I'm reading the fields of the attribute, will they be kept?
I don't know if any part of it is currently being trimmed (as I'm only just going through the process of enabling trimming with .NET 8), but I'm trying to be thorough as I know these sorts of things can seem flaky if they're not done correctly (since it will seem like random things are being removed when an unrelated change is made, etc.). Shape of code: All my libraries (generated code - note I'm currently using msbuild generation, not source generation, but that's not really relevant): [module global::hamarb123.Base.Attributes.AssemblyVersionIdentifierV1Attribute("some git hash", ...)] If they do not reference the applicable library, they will also generate the attribute like so: namespace hamarb123.Base.Attributes
{
[global::System.AttributeUsageAttribute(global::System.AttributeTargets.Module, Inherited = false, AllowMultiple = true)]
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
internal sealed class AssemblyVersionIdentifierV1Attribute : global::System.Attribute
{
private readonly string assemblyGitHash;
//...
public AssemblyVersionIdentifierV1Attribute(string assemblyGitHash, ...)
{
this.assemblyGitHash = assemblyGitHash;
//...
}
}
} The library then will have the above attribute defined, also with some helper properties & methods though (which are not added to the generated ones, since only the fields are read on them). My apps ensure that all the libraries are loaded early using The library uses code like this to read it (simplified, to focus on the reading of the attributes - error handling, etc are omitted): foreach (var lib in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var mod in lib.Modules)
{
//check the main type
foreach (var attribute in mod.GetCustomAttributes<AssemblyVersionIdentifierV1Attribute>())
{
//do something with it
}
//check for a user-defined type
var type = mod.GetType("hamarb123.Base.Attributes.AssemblyVersionIdentifierV1Attribute");
if (type != null)
{
//check if all the fields are defined properly
var f_assemblyGitHash = type.GetField("assemblyGitHash", BindingFlags.NonPublic | BindingFlags.Instance);
if (f_assemblyGitHash == null) continue;
//... same with other fields
//loop through all instances of it
foreach (var attribute in mod.GetCustomAttributes(type))
{
//read the fields from attribute using reflection, convert it to the real attribute type & do the same thing as before
}
}
}
} Note that this is the only place that should ever be reading these values. If I could add an inverse |
I think the runtime is still lazy in populating the runtime structures for assemblies - so if you need to rely things like In general, I would advice against relying on
I assume this came from .NET Framework, on .NET 6+ the runtime doesn't allow multi-module assemblies anymore, so this is not needed.
This is going to be the main problem. There's no way for the static analysis to figure this out and do what you want - which is to preserve that attribute on all loaded assemblies. In your case, I think the best solution would be to generate a module initializer - so basically code which is called from the module's type static .cctor. Trimming will always keep module constructor, and so anything referenced from it will be kept as well. If you want to do this completely reflection free - you could generate code into the module .ctor which registers something into a global property in your main library. That way you don't need to perform the assembly enumeration and possibly not even the search for the attributes (as the generated code could do that directly). But it depends on how you do the code generation and the setup there.
If you have a direct reference to the attribute type in your code, and read those fields via direct references, they will be kept. If you do it through reflection then it depends - if you get warnings in this code, then those might not be kept. In your sample above the fields would not be kept necessarily - because the tooling won't figure out what type you're talking about.
I must admit I don't really understand this comment. What would you add /cc @sbomer (interesting scenario) |
The way I do it allows it to be swapped out with a different version of the assembly later if needed, and still see relevant information in, e.g., crash reports. I have actually found my approach very nice to use overall, the only annoyance is the need to pre-load assemblies, but other than that it works very well for me.
Interesting :)
This is a workaround I hadn't thought of yet. Note that only the empty method with Another idea I had which might work is to instead stick the
Inverse |
Yes, that would work. Honestly I don't think it's necessary to design it such that the attribute can be kept, but not its fields if they're not going to be accessed. The fields are probably small enough (and their dependencies) that this would make little difference. You can't store complex structures in attributes anyway (since attributes only allow primitive type parameters), and your attribute is "data only", it doesn't have any of the logic in it. |
In Avalonia, for each public XAML file we put a switch branch in a generated XamlLoader class: // pseudocode
public class !XamlLoader
{
public static object TryLoad(string path)
{
if (path == "avares://MyAssembly/MyFile.axaml")
return CompiledXamlFactories.BuildAndPopulate_MyFile();
return null;
}
} Where XamlLoader is used via reflection from the In a typical XAML type this issue is already solved, as we have control over XAML files, and can replace StyleInclude/ResourceInclude nodes with direct references in the IL. But in applications that don't use XAML or require AvaloniaXamlLoader in the C# code for some other reason, reflection is still used. As right now we don't have a solution for AvaloniaXamlLoader problem, |
Background and Motivation
I'm working on the new version of the
Microsoft.Toolkit.Mvvm
package (aka MVVM Toolkit), and I'm specifically focusing on integrating source generators to make the library more flexible and to improve performance. With respect to this second point, one thing I've worked on is to use source generators to create code at runtime to skip a couple paths in the library where I'm currently using a compiled LINQ expression. The idea is that when the source generator runs, the library will try to grab the generated method and use that instead of a LINQ expression.The issue I'm having is that with this setup I don't know how to express the dynamic dependency between my code, to help with trimming. As in, the generated code will be in consuming projects, and it will be used via reflection by code in the shipped library. I've seen the
[DynamicAttribute]
type, but it seems to basically be the inverse of what I need here. As in, according to the info in this blog post, there's two issues I see in this scenario:[DynamicAttribute]
(according to the blog post) works as follows: "when you include me, also include my friends". I basically need the opposite, as in, "when my friend is included, then include me as well".[DynamicAttribute]
also wants to know the assembly name of the dependent APIs. But in my case the annotated method called by the user (in the MVVM Toolkit) won't know the assembly name at build-time, as that'll just be whatever assembly name the consuming projects will use. I'd just know the fully qualified name (without assembly) of the types I want to keep. Also note that there could be multiple types with this same fully qualified name across different assemblies.Basically I'm looking for a way to specify an inverse dependency, so that my generated code (which is not used directly) will be able to inform the linker that when a well known API (from a well known assembly) is included, then this generated code should be included as well, as it might be called via reflection by that code. I don't see a way to express that currently with existing APIs 🤔
Detailed use-case example
There's an
ObservableValidator
class that has the following method (EntityValidatorMap
is aConditionalWeakTable<K, V>
):This will use a compiled LINQ expression to create and cache a function that will invoke
ValidateProperty
on all validatable properties on the current instance (the type of the instance is used as key to cache the delegates). Works fine, but avoiding a compiled LINQ expression would be nice. The new version with source generators has one that finds all classes in consuming projects inheriting fromObservableValidator
, and for each of them it produces a method like this:Then the method in
ObservableValidator
(in the actual library) is updated as follows:Proposed API
Not 100% sure on what the best API design would be for this. One possible solutions would be to add a new attribute that'd be the "inverse" of
[DynamicDependency]
, such as a[DynamicallyAccessed]
attribute. In this example below I've mostly just adapted the definition from[DynamicDependency]
with a few changes, so consider this more of an idea than a refined proposal (it isn't):Usage example
With an attribute like this, I could essentially generate the following code:
So that when
ObservableValidator.ValidateAllProperties
isn't trimmed out, all these generated methods that could be invoked via reflection by that method will not be trimmed out either.Alternative Designs
n/a
Risks
None that I can see.
The text was updated successfully, but these errors were encountered: