-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Add support for views + SingleFileExe #24925
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
Conversation
} | ||
catch (IOException) | ||
{ | ||
// The assembly isn't already loaded. Patience, we'll attempt to load it from disk next. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First chance exceptions creeping into ASP.NET Core... Making me sad. Do we need this check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this perf critical in some sense? If not, I think a cleaner way would be to skip this check and simply try to load the assembly. If it's already loaded, runtime will relatively quickly return the loaded assembly from cache.
The only problem might be for assemblies which are in the single-file bundle, but could also appear on disk (possibly in a different version) - is that something we need to handle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a perf critical scenario. It happens one time at the start of the application. Could you elaborate what "simply try to load the assembly" means? What API are we talking of?
The only problem might be for assemblies which are in the single-file bundle, but could also appear on disk (possibly in a different version) - is that something we need to handle?
It's possible to abuse the api to do this, but it's not something we would claim to support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"simply try to load the assembly" - I meant basically remove the check and just always run the code below it - which will find the file on disk and load it (hopefully via something like AssemblyLoadContext.LoadFromAssemblyPath
). Runtime has a cache which per-ALC stores all loaded assemblies - if it's asked to load the same file again, it will simply return the already loaded assembly. That check happens relatively early on and is pretty fast (no file IO if I remember correctly).
@@ -106,7 +123,7 @@ public static IReadOnlyList<Assembly> GetRelatedAssemblies(Assembly assembly, bo | |||
} | |||
} | |||
|
|||
var relatedAssembly = loadFile(relatedAssemblyLocation); | |||
relatedAssembly = loadFile(relatedAssemblyLocation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this LoadFile isn't correct as it loads the assembly into a different load context. @vitek-karas What would we be using instead? AssemblyLoadContext.GetLoadContext(typeof(RelatedAssemblyAttribute)).LoadFormAssemblyFile
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on what the goal is. If the usage of the related assemblies is to "attach" some code to an existing assembly, then I think it would make sense to load the related assemblies into the same load context as the "annotated" assembly.
So something like AssemblyLoadContext.GetLoadContext(assembly).LoadFromAssemblyPath
(assembly
is where we look for the attributes).
Note though that this may be a breaking change. Assembly.LoadFile
has the specific behavior that it will load into a new load context for every file. Side effect of this is that it pretty much always succeeds (there will never be any versioning problems) - so if the related assemblies contain two assemblies of the same name but from different place and thus potentially different version, LoadFile
will not fail on them. Loading into a manually picked ALC might fail (depends if there's any load context isolation in play or not).
That said LoadFile
typically leads to more subtle versioning problems - even though the assembly specified is loaded in isolation and thus always works, its dependencies are NOT isolated and will fallback to the Default ALC. So for example if I have two assemblies which both use Newtonsoft.Json but one version 11 and the other version 12 and I load those two assemblies via LoadFile
, it will likely lead to some kind of problem - either loading of references for one of them will fail (if 11 gets loaded before 12 is asked for), or I might get weird exceptions later on (if 12 is used for reference to 11 and there are behavioral changes in 12).
All in all - I think it would be great to also look in detail into what assembly loading behavior is desirable in this code. That said it's not really related or necessary for single-file use cases (those will pretty much work as before).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It likely has no meaningful observable difference (other than avoiding a new context) as these assemblies have unique names and share references with the app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To expand on @davidfowl's comment, these assemblies contain the result of compiling views that the application cannot reference. They're loaded so that MVC can discover and execute them. They also have the exact set of dependencies as the application so there's little risk of version conflict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case using AssemblyLoadContext.GetLoadContext(assembly).LoadFromAssemblyPath(path)
is probably the best solution. It should work even if the code is using multiple ALCs (right now it probably never does), so good for the future and it saves you the additional ALCs. It also sort of guards against potential "misuse" which you don't want to support anyway. Having the related assemblies share the same ALC as the "main" assembly will also have the nice side-effect that they will share the dependency resolution with the "main" assembly. So if in the future you add some logic to how dependencies are resolved and so on, the related assemblies would just keep on working.
@DamianEdwards how do you feel about this. |
var assemblyLocation = assembly.Location ?? AppContext.BaseDirectory; | ||
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is problematic. For normal file-based assembly C:\temp\MyApp.dll
the assemblyDirectory
will end up C:\temp
. But for single-file in C:\temp\MyApp.exe
the assemblyDirectory
will end up as C:\
- very likely wrong.
} | ||
catch (IOException) | ||
{ | ||
// The assembly isn't already loaded. Patience, we'll attempt to load it from disk next. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this perf critical in some sense? If not, I think a cleaner way would be to skip this check and simply try to load the assembly. If it's already loaded, runtime will relatively quickly return the loaded assembly from cache.
The only problem might be for assemblies which are in the single-file bundle, but could also appear on disk (possibly in a different version) - is that something we need to handle?
@@ -106,7 +123,7 @@ public static IReadOnlyList<Assembly> GetRelatedAssemblies(Assembly assembly, bo | |||
} | |||
} | |||
|
|||
var relatedAssembly = loadFile(relatedAssemblyLocation); | |||
relatedAssembly = loadFile(relatedAssemblyLocation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It depends on what the goal is. If the usage of the related assemblies is to "attach" some code to an existing assembly, then I think it would make sense to load the related assemblies into the same load context as the "annotated" assembly.
So something like AssemblyLoadContext.GetLoadContext(assembly).LoadFromAssemblyPath
(assembly
is where we look for the attributes).
Note though that this may be a breaking change. Assembly.LoadFile
has the specific behavior that it will load into a new load context for every file. Side effect of this is that it pretty much always succeeds (there will never be any versioning problems) - so if the related assemblies contain two assemblies of the same name but from different place and thus potentially different version, LoadFile
will not fail on them. Loading into a manually picked ALC might fail (depends if there's any load context isolation in play or not).
That said LoadFile
typically leads to more subtle versioning problems - even though the assembly specified is loaded in isolation and thus always works, its dependencies are NOT isolated and will fallback to the Default ALC. So for example if I have two assemblies which both use Newtonsoft.Json but one version 11 and the other version 12 and I load those two assemblies via LoadFile
, it will likely lead to some kind of problem - either loading of references for one of them will fail (if 11 gets loaded before 12 is asked for), or I might get weird exceptions later on (if 12 is used for reference to 11 and there are behavioral changes in 12).
All in all - I think it would be great to also look in detail into what assembly loading behavior is desirable in this code. That said it's not really related or necessary for single-file use cases (those will pretty much work as before).
@vitek-karas the reason we need to do this is because we generate an assembly post build and put the dll in the output folder. That dll isn't in the deps file today and we find it by looking next to the main dll. If there was a way to load from the bundle, then it would be possible to load include the files. @pranavkm did you look at making this an embedded resource? Do we I'm too late in the pipeline to do that? |
Currently the only way to put it into the bundle would be to include it in the deps file.. so I guess that's a no. Embedded resource would definitely work (depending on how complex would that be to make work within the build system though). |
That's problematic. We need to automate this outside of CTI. |
243b77d
to
05a421b
Compare
private static readonly Func<string, Assembly> LoadFromAssemblyPathDelegate = | ||
AssemblyLoadContext.GetLoadContext(typeof(RelatedAssemblyAttribute).Assembly).LoadFromAssemblyPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not do this - RelatedAssemblyAttribute
is from Mvc.Core which is part of the framework, so pretty much always loaded into the default ALC. So this is the same as doing AssemblyLoadContext.Default.LoadFromAssemblyPath
.
If in the future something loads the "main" assembly (the one with the related assemblies on it) into a secondary ALC this will likely be wrong and things might break. I think it should be AssemblyLoadContext.GetLoadContext(assembly).LoadFromAssemblyPath
- where assembly
is the "main" assembly.
05a421b
to
f729895
Compare
f729895
to
ad8a927
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is updated. I had a chat with @vitek-karas and this seems like the most reasonable solution given how some views appear inside the bundle and others do not. I was also able to figure out a way to author a test. Hopefully it works on the CI 🤞
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good - the parts I know about - so load contexts and single-file. Testing and such - somebody else should take a look as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
4d5e938
to
6a9241b
Compare
I tried writing a template test for this, but we don't build rid specific M.A.A before the template tests are available. I'll add a CTI item for this.