简体   繁体   中英

.net Core 3.0 GetMethod with types parameter doesn't work

I am trying to load plugin assemblies that contain static methods that take an IServiceCollection parameter. I have successfully loaded the assembly and obtained the type that contains a method (t below) but if I include the types parameter I am unable to get the method with GetMethod().

methodInfo = t.GetMethod(mappingConfig.MethodName, BindingFlags.Public | BindingFlags.Static, null, 
  new Type[] { typeof(IServiceCollection) }, null);

The interesting thing is I can get the method using

methodInfo = t.GetMethod(mappingConfig.MethodName, BindingFlags.Public | BindingFlags.Static); 

But if I attempt to verify the parameter of the method the type comparison seems to fail for no explainable reason.

methodInfo = t.GetMethod(mappingConfig.MethodName, BindingFlags.Public | BindingFlags.Static);

methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection); // Returns false
methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection)); // Returns false
methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection)); // Returns false

Static method looks like this:

public static class CustomFieldsEntityMapBuilderPlugin
{
  public static void AddCustomFieldMaps(IServiceCollection services)
  {
    var builder = new EntityMapBuilder(services);
    builder.AddCustomFieldsToMaps<AxEntities.CustomerV3, CRMEntities.lev_customer>();
  }
}

When I assign the two types to variables and inspect them I can't see any differences:

var parmType = methodInfo.GetParameters()[0].ParameterType;
var iServiceCollectionType = typeof(IServiceCollection);

parmType.FullName == iServiceCollectionType.FullName; // true
parmType.AssemblyQualifiedName ==  iServiceCollectionType.AssemblyQualifiedName; //true

// parmType.FullName == Microsoft.Extensions.DependencyInjection.IServiceCollection
// iServiceCollectionType.FullName == Microsoft.Extensions.DependencyInjection.IServiceCollection
// parmType.AssemblyQualifiedName == Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.DependencyInjection.Abstractions, Version=3.0.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
// iServiceCollectionType.AssemblyQualifiedName == Microsoft.Extensions.DependencyInjection.IServiceCollection, Microsoft.Extensions.DependencyInjection.Abstractions, Version=3.0.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60

It appears to me that whatever is causing those types to not be considered as equal is causing the GetMethod() to fail.

It seems as though I must be missing something. Any help is appreciated. I want to make sure I am getting a method with the correct parameter.

Update:

I've been able to reproduce this issue with a small console application. If I use a method in a plugin the reflection seems to treat the IServiceCollection type differently even though they are from the same assembly, but may be loaded in a different context.

Since I have been asked to share the complete code & I don't know how to upload it, I have now posted it in the question.

From the TestGetMethod.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.2" />
  </ItemGroup>

</Project>

Program.cs

using System;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using System.IO;

namespace TestGetMethod
{
    class Program
    {
        static void Main(string[] args)
        {
            var assembly = Assembly.GetExecutingAssembly();
            Type t = assembly.GetType($"TestGetMethod.{nameof(CustomFieldsEntityMapBuilderPlugin)}");

            MethodInfo methodInfo = t.GetMethod("AddCustomFieldMaps",
                BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(IServiceCollection) }, null);

            Console.WriteLine($"methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection) Returns {methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection)}");
            Console.WriteLine($"methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection)) Returns {methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection))}");
            Console.WriteLine($"methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection)) Returns {methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection))}");

            assembly = LoadPlugin();
            t = assembly.GetType($"PluginLibrary.{nameof(CustomFieldsEntityMapBuilderPlugin)}");
            methodInfo = t.GetMethod("AddCustomFieldMaps",
                BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(IServiceCollection) }, null);
            Console.WriteLine($"methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection) Returns {methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection)}");
            Console.WriteLine($"methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection)) Returns {methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection))}");
            Console.WriteLine($"methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection)) Returns {methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection))}");
        }
        static Assembly LoadPlugin()
        {
            string pluginLocation = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location), "PluginLibrary.dll");
            Console.WriteLine($"Loading commands from: {pluginLocation}");
            PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);
            return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
        }
    }
}

PluginLoadContext.cs

using System;
using System.Reflection;
using System.Runtime.Loader;

namespace TestGetMethod
{
    class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;

        public PluginLoadContext(string pluginPath)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }

        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }

            return IntPtr.Zero;
        }
    }
}

CustomFielsEntityMapBuilderPlugin.cs

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;


namespace TestGetMethod
{
    public static class CustomFieldsEntityMapBuilderPlugin
    {
        public static void AddCustomFieldMaps(IServiceCollection services)
        {
            Console.WriteLine("InAddCustomerFieldMaps");
        }
    }
}

From PluginLibrary.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.2" />
  </ItemGroup>

</Project>

CustomFieldsEntityMapBuilderPlugin.cs

using System;
using Microsoft.Extensions.DependencyInjection;

namespace PluginLibrary
{
    public static class CustomFieldsEntityMapBuilderPlugin
    {
        public static void AddCustomFieldMaps(IServiceCollection services)
        {
            Console.WriteLine("InAddCustomerFieldMaps");
        }
    }
}

I built PluginLibrary and deployed it to the same location as the TestGetMethod assembly.

If you look in program.main, I load the executing assembly and get the CustomFieldsEntityMapBuilderPlugin class from the current assembly with the TestGetMethod namespace. When I compare TestGetMethod.CustomFieldsEntityMapBuilderPlugin.AddCustomFieldMaps(IServiceCollection services) parameter type to typeof(IServiceCollection) the parameter is the same type.

Then I call LoadPlugin() which creates a new context and loads PluginLibrary.dll into that context and returns it as an assembly. Then I get the CustomFieldsEntityMapBuilderPlugin class from the PluginLibrary assembly with the PluginLibrary namespace. When I compare PluginLibrary.CustomFieldsEntityMapBuilderPlugin.AddCustomFieldMaps(IServiceCollection services) parameter type to typeof(IServiceCollection) the parameter is not the same type.

To make it work, change PluginLibrary.csproj to this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.0.2">
        <Private>false</Private>
        <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>

@Ryanman

"They may require the root DI package as well."

The root DI package is not required, since the PluginLibrary project depends on IServiceCollection interface (used in CustomFieldsEntityMapBuilderPlugin class) which is defined in Microsoft.Extensions.DependencyInjection.Abstractions. Referencing it is enough to compile the project. It is generally a good practise to not include references of libraries, which features we don't actually use.

"Can you explain why Abstractions being included will help solve the problem?"

Including Abstractions package instead of core DI package is not intended to solve the problem. It's just a matter of preference.

The key feature which solves the problem is this:

<ExcludeAssets>runtime</ExcludeAssets>

I will try to explain it below:

Let get back to the original PluginLibrary.csproj code and do a step by step debugging to see what's going on under the hood. First we should change line 24 and 25 of Program class to the line below, to let the program run not causing an exception:

methodInfo = t.GetMethod("AddCustomFieldMaps");

Than after starting the program and loading the plugin library we see this in console output:

methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection) Returns False
methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection)) Returns False
methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection)) Returns False

What does it mean is that IServiceCollection type referenced from PluginLibrary project IS NOT EQUAL to IServiceCollection type referenced from TestGetMethod project (currently executing project).

Let's see what the documentation says about comparing types.

https://docs.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext

"When two AssemblyLoadContext instances contain type definitions with the same name, they're not the same type. They're the same type if and only if they come from the same Assembly instance."

Let's check what are actual load contexts of the types that we are comparing and add these lines to the end of Main method:

Console.WriteLine($"PluginLibrary IServiceCollection load context: {System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(methodInfo.GetParameters()[0].ParameterType.Assembly)}");
Console.WriteLine($"TestGetMethod IServiceCollection load context: {System.Runtime.Loader.AssemblyLoadContext.GetLoadContext(typeof(IServiceCollection).Assembly)}");

After starting the program we see the following:

PluginLibrary IServiceCollection load context: "" TestGetMethod.PluginLoadContext #0
TestGetMethod IServiceCollection load context: "Default" System.Runtime.Loader.DefaultAssemblyLoadContext #1

What is evident is that contexts are different and that's why type comparsion returns False result.

Let's now change PluginLibrary.csproj to the code below, copy ALL the resulting files to TestGetMethod output folder and run the program:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.2">
    <ExcludeAssets>runtime</ExcludeAssets>
  </PackageReference>
</ItemGroup>

Now we see in console the following:

methodInfo.GetParameters()[0].ParameterType == typeof(IServiceCollection) Returns True
methodInfo.GetParameters()[0].ParameterType.Equals(typeof(IServiceCollection)) Returns True
methodInfo.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(IServiceCollection)) Returns True
PluginLibrary IServiceCollection load context: "Default" System.Runtime.Loader.DefaultAssemblyLoadContext #1
TestGetMethod IServiceCollection load context: "Default" System.Runtime.Loader.DefaultAssemblyLoadContext #1

Now the program is working as expected, because it is configured to treat the Microsoft.Extensions.DependencyInjection assembly and all of it's dependencies as shared. In fact that configuration is defined in PluginLibrary.deps.json output file. We could achive the same efect if we wouldn't modified PluginLibrary.csproj file but instead deleted PluginLibrary.deps.json from TestGetMethod output directory.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM