Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does VS 2017 build and test a mixed solution containing .Net Core

I have a more-than-average complex (i suspect) solution, which I have trouble building and testing from command line after upgrading to dotnet sdk 1.1 and Visual Studio 2017.

It has been working in VS 2015, with dotnet sdk 1.0.0-preview2-003131, so it builds and run both in VS 2015 and command line on our buildserver.

But I encounter some problems after upgrading to VS 2017.

To outline the setup. I have a solution roughly laid out like below (more projects in real-life)

MySolution.sln

  • FoundationClasses (x86, .Net Framework 4.5, csproj (legacy)
  • BusinessLogic (x86, .Net Framework 4.5, csproj (legcay)
    • References foundation classes
  • WebApi (Dotnet Core WebApi, net451, x86 (runtimes (win8-x86, win10-x86)
  • TestProject (Dotnet Core, net451, x86)
    • References WebApi

In VS 2015 this was made working by storing restore.dg and project.fragment.lock.json in Git, and then I could run dotnet restore, and subsequently dotnet build, and dotnet test.

After upgrading to VS 2017, everything works fine when I build and run from Visual Studio. (Migrate had some troubles with the references to the foundation projects - but removed those, and readded after migration, and then all was fine)

'dotnet restore mySolution.sln' works fine. It restores packages for WebApi and TestProject correctly - and out of the box in contrary to the preview bits. where I had to fiddle with restore and fragment files.

However, if I run 'dotnet build MySolution.sln -f net452 -r win10-x86' I get a bunch of build errors.

If I run 'dotnet msbuild MySolution.sln -f net452 -r win10-x86' it works.

Is this the correct way to build from CLI tools in a solution like outlined?

So for build and restore I can get it to work both from CLI and VS 2017. And with same results.

But for testing, the consistency stops.

I can run the tests in Visual Studio Test Explorer just fine. Unittests runs fine and are green. But integration tests where I startup a TestServer fails with reference mismatch of Microsoft.Extensions.DependencyInjection.Abstractions 1.0.0 and 1.1.0.

So something in the test chain is requiring 1.0.0 of the assembly, but only 1.1.0 is found in debug dir.

This can be solved by assembly redirection - but quite a few assemblies seems to be wrong/mismatched.

If I run 'dotnet test --no-build TestProject/TestProject.csproj' the tests are all green - and no problems.

So outstanding questions:

  • Is the 'dotnet msbuild' the right way to build a mixed solution?
  • If I run 'dotnet test' without --no-build it fails on compilation - with similar errors as 'dotnet build' (not msbuild)
  • What are the inconsistencies based on in my tests - how can I execute the same within VS as from CLI (preferably figuring out what in VS 2017 that requires assemblies in 1.0.0 range)

I hope I managed to explain thoroughly (but yet simple enough to understand).

If more information is needed to understand the scenario please let me know.

Best Regards Anders

like image 466
ankhansen Avatar asked Dec 24 '22 19:12

ankhansen


1 Answers

Ok, after some hard thinking/digging hours I think I found the proper solution.

Will post here for reference.

Cross-reference to dotnet cli issue: https://github.com/dotnet/cli/issues/6032

I have an existing legacy solution that builds to .Net 4.5, x86, let's call it oldsolution.sln

I have a new solution that contains some of the projects from oldsolution.sln, and some new dotnet core projects, lets call it mySolution.sln.

The new solution also need to build to x86 for .Net 4.5

Building

The key thing to grasp here is that Visual Studio 2017 uses a msbuild.exe in the installation folder to build. dotnet msbuild uses assemblies from the dotnet sdk folder. And these are not identical.

So to build like Visual Studio I have to find the right msbuild executable to use.

The Visual Studio team has developed a tool for that (https://www.nuget.org/packages/vswhere) which I use in my build script.

And with that in place everything works. (build wise)

Below is my PSake script to build and test the solution.

Task BuildApi {
    exec { msbuild ./oldSolution.sln /t:Rebuild /p:Configuration="Release" /p:Platform=x86 /m /verbosity:minimal /nr:false }

    exec { dotnet restore .\mySolution.sln }

    #Find location of VS2017
    $VsPath = .\packages\vswhere.1.0.50\tools\vswhere.exe -latest -property installationPath
    $msBuild17 = "$vsPath\MSBuild\15.0\Bin\MSBuild.exe"

    exec { &$msbuild17 ./mySolution.sln /t:Build /p:Configuration=Release /p:Platform=x86 /m /verbosity:minimal /nr:false }
}

Task TestApi -depends BuildApi{
    if(!(Test-Path TestResults))
    {
    mkdir TestResults
    }
    if(!(Test-Path TestResults/coverage))
    {
    mkdir TestResults/coverage
    }

    $coverage = './packages/OpenCover.4.6.519/tools/OpenCover.Console.exe'
    $target = "`"C:\Program Files (x86)\dotnet\dotnet.exe`""
    $filter = "`"+[WebApi]*`""

    #UnitTests
    $targetargs = "`"test --no-build .\WebApi\test\WebApi.UnitTests\WebApi.UnitTests.csproj -c Release  --logger `"trx;LogFileName=UnitTests.trx`"`""
    $output = 'TestResults/coverage/WebApi.UnitTests.Coverage.xml'
    &$coverage -register:user -oldstyle -target:$target -targetargs:$targetargs -output:$output -filter:$filter

    # IntegrationTests
    $targetargs = "`"test --no-build .\Web\test\WebApi.IntegrationTests\WebApi.IntegrationTests.csproj c Release --logger `"trx;LogFileName=IntegrationTests.trx`"`""
    $output = 'TestResults/coverage/WebApi.IntegrationTests.Coverage.xml'
    &$coverage -register:user -oldstyle -target:$target -targetargs:$targetargs -output:$output -filter:$filter

    #Generate HTML report
    $reportGenerator = "./packages/ReportGenerator.2.4.5.0/tools/ReportGenerator.exe"
    $reportFiles = "TestResults/coverage/WebApi.UnitTests.Coverage.xml;TestResults/coverage/WebApi.IntegrationTests.Coverage.xml"
    $targetDir = "./TestResults/coverage/WebApi"
    &$reportGenerator -reports:$reportFiles -targetdir:$targetDir
}

So far so good.

Testing

Then I had troubles getting OpenCover to pick up coverage results.

Found out that XUnit does a shadowCopy - fix for this is to place a xunit.runner.json in each test project (https://xunit.github.io/docs/configuring-with-json.html)

{
  "shadowCopy": false
}

Telling XUnit not to do shadow copying, and thus OpenCover can find the PDB file matching the executable under test.

Finally...

Running test from within Visual Studio, all tests referencing some of the dotnet core assemblies using a Microsoft.AspNetCore.TestHost.TestServer failed because "something" referenced v 1.0.0 version of the assemblies - and my projects referenced v 1.1.x.

I have fixed this by making assembly redirections in an app.config file for all failing assemblies. Didn't manage to figure out who/what used the old 1.0.0 assemblies - but something in the VS 2017 build chain seemed to do that - as that worked with dotnet test.

But, here's a copy of my app.config with redirects placed in my integration test project.

<?xml version="1.0" encoding="utf-8"?>

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">"
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.Mvc.Core" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Extensions.Options" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.Http.Abstractions" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.StaticFiles" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Extensions.FileProviders.Abstractions" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Extensions.Primitives" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.Routing" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.Routing.Abstractions" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.1.0" newVersion="1.1.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.Mvc.Formatters.Json" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.AspNetCore.Mvc.ApiExplorer" culture="neutral" publicKeyToken="adb9793829ddae60" />
        <bindingRedirect oldVersion="0.0.0.0-1.1.2.0" newVersion="1.1.2.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Phew... That was a rought battle.

But for what it's worth.

A combined solution with legacy x86, net 4,5 assemblies, running inside brand new .Net Core Web Api solution works - and I am happy.. :-)

like image 153
ankhansen Avatar answered Apr 30 '23 04:04

ankhansen