Skip to content

Conversation

pranavkm
Copy link
Contributor

@pranavkm pranavkm commented Aug 14, 2020

  • Exclude views dll \ pdb from single file archive
  • When loading related parts, check if the Assembly is already loaded.

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.

@ghost ghost added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Aug 14, 2020
@pranavkm pranavkm requested review from davidfowl and javiercn August 14, 2020 23:38
}
catch (IOException)
{
// The assembly isn't already loaded. Patience, we'll attempt to load it from disk next.
Copy link
Member

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?

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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);
Copy link
Member

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?

Copy link
Member

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).

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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.

@davidfowl
Copy link
Member

@DamianEdwards how do you feel about this.

cc @glennc @LadyNaggaga

Comment on lines 82 to 83
var assemblyLocation = assembly.Location ?? AppContext.BaseDirectory;
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation);
Copy link
Member

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.
Copy link
Member

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);
Copy link
Member

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).

@davidfowl
Copy link
Member

@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?

@vitek-karas
Copy link
Member

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).

@davidfowl
Copy link
Member

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.

That's problematic. We need to automate this outside of CTI.

@pranavkm pranavkm force-pushed the prkrishn/related-assembly-part branch from 243b77d to 05a421b Compare August 18, 2020 00:04
Comment on lines 20 to 21
private static readonly Func<string, Assembly> LoadFromAssemblyPathDelegate =
AssemblyLoadContext.GetLoadContext(typeof(RelatedAssemblyAttribute).Assembly).LoadFromAssemblyPath;
Copy link
Member

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.

@pranavkm pranavkm force-pushed the prkrishn/related-assembly-part branch from 05a421b to f729895 Compare August 19, 2020 00:52
@pranavkm pranavkm requested a review from a team August 19, 2020 00:52
@pranavkm pranavkm changed the base branch from master to release/5.0 August 19, 2020 00:53
@pranavkm pranavkm force-pushed the prkrishn/related-assembly-part branch from f729895 to ad8a927 Compare August 19, 2020 00:57
Copy link
Contributor Author

@pranavkm pranavkm left a 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 🤞

Copy link
Member

@vitek-karas vitek-karas left a 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.

Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@pranavkm pranavkm force-pushed the prkrishn/related-assembly-part branch from 4d5e938 to 6a9241b Compare August 20, 2020 21:59
@pranavkm pranavkm merged commit 3e8c5c4 into release/5.0 Aug 21, 2020
@pranavkm pranavkm deleted the prkrishn/related-assembly-part branch August 21, 2020 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants