简体   繁体   中英

Handling conditional package references in code and unit tests

I have a library project that extends some functionality on EntityFrameworkCore . I'm looking to support both 2.* and 3.* . My project is setup like so:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
[...]
  </PropertyGroup>
  <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
  </ItemGroup>
  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.6" />
  </ItemGroup>
[...]
  </PropertyGroup>
</Project>

In the code I'm using the function EntityTypeExtensions.FindProperty(...) . The signature of this function changes between 2.2.6 and 3.0.0 .

The project's code (incorrectly?) uses the signature for 2.2.6 . This compiles properly (which shouldn't be the case?) in both target frameworks.

I have a unit test project that multi-targets and has conditional references, much like the original project:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netcoreapp3.0;netcoreapp2.0</TargetFrameworks>
[...]
  </PropertyGroup>
[...]
  <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.2.6" />
  </ItemGroup>
  <ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.0.0" />
  </ItemGroup>
[...]
</Project>

All unit tests (incorrectly?) pass in both target frameworks.

Note that even though it builds and tests pass, when the library is used in a netcore3 project (which references efcore 3.0.0 directly) it throws the following exception. Which seems completely reasonable, I just don't understand why it allowed me to get to this point.

System.MissingMethodException: Method not found: 'Microsoft.EntityFrameworkCore.Metadata.IProperty Microsoft.EntityFrameworkCore.EntityTypeExtensions.FindProperty(Microsoft.EntityFrameworkCore.Metadata.IEntityType, System.Reflection.PropertyInfo)'.

Questions:

  • Is there a way around this so it gets picked up as an error/warning/something, at least, during the build?
  • Is the solution to this to use preprocessor directives around the call to .FindProperty(...) and based on the framework make the correct method call? Isn't there a way to do this based on the version of efcore instead of the dependency?
  • Is there a way to unit test this properly with the different packages? Right now as it is, I expected the unit tests to fail in one of the versions since the method does not exist.

  • Source repository and specifically the call to FindProperty can be found here .
  • Sample netcore3 project that results in a MissingMethodException when calling the library can be found here .
  • Stack trace of the exception can be found here .

Not to directly answer all questions above one by one, just to describe the cause of the original issue and some suggestions.

In the code I'm using the function EntityTypeExtensions.FindProperty(...). The signature of this function changes between 2.2.6 and 3.0.0.

According to your description, I assume you may use code like EntityTypeExtensions.FindProperty(entityType, propertyInfo);in your original project.

For Microsoft.EntityFrameworkCore 2.2 :

FindProperty (this Microsoft.EntityFrameworkCore.Metadata.IEntityType entityType, System.Reflection.PropertyInfo propertyInfo); second parameter=>PropertyInfo

For Microsoft.EntityFrameworkCore 3.0 :

FindProperty (this Microsoft.EntityFrameworkCore.Metadata.IEntityType entityType, System.Reflection.MemberInfo memberInfo); second parameter=>MemberInfo

However, please check PropertyInfo Class , you'll find:

Inheritance: Object->MemberInfo->PropertyInfo

And I think that's the reason why the project's code uses the signature for 2.2.6 but it compiles properly in both target frameworks . And it's the cause of other strange behaviors you met after that...

So for this issue, you could use the signature for 3.0.0 (MemberInfo) in code instead of 2.2.6 (PropertyInfo) to do the test. I think the build will fall as you expected. And as Heretic suggests in comment, for multi-target project, use #if is a good choice.

Hope all above makes some help and if I misunderstand anything, please feel free to correct me:)

I have good news and bad news. The good news is that the problem is with your package, and everything works just how you appear to believe it should work. The bad news is I don't know how your package got incorrectly authored.

Steps to verify: Download Panner.Order version 1.1.0 from nuget.org (you've published 1.1.1 since asking this questions, which has the same, but different, problem). If you have NuGet Package Explorer installed, open the nupkg with that, expand the lib/ folder and double click each of the .dll files. Alternatively you can extract the nupkg as a zip file then use ILSpy or ILDasm or whatever else you want to inspect the assemblies. Notice that both the netstanard2.0 and netcoreapp3.0 assemblies have the same assembly references. In particular the Microsoft.EntityFrameworkCore.dll reference is for version 2.2.6.0, even though we'd expect the netcoreapp3.0 version to use version 3.0.0.0. Therefore I conclude that your netstandard2.0 assembly was copied incorrectly into the netcoreapp3.0 folder of your package. Your 1.1.1 package has the opposite problem. Both the netstandard2.0 and netcoreapp3.0 folders contain the netcoreapp3.0 assembly, so your package doesn't work with projects that try to use the netstandard2.0 assembly.

However, I have no idea why this happens. When I clone your repo and run dotnet pack and check the generated nupkg, I can see that the netstandard2.0 and netcoreapp3.0 assemblies have different references, so I'm confident that the package I generated locally should work. You need to investigate why the packages you publish are not being generated correctly.

To quickly answer your questions:

Is there a way around this so it gets picked up as an error/warning/something, at least, during the build?

It will, as the problem was not with the project, but with the package. If you multi-target your project and call an API that does not exist in at least one of the TFMs, you will get a compile error.

Is the solution to this to use preprocessor directives around the call to.FindProperty(...) and based on the framework make the correct method call? Isn't there a way to do this based on the version of efcore instead of the dependency?

When you call APIs that are different in different TFMs, yes, you can use #if to change your code per project TFM, as described in ASP.NET Core's docs when migrating to 3.0 .

I'm going to ignore the "based on the version of efcore" because I'm a detail oriented person, and I don't want to write one thousand words for something that ultimately doesn't matter. The key is that in this scenario, you don't need to. You used conditions on your package references to bring in a different version of efcore per project TFM, so each time your project gets compiled, it's using a different version of efcore, but only one version per compile target. Therefore you don't need runtime selection of different versions of efcore.

Is there a way to unit test this properly with the different packages? Right now as it is, I expected the unit tests to fail in one of the versions since the method does not exist.

You multi-target your test project, but I see you've done that already. Since you're using a project reference, the test won't detect package authoring problems like what's happening.

If you really want to test the package, rather than your code, you could use a nuget.config file to add a local folder as a package source, then your multi-targeting test project references the package, not the project. You'd probably want to also use the nuget.config file to set the globalPackagesFolder to something that's in .gitignore because NuGet considers packages to be immutable and if a debug version of your package gets into your user profile global packages folder, every project you use on that machine (that uses your user profile global packages folder) will use that debug version, making it more difficult for you to make updates. For customers who want to test packages, rather than projects, I highly recommend using SemVer2's pre-release labels and create a unique package version for every single build to reduce the risk of testing a different version than you intend.

Using package reference rather than project reference is a pain, because it's no longer as simple as writing code and then running the test. You'll need to change code, compile the project that gets generated into a package, copy the package into the package source folder if you haven't automated that, update the package version in your test project, then compile and run the test project. I think you're better off keeping the project reference. Fix the package authoring problem and then trust the tooling works.

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