1

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.
IOrlandoni
  • 1,790
  • 13
  • 30
  • 1
    I think you mean preprocessor directives like [`#if`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-if) rather than pragma directives, and yes, that's the way to do it. You can either create one based on some compile-time constant, or use the ones included by the framework (since the version of EFCore depends on the target framework). Your tests can use the same strategy. – Heretic Monkey Oct 24 '19 at 18:27
  • I do mean preprocessor directives. Thanks for the input. What worries me is that my unit tests do not currently fail even though they multi-target, and that my project compiles without warnings/errors. – IOrlandoni Oct 24 '19 at 21:33
  • 1
    See [PropertyInfo](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.propertyinfo?view=netframework-4.8), `Object -->MemberInfo-->PropertyInfo`. maybe this is the reason why both builds succeed. – LoLance Oct 25 '19 at 08:41
  • As stated in the comments to the answer below: That makes sense but then why do I get the exception when trying to use it? – IOrlandoni Oct 25 '19 at 15:43

2 Answers2

1

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

LoLance
  • 25,666
  • 1
  • 39
  • 73
  • This makes some sense but... when the library is used in a `netcore3` project that references `Microsoft.EntityFrameworkCore 3.0.0`, I get the following exception: `System.MissingMethodException: Method not found: 'Microsoft.EntityFrameworkCore.Metadata.IProperty Microsoft.EntityFrameworkCore.EntityTypeExtensions.FindProperty(Microsoft.EntityFrameworkCore.Metadata.IEntityType, System.Reflection.PropertyInfo)'.` – IOrlandoni Oct 25 '19 at 12:39
  • 1
    Given that that `PropertyInfo` casts implicitly to `MemberInfo`... shouldn't it just work? – IOrlandoni Oct 25 '19 at 12:51
  • I think that should work, but something broken in my personal PC stops me from testing it more... I may do more research to reproduce the strange issue and reply on Monday... And it could be better if I can know the details of your 2.2.6 FindProperty code and how you call that in .net core 3.0 project :) – LoLance Oct 25 '19 at 13:51
  • This is the line that uses it: [here](https://github.com/OSDKDev/Panner.Order/blob/059420e4d8fb185fd1b88ff56c7ba2d1e6b8cc5c/src/Utilities/ParserHelper.TryParseOrder.cs#L60). You can see the source code for the whole solution in that repository. The library is available in nuget. A sample solution that reproduces the exception when used in netcore3 is available [here](https://github.com/Dunning-Kruger/Panner.Swashbuckle.AspNetCore/tree/initial-release). – IOrlandoni Oct 25 '19 at 14:12
  • 1
    A sample request for the sample solution at `GET /posts?sorts=Id`. – IOrlandoni Oct 25 '19 at 14:22
  • Stack trace of the exception can be found [here](https://gist.github.com/Dunning-Kruger/51712fca549b8987e3c56e2ad40ec607). – IOrlandoni Oct 25 '19 at 14:24
  • Also, dotnetfiddle that generates the MethodNotFound exception [here](https://dotnetfiddle.net/zfq5J3). – IOrlandoni Oct 25 '19 at 14:51
1

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.

zivkan
  • 12,793
  • 2
  • 34
  • 51
  • Thanks for pointing out the base problem and how to debug it. I'll look into it and see if I can find how to fix it. – IOrlandoni Oct 27 '19 at 14:18