Open John0King opened 4 years ago
Due to 5.0 schedule constraints, this is being moved to Future.
a image describe the problem .
@steveharter Is there any discussion in your team for this issue ?
if you use .net 5.0.100 sdk and use a nuget package newer version micrsoft.extension.*
package , the dll from nuget will be copy and cause a assembly redirection, but it will not for a plugin alc.
you can see that in $1 , it fallback to $0 to find the needed assemly, but after it goes into $0 it can't go back, it will find $0.Configuration.Json, and in your plugin , you use $1.Configuration.Json , and this will not case assembly redirection.
Tagging subscribers to this area: @vitek-karas, @agocke, @coffeeflux See info in area-owners.md if you want to be subscribed.
Author: | John0King |
---|---|
Assignees: | - |
Labels: | `api-needs-work`, `area-AssemblyLoader-coreclr`, `area-System.Reflection`, `untriaged` |
Milestone: | Future |
@vitek-karas @jeffschwMSFT any thoughts on this proposal?
It's pretty simple to implement the parent/child relationship in a custom ALC. In the Load
method simply instead of returning null
, call the parent's ALC's Load
instead. And track the parent ALC from the child ALC (member field). If we were to do this in the framework, this is pretty much the extent of the change we would do.
I'm not opposed in principal to introduce this into the framework, I just don't see a big need for it. Multi-level nested ALCs are not that common, and if they're needed the added complexity is minimal even without the framework supporting this directly.
that people in .net team seems not “support” to use the CurrentContextualReflectionContext
Can I ask where did you get this impression from? We added this feature in 3.0 timeframe, so not that long ago... so we definitely "support" it. It's true that it would be better if it was not needed - it's preferable to be explicit about load contexts in all operations which rely on them. The magical "current context" is problematic for many reasons, but it's a necessary workaround given the current state of the code both in the frameworks and in external libraries/apps.
@vitek-karas the problem of one AssemblyLoadContext is that you can not load different version of dependency when you implement a plugin architecture , for example:
AutoMapper
each major version have break change, and plugins may use different version, and this is not a problem in Php/Js and C++, because Php simply use file system and as long as the file is not conflict then it will work, and js/ts and use webpack to pack the node_model into 1 file (like static linking), Cpp can use static link library.
but in .net ,the only way to load different version of same library is to use different AssemblyLoadContext
,
and now the major issue is to make some type across different ALC,
due to the fact that .net do not have header file , and nuget will load every dependency into Alc
, and there no clear BCL (Microsft.Extensions.*
is not BCL only, and Microsoft.AspNetCore.*
is from different Runtime Libraray) , there no way to implement a Public-oriented plugin architecture
and I already describe the issue ,
we obviously can not force people to not reference any Microsoft.Extension.*
in a plugin ! and in fact any nuget package can have the same issue.
the solution I thought is to make CurrentContextualReflectionContext
to behave like the AssemblyLoadContext.Default
, so that will be multiple assembly redirection in different CurrentContextualReflectionContext
.
eg.
current ALC --> parent ALVC -> ..... -> current ALC
=======example ==========
class Foo1{ public Foo2 B{get;set;} }
class Foo2{}
plugin$1 ----> Default ALC (Found) --(looking for Foo2)-> plugin$1
[Foo2#1 ] ··········· [Foo1, Foo2#0]
result: in default alc , it treat Foo2#1 a different type than Foo2#0,
and in current plugin : Foo1 is reference Foo2#1
in this example , Foo1
is the actual type that shared in different ALC, but it's dependency can be load different version
and this is the current type looking behaviors in .net core 3.x and .net 5
=======current .net 5 example (bad one) ==========
class Foo1{ public Foo2 B{get;set;} }
class Foo2{}
plugin$1 ----> Default ALC (Found) --(looking for Foo2)-> DefaultALC
[Foo2#1 ] ··········· [Foo1, Foo2#0] ············
result : plugin$1 use Foo2#1, and Foo1 reference Foo2#0 , so Foo2#1 is not Foo2#0
but as long as we can solve this issue , any thought is welcome, maybe the simplest way is CurrentContextualReflectionContext.SharedAssmelies.AddRange( DependencyScaner.ScanDependencies( typeof(MyType).Assmebly ) )
The current assumption is that plugins have a clearly defined interface between them and the host. And that plugins don't talk to each other. It's technically possible to do all those things as well, but it gets complicated.
In this sense I don't see how this is different from the "Static linking" approach (other than it's obviously easier to use). Basically each plugin needs to carry all of its dependencies with it, EXCEPT the dependencies which the host provides (this is basically part of the interface between the host and the plugin). Each plugin gets isolated in its own ALC - all of the dependencies which it carried with it are also isolated.
This enforces the contract with the host, where only the defined interface is shared (no plugin should carry the assemblies from that interface).
Again similar to static-linking, statically linked C++ library will have a clearly defined interface (the ABI it exposes) and a set of external dependencies it requires (anything it loads as dynamic library and doesn't carry with it statically linked in). The only difference between C++ and .NET is that it's easier to do versioning of the host/plugin interfaces in C++ in some cases (since there's no type identity, if the struct matches it will work). On the .NET side if you need to version the host/plugin interface it will probably mean a need for multiple versions of the interface assemblies - but it depends on the actual interface and design.
I agree that this is a change from .NET Framework, there it was kind of a "load whatever you want" which sometimes worked (and if it didn't one got stuck in binding redirect hell). In .NET Core the isolation is VERY explicit and so it typically enforces a clearly defined contracts between the components involved (plugins, host).
@vitek-karas
The current assumption is that plugins have a clearly defined interface between them and the host.
the truth is we almost can't in asp.net core. for an asp.net core plugin , it need to share a few type IApplicationBuilder
and HttpContext
, but the host also has many dependency , and one is Microsft.Extension.Configuration.Abstraction
,
first, it's a nuget package, and second , it not a key assembly of plugin , but the host may carry this assmebly and may not. if you just use Microsoft.AspNetCore.App
, then the host app not have this type , and if you use any dependent-ed nuget pakcage, the host will have this type as well , and it's also the same with plugin.
if your plugin have this assembly and your host have this assembly as well , then the problem happened.
Microsft.Extension.Configuration.Abstraction
short name as MECA
MECA
as well.MECA
type you write in your plugin is the plugin onethe result is that , the fallback to Default ALC became a assembly black hole, and any assembly fall in and will can not get out.
so that means , the plugin must not use any assembly that host use. and that;s mean , if the host use 100 assembly , then any plugin must not use those 100 assembly as well, even the plugin architecture shared is just 3 assemblies !!!!!
That's a constraint of the plug-in system. The host can't "pin" dependencies or plugins can't load different versions of those assemblies. Especially if types are exchanged between the plug-in and the host, this "restriction" makes sense.
Now if you're saying that those types aren't exchanged and are totally self-contained in each plug-in then I agree there should be a way to make that work. I think it's possible today but not at all obvious (likely you need the host's deps file to not to include the dependencies that you want loads less by plug-in contexts).
That's why I described that the list of assemblies which the host wants/needs to share with all plugins is effectively part of the contract. For example if I have an Interface.dll
which defines the interface between the host and the plugin, then the full dependency closure of this assembly should be provided by the host - so that all plugins and host agree on those types.
This effectively encourages the interface to be minimalistic, so that the closure is as small as possible. Again not any different from the C++ static linking style - if the native library has lot of dynamic dependencies (which are used in the interface) then it will have the same problem.
The one unfortunate thing today is that the host can't declare/enforce which assemblies should be shared. But the custom ALC which loads the plugins can do this if it wants to (and that ALC is effectively part of the host). Currently the plugins should ideally know this up-front and build accordingly. Any dependencies which are to be shared should use Private=false
which will avoid putting those assemblies into the output of the plugin.
@vitek-karas @davidfowl @steveharter
1st, let me apologies for my bad englisth. Here is some thing I'm thinking for a common used plugin/module framework. https://github.com/John0King/LazyMan.ModularLoader/issues/2.
from a script language, the isolation is pretty simple, all the isolation is done by the file system. for example
//ts
import { Foo } from "Lib1"
import { Foo as Foo2} from "Lib1-copy"
// php
requre_once('../host-shared.php')
// do something with plugin host type
requir_once('./local-shared.php')
// do something with local type
I dont's now about C/C++, but I think it may be the same , isolated by file, at latest it can use static linking with plugin, and dynamic linking with plugin's host app (but I think it may work , but the method and type may not be fully isolated )
I'm wonder that can we using file system to do the isolation ? after all the assemblies in a plugin is identifiable, so why we need to do type redirection in runtime instead of build time ?
previously two dll have same namspace and type can use alias
(on <reference />
) add call using alias:namespace.type
, it sound like this is our file system isolation.
Technically C# can do this: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/extern-alias .NET Core doesn't support this out of the box though - you would have to implement custom ALC and adapt the resolution accordingly, but it's technically possible.
But honestly that's not the main problem. The problem is if the explicitly specified "plugin" (by file path) references some other assembly. How is that reference resolved. I actually don't know how ts or php do this - if it's also filepath based, then it's pretty simple to do the same in .NET.
But then the main problem will be sharing of types - ts is basically javascript and so it relies on just shapes of objects (there are really no types). And so if the host's view and plugin's view of a given "type" are similar enough, they will be able to exchange instances of that "type". I honestly don't know PHP well enough to comment on that. .NET on the other hand is strongly typed - each instance has a strictly defined type, and so to exchange instances (without usage of reflection) both host and plugin must share the exact same type definition.
There are ways around this even in .NET, but they're not "nice" - you could use dynamic
, or you could build something similar with DispatchProxy
.
one problem is that now .net release 2 set of runtime + 2 more deploy mode.
runtime:
1) Server Runtime (.net BCL core + asp.net core workload)
2) DesktopRuntime (.net BCL core + Windows workload)
3) AndroidRuntime (.net BCL core + Android workload)
4) ~~IOSRuntime (.Net BCL core + IOS workload)~
deploy mode: 1) Runtime installed + App 2) Self-containd 3) AOT (all static linking , but no dynamic link)
and with .net 6 , there's no BCL assemblies in workload can be known before you deploy it ( "System.Drawing.Common" is an example), and the "framework.extensions.*" always be have an issue "does it already include in host app ? or include those dll in plugin by accidentally reference one of the nuget package"
and another problem is Share the type that need isolation :
Host App -----> a stand alone app as plugin (it's a host of it's won plugin) ----> plugin of plugin
App.exe/dll (+ PluginLoader.dll@v1 ) ----> (standApp.dll + PluginLoader.dll@v2) ---> plugin.dll
in this example, as the developer , we know the standApp.dll need share Plugin.Loader.dll from App, so it can not include the PluginLoader.dll@v2
otherwise it can not provide the type that the host App.exe can recognition, and if it not provide this PluginLoader.dll@v2
, then the standApp.dll may won't work because it need this new version.
I know this may be bad example , but it describe the problem: we know there two type look exactly the same, but we know they are not the same one , but the C#'s type system can not .
and
ts is basically javascript and so it relies on just shapes of objects (there are really no types). And so if the host's view and plugin's view of a given "type" are similar enough, they will be able to exchange instances of that "type"
but in my example , there no type exchange, it just ts/js syntax can recognize those are two different type.
//ts
import { Module } from "module-loader" // package
import { Module as M2} from "./module-loader/index" //locale version
export class MyModule extends Module{
load(){
let loader = new ModuleLoader();
loader.load(new XModuleFromSomePlugin())
}
}
//
export class ModuleLoader{
load(m M2){
if(m instance of M2){
// real check for type , not the shape of object
m.load();
}
else{
throw new Error('you can not load this, because it's not a module')
}
}
}
I conclude to follow conclusion:
1) with nuget system , we lost more control on file system.
2) there no syntax in C#, can be use to help us from pick the right type from right assembly.
3) no core BCL , eg. ( std
lib for C++
, es
api for javascript
, php script env
for PHP
)
4) C#'s Assmbly -> module -> type , not make any thing better
I hope all of you the smart guy , can start create a module/plugin system, can find ways to make this better. As a .Net developer , don't you jealous PHP's WordPress can have such nice plugin system ?
background
In most time we need alc fallback when doing plugin dynamic load. When a assembly can not be find it should look to it's parent alc instead of just default alc. current issues like #13472 , #37231 also has the issue in alc fallback.
after I look to those issue I also notice that people in .net team seems not “support” to use the
CurrentContextualReflectionContext
, and I do see a valuable usage when I create the sub app for asp.net corethe problem
when create my library
sub app for asp.net core
, I need to share two type from theHost
and theSubApp
===>HttpContext
andRequestDelegate
, but after assembly fallback to use theHostAlc
's Assembly (the current HostAlc is also the default alc), for example:ConfigureLogging()
onIHostBuilder
theIHostBuilder
and the parameter will also use fromdefault alc
, then it end up typeIHostBuilder
is not the typeIHostBuilder
, the reason this issue happen is because a few assembly from<FrameworkReference Include="Microsoft.AspNetCore.App"/>
is being copy or not copy to the app directory. (this is uncommon from a clear plugin loader which theabstract
will only exist in theHostAlc
) anyway, afterConfigureLogging
onhost alc
being calledeverything
will call into theHostAlc
even theCurrentContextualReflectionContext
isPlugin Alc
the current solution
the current solution is make a full
framework assembly manifest
that force it being loaded toHostAlc
the future solution
AssemblyLoadContext.CurrentContextualReflectionContext
to fallback again to the plugin alc , every time looking for a type look "CurrentContextualReflectionContext" fistAssemblyLoadContext.Load(AssemlbyName)
andAssemblyLoadContext.LoadUmanagedDll(string)
to handle the alc fallback by default fromContextualReflectionScope
, so we need a new static property ofStack<AssemblyLoadContext>
inContextualReflectionScope
itself orAssemblyLoadContext
, and people can just callbase.Load(assemlbyName)
to do this complex thing eg.CurrentContextualReflectionContext
will default to theAseemlbyLoadContext.Default
, we are alway in aContextualReflectionScope
, so the fallback to default alc is not being affect (see 1) eg.Assemlby? AssemblyLoadContext.EntryAssembly
and optional constructorAssemblyLoadContext(string name, boo isCollectible, string entryAssemlbyPath,)
thisEntryAssembly
property is the first Assemlby this alc load, the constructor will also haveAssemblyDependencyResolver
but I think maybe this contronctor is not nesssory, because using it we also need to add aSharedAssemblies
property, maybe the best is to add a sub class .Resolving
event and avoid dead loop, theAssemblyLoadContext
need to add a new private property_currentPredecessor
with is a copy ofContextualReflectionScope.Predecessor
whenEnterContextualReflection()
is called. and when.Load( .LoadUnmananedDll()
being called each time it will.Pop()
an alc , and when this event being fired, thisStatc<AssemblyLoadContext>
is empty, so it's safe to callAssemlbyLoadContext.Default.LoadFromAssemblyName()
by using Stack, will the perf too slow
for 99% user case , the stack will only contains 1 item, which is the default alc, so it will not fallbak many times for most user , unless there many child of alc