dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.66k stars 1.06k forks source link

Resolve package references to projects #1151

Open natemcmaster opened 7 years ago

natemcmaster commented 7 years ago

In project.json, dependencies could be either a project or a package reference. NuGet and compilation would resolve the package vs project based on some conventions (like folder name) and the global.json file. Futhermore, VS would automatically add new projects to the solution explorer when the were discovered.

In MSBuild, there is no equivalent feature. References are either ProjectReference or PackageReference. Swapping a package reference to a project is not always straight forward. In cases where the package is a transitive reference from other PackageReferences, you have to keep the PackageReference and set ExcludeAssets=All in order to avoid conflicts between ProjectReference and PackageReference. (See https://docs.microsoft.com/en-us/nuget/schema/msbuild-targets#replacing-one-library-from-a-restore-graph). It also requires manually adding new projects to the sln file so they appear in VS.

It would be nice to provide a way to resolve PackageReference to projects without requiring changes to csproj.


Some data on this:


cc @davidfowl @dsplaisted

dsplaisted commented 7 years ago

This is possible to some degree today. If you have a ProjectReference, it will override any package references to the same package ID in the restore graph. You don't need the PrivateAssets=all PackageReference shown in the docs when want to replace the package with a project reference instead of a reference to a DLL.

Some caveats:

@cesarbs got this working for the ASP.NET benchmarks repo: https://github.com/aspnet/benchmarks/pull/190

davidfowl commented 7 years ago

@dsplaisted It's such a bad experience though. We need something comparable to what project.json had. It needs to be obvious and it needs to just work without the caveats listed. On top of that, it doesn't work well in VS if the projects aren't added to the solution (something xproj did automatically)

cwe1ss commented 7 years ago

Getting this feature back would be awesome!

With global.json, it was pretty easy to mistakenly commit the "wrong" global.json/sln file with the local debug paths. It would show up as a broken build on the CI server so it's not the end of the world. I'm just wondering if there's any chance of doing this without touching any of the committed files? Maybe the overrides could be stored in the .suo file or in a separate MySolution.sln.overrides file which could be added to the .gitignore file.

fancyDevelopment commented 7 years ago

@davidfowl I completly agree to you. The developer experience on dnx with project.json was great. especially in the case of debugging into a nuget package from own code. You had just to clone the git repository and set one path in global json. Had to make not a single Change to my project. I couldn't be easier. I can't understand why Microsoft has thrown away such a great feature when all this was already a release candidate and very close to a final release.

mclark1129 commented 7 years ago

I also created a uservoice to restore this or similar tooling within VS.

https://visualstudio.uservoice.com/forums/121579-visual-studio-ide/suggestions/19071868-restore-global-json-functionality-in-vs2017

tmitchel2 commented 7 years ago

I've been crying out for this for years, https://lernajs.io/ does something very similar (functionality, not implementation obviously) in the node.js world and it works really well.

khirakawa commented 7 years ago

For anyone who is looking to try the conditional reference solution accepted in this SO question to workaround this issue, note that conditional references aren't currently supported when doing nuget restores https://github.com/NuGet/Home/issues/4996. Restoring nuget packages from VS GUI seems to work fine though.

ToddThomson commented 6 years ago

Here is a manual process that works for me:

1) When using debug configuration and source exists: Use project reference(s)

2) Otherwise utilize the package reference(s) Packages are created by CI with a custom msbuild target to generate the PackageVersion ( semver version 2 ) number when commits are pushed to the TFS Server.

3) When changing the project build configuration ( Debug <-> Release ) the project must be unloaded/reloaded ( there might be a better way, but I rarely need to move from the Debug configuration ).

4) TFS CI produces the Release build configuration packages with PackageVersion using: version.props imported from Directory.Build.props

Project>
  <PropertyGroup>
    <VersionPrefix>0.9.0</VersionPrefix>
    <VersionSuffix>ci</VersionSuffix>
  </PropertyGroup>
</Project>

file: Directory.Build.targets

<Project>

  <Target Name="SetVersion" BeforeTargets="Build" >
    <PropertyGroup>
      <BuildNumber>$([System.DateTime]::Now.ToString(yyyyMMdd-mmss))</BuildNumber>
      <PackageVersion Condition="'$(VersionSuffix)' != '' AND '$(BuildNumber)' != ''">$(VersionPrefix)-$(VersionSuffix)-$(BuildNumber)</PackageVersion>      
    </PropertyGroup>
  </Target>

</Project>

project csproj snippet

  <Choose>
    <When Condition="'$(Configuration)' == 'Debug' AND Exists('..\..\..\Acme')">
      <ItemGroup Condition="'$(Configuration)' == 'Debug'">
        <ProjectReference Include="..\..\Acme.Composition\Acme.Composition\Acme.Composition.csproj" />
      </ItemGroup>
    </When>
    <Otherwise>
      <ItemGroup>
        <PackageReference Include="Acme.Composition" Version="0.9.0-ci-*" />
      </ItemGroup>
    </Otherwise>
  </Choose>
jnm2 commented 6 years ago

Is there a way to replace a package reference with a project reference when the package is referenced both directly and transitively? I'm not finding a way to suppress the transitive DLL reference, so the compiler finds every type both places and nothing compiles.

natemcmaster commented 6 years ago

You can't for a transitive package reference. You have to lift it to a direct package reference and add ExcludeAssets=All. https://docs.microsoft.com/en-us/nuget/schema/msbuild-targets#replacing-one-library-from-a-restore-graph

jnm2 commented 6 years ago

I think that would have done the trick except that this is an old csproj due to needing winforms designers to work, so necessary package references are not transitively referenced through the replacement project reference.

TFTomSun commented 6 years ago

@natemcmaster you could do something similar to what I do to automatically treat packagereferences as projectreferences. Create a Directory.Build.targets in the root your repositories and add something like this:

<!--?xml version="1.0" encoding="utf-8"?-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>

    <!-- Keep the identity of the  packagereference -->
    <ExtendedPackageReference Include="@(PackageReference)">
      <PackageName>%(Identity)</PackageName>
    </ExtendedPackageReference>

    <!-- Find all package references that are expected to be available as projects -> My convetion: they start with TomSun. -->
    <ExtendedPackageReference2 Include="@(ExtendedPackageReference -> StartsWith('TomSun.') )">
      <IsTomSunPackage>%(Identity)</IsTomSunPackage>
    </ExtendedPackageReference2>

    <!-- Filter them by mapping them to another itemGroup using the WithMetadataValue item function -->
    <TomSunPackageTemp Include="@(ExtendedPackageReference2 -> WithMetadataValue('IsTomSunPackage', True) )"/>

    <!-- Map back to the orginal packagereference identity -->
    <TomSunPackage Include="@(TomSunPackageTemp -> '%(PackageName)' )"/>

    <!-- Remove the TomSun. prefix -->
    <TomSunRepository Include="@(TomSunPackage -> Replace('TomSun.','') )"/>

    <!-- Reference them as the projects:  My convention here is that the project is located in [RepositoryNameWithoutTomSun]\[PackageName]\[PackageName].csproj -->
    <ProjectReference  Include="@(TomSunRepository -> '..\..\%(Identity)\TomSun.%(Identity)\TomSun.%(Identity).csproj' )"></ProjectReference>

    <!-- Remove the package references that are now referenced as projects -->
    <PackageReference Remove="@(TomSunPackage)"/>
</ItemGroup>

 <!-- Some logging ... -->
  <Target Name="LoggingTarget" BeforeTargets="Build">
    <Message Text="@(TomSunPackage)" Importance="high"></Message>
  </Target>
</Project>

The result is, that you can keep your packagereferences without any switch/case logic within the projects itself. They will be only converted to project references, based on conventions, when the Directory.Build.targets file is available in some parent directory. In my case the file is in the parent directory of all git repositories and is only available on my dev machine. I'm quite sure that you could extract such a behavior also into a build-nuget-package.

Of course your convention is different, but maybe this gives you an idea how u could solve it in your case. What is missing in this example is the check, whether the project is in the current solution. That check is technical possible, but i haven't take over the logic yet from my old target framework that was written for classic MSBuild project files.

Could be that this can be done easier now (starting from VS 2017) with targets and a task written in C#. But at least up to VS2015 it was necessary to do that transformation outside of the targets, otherwise the VS UI didn't show the references correctly.

NitashEU commented 6 years ago

@TFTomSun your solution helped me alot! You might consider to add <InProject>false</InProject>.

In VS2017 it showed me all the packages in the UI. Doing this in the first item was enough for me:

<ExtendedPackageReference Include="@(PackageReference)">
  <InProject>false</InProject>
  <PackageName>%(Identity)</PackageName>
</ExtendedPackageReference>
ManniManfred commented 6 years ago

Hi, my current workaround is the following, that checks the solution file.

<PropertyGroup>
    <SolutionContent Condition=" '$(SolutionPath)' != '' and Exists('$(SolutionPath)') ">$([System.IO.File]::ReadAllText($(SolutionPath)))</SolutionContent>
</PropertyGroup>

<ItemGroup Condition="'$(SolutionContent)' != ''">
    <MyPackageReferences Include="@(PackageReference)">
        <IsInSln>$(SolutionContent.Contains('$([System.String]::Copy('"%(Identity)"'))'))</IsInSln>
        <ProjectPath>%(ProjectPath)</ProjectPath>
    </MyPackageReferences>

    <ToUseProjectRef Include="@(MyPackageReferences -> WithMetadataValue('IsInSln', 'True') )"
                Exclude="@(MyPackageReferences -> WithMetadataValue('ProjectPath', '') )">
        <PackageName>%(Identity)</PackageName>
        <ProjectPath>%(ProjectPath)</ProjectPath>
    </ToUseProjectRef>

    <ProjectReference Include="@(ToUseProjectRef -> '%(ProjectPath)' )" />
    <PackageReference Remove="@(ToUseProjectRef)"/>
</ItemGroup>

And add the ProjectPath to package element:

    <PackageReference Include="GInfoNET.Libs.ErrorHandler" Version="1.0.0">
      <ProjectPath>..\..\ErrorHandler\GInfoNET.Libs.ErrorHandler.csproj</ProjectPath>
    </PackageReference>

I tried to get the project path dynamically from solution file, but without success :(


    <ToUseProjectRef Include="@(MyPackageReferences -> WithMetadataValue('IsInSln', 'True') )"
             Exclude="@(MyPackageReferences -> WithMetadataValue('ProjectPath', '') )">
        <PackageName>%(Identity)</PackageName>
        <ProjectPathStartIndex>$([MSBuild]::Add(3, $(SolutionContent.IndexOf(',', %(ProjectNameIndex)))))</ProjectPathStartIndex>
        <ProjectPathEndIndex>$(SolutionContent.IndexOf('"', %(ProjectPathStartIndex)))</ProjectPathEndIndex>
        <ProjectPath>$(SolutionContent.Substring(%(ProjectPathStartIndex), $([MSBuild]::Subtract(%(ProjectPathEndIndex), %(ProjectPathStartIndex)))))</ProjectPath>
    </ToUseProjectRef>
TFTomSun commented 6 years ago

@NitashEU @ManniManfred My current solution determines the project path automatically using regex. It's now completely generic/common and easy to reuse.


  <PropertyGroup>
    <ReplacePackageReferences>true</ReplacePackageReferences>
  </PropertyGroup>

  <Choose>
    <When Condition="$(ReplacePackageReferences) AND '$(SolutionPath)' != '' AND '$(SolutionPath)' != '*undefined*' AND Exists('$(SolutionPath)')">

      <PropertyGroup>
        <SolutionFileContent>$([System.IO.File]::ReadAllText($(SolutionPath)))</SolutionFileContent>
        <SmartSolutionDir>$([System.IO.Path]::GetDirectoryName( $(SolutionPath) ))</SmartSolutionDir>
        <RegexPattern>(?&lt;="[PackageName]", ")(.*)(?=", ")</RegexPattern>
      </PropertyGroup>

      <ItemGroup>

        <!-- Keep the identity of the  packagereference -->
        <SmartPackageReference Include="@(PackageReference)">
          <PackageName>%(Identity)</PackageName>
          <InSolution>$(SolutionFileContent.Contains('\%(Identity).csproj'))</InSolution>
        </SmartPackageReference>

        <!-- Filter them by mapping them to another itemGroup using the WithMetadataValue item function -->
        <PackageInSolution Include="@(SmartPackageReference -> WithMetadataValue('InSolution', True) )">
          <Pattern>$(RegexPattern.Replace('[PackageName]','%(PackageName)') )</Pattern>
          <SmartPath>$([System.Text.RegularExpressions.Regex]::Match( '$(SolutionFileContent)', '%(Pattern)' ))</SmartPath>
        </PackageInSolution>

        <ProjectReference  Include="@(PackageInSolution -> '$(SmartSolutionDir)\%(SmartPath)' )"/>

        <!-- Remove the package references that are now referenced as projects -->
        <PackageReference Remove="@(PackageInSolution -> '%(PackageName)' )"/>
      </ItemGroup>

    </When>
  </Choose>

@ManniManfred The reason why ur solution doesn't work is a known bug in MSBuild that has been shipped with Visual Studio 2017 Update 3: https://github.com/Microsoft/msbuild/issues/2476

ManniManfred commented 6 years ago

@TFTomSun thanks for your solution

tmitchel2 commented 6 years ago

Does anyone know if the https://github.com/dotnet/sourcelink project is meant to lend a hand with improving the experience here...?

ChristopherHaws commented 6 years ago

@tmitchel2 Not really. SourceLink allows you to embed the location of source files into the PDB, which is particularly useful when the link is to a public URL (such as a specific commit to a file on GitHub). Essentially, SourceLink provides the ability for IDE's such as Visual Studio to get the file for the code you are debugging into, but it does not load the project in your solution and allow you to modify it which is what this issue is about.

TFTomSun commented 6 years ago

@tmitchel2 to be a bit more specific, project references don't only ensure a nice debugging experience which u can indeed have with packagereferences too....but also think about refactorings over multiple projects...that wouldn't be possible with package references.therefore automatically replacing package references with project references when the source code of the referenced project is pulled still makes sense.

ManniManfred commented 6 years ago

Hi, I'm looking for a way to also include the .props and the .targets file of the "transfered" project. I tried the following

...
        <!-- Filter them by mapping them to another itemGroup using the WithMetadataValue item function -->
        <PackageInSolution Include="@(SmartPackageReference -> WithMetadataValue('InSolution', True) )">
          <Pattern>$(RegexPattern.Replace('[PackageName]','%(PackageName)') )</Pattern>
          <SmartPath>$([System.Text.RegularExpressions.Regex]::Match( '$(SolutionFileContent)', '%(Pattern)' ))</SmartPath>
          <ProjectDir>$(SmartSolutionDir)\$([System.IO.Path]::GetDirectoryName(%(SmartPath)))</ProjectDir>
          <PropsToImport Condition="Exists('%(ProjectDir)\build\%(PackageName).props')">%(ProjectDir)\build\%(PackageName).props</PropsToImport>
          <TargetsToImport Condition="Exists('%(ProjectDir)\build\%(PackageName).targets')">%(ProjectDir)\build\%(PackageName).targets</TargetsToImport>
        </PackageInSolution>

        <ProjectReference Include="@(PackageInSolution -> '$(SmartSolutionDir)\%(SmartPath)' )"/>

        <!-- Remove the package references that are now referenced as projects -->
        <PackageReference Remove="@(PackageInSolution -> '%(PackageName)' )"/>

      </ItemGroup>

      <PropertyGroup>
        <AllPropsToImport>@(PackageInSolution->'%(PropsToImport)')</AllPropsToImport>
        <AllTargetsToImport>@(PackageInSolution->'%(TargetsToImport)')</AllTargetsToImport>
      </PropertyGroup>

    </When>
  </Choose>

  <Import Project="$(AllPropsToImport)" Condition="'$(AllPropsToImport)' != ''" />
  <Import Project="$(AllTargetsToImport)" Condition="'$(AllTargetsToImport)' != ''" />

but without success. The import statement failed.

TFTomSun commented 6 years ago

@ManniManfred I'm quite sure that the Condition attribute is not supported in itemgroup meta data. As far as I know, the imports are processed first, before any properties or even itemgroups are evalulated. So i think it's technical not possible.

dsplaisted commented 6 years ago

@TFTomSun There are several passes of MSBuild evaluation. The first pass evaluates properties and also follows imports. So an import condition can depend on properties which were previously defined. However, items are evaluated in a subsequent phase, so imports can't depend on items.

There is some documentation on this under "Property and item evaluation order" here: https://docs.microsoft.com/en-us/visualstudio/msbuild/comparing-properties-and-items

The source code which implements this is here: https://github.com/Microsoft/msbuild/blob/master/src/Build/Evaluation/Evaluator.cs

TFTomSun commented 6 years ago

@dsplaisted yeah, but properties that depends on item groups will be evaluated later, right? i had this in mind, because we have the packagereference item group as a dependency here. So even if we would be able to transform them into one paths property it wouldn't still work, right?

@ManniManfred there is maybe another solution. Let's assume all package references are defined in the csproj. It might be possible to extract them with a complex regex logic from the csproj and replace a common left over prefix with a wildcard based path that matches for all referenced csproj directories. if u r able to do that based only on property functions it might work. I'm not sure if the wildcards would be resolved correctly when they are generated by a property function.

@dsplaisted Do you have another, less hacky idea?

ManniManfred commented 6 years ago

hi, i found an other solution for that problem. I write all needed imports to a file and import that file.


    <Target Name="WritePropImports" BeforeTargets="ResolveReferences">
        <PropertyGroup>
            <CmdEchoProps>echo ^&lt;Project^&gt; ^&lt;Import Project="$(AllPropsToImport)" /^&gt; ^&lt;/Project^&gt; &gt; "$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)ref_projects.props"</CmdEchoProps>
            <CmdEchoTargets>echo ^&lt;Project^&gt; ^&lt;Import Project="$(AllTargetsToImport)" /^&gt; ^&lt;/Project^&gt; &gt; "$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)ref_projects.targets"</CmdEchoTargets>
        </PropertyGroup>

        <Exec Command="$(CmdEchoProps)" Condition="'$(AllPropsToImport)' != ''"/> 
        <Exec Command="$(CmdEchoTargets)" Condition="'$(AllTargetsToImport)' != ''"/> 

        <Delete Files="$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)ref_projects.props" Condition="'$(AllPropsToImport)' == ''" />
        <Delete Files="$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)ref_projects.targets" Condition="'$(AllTargetsToImport)' == ''" />
    </Target>

    <Import Project="$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)ref_projects.targets" Condition="Exists('$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)ref_projects.targets')" />
TFTomSun commented 6 years ago

i can't imagine that this will work. The target will be executed after the imports have already been processed.

.... well ok it works on every second build. U'll face issues on clean builds or when changes on the references are built the first time

ManniManfred commented 6 years ago

I thought that too ... I tested it by calling msbuild with deleted files (ref_projects.props, ref_projects.targets) and it works for the first run. I tested it with Visual Studio and it also works with the first build. Visual Studio seems to call my target "WritePropImports" often and before i trigger the build. If i delete the files, visual studio create them right after that.

TFTomSun commented 6 years ago

cool maybe this is a change in the build engine. there are many other use cases for that.

mjamro commented 6 years ago

@TFTomSun Could you please provide some help on how to use your solution? Should this be added in .csproj?

matkoch commented 6 years ago

@mariuszjamro just put it in a Directory.Build.targets file, and that logic will be applied to all project files located under that directory. You also need to define the SolutionPath and add <Project> tag around the whole content @TFTomSun provided.

brunzefb commented 5 years ago

If I create a library consisting of 2 assemblies, ProjectA and ProjectB. Now ProjectA references ProjectB and my App references ProjectA. In my case ProjectA has a Project (not package) ref to Project B. The build (ProjAB.sln) is setup to produce (via .csproj, not .nuspec) A.nupkg and B.nupkg with both packages set to the same version. Lets say the version is 1.0.123

When I look at the App solution, I see a reference to ProjectA, 1.0.123, but when I open ProjectA dependencies I see it depends on ProjectB, 1.0.0 (transitive dependency, but because its a project reference 1.0.0). Ideally I want project A to reference Project B, version 1.0.123.

This will work if Project A does a package ref to Project B, 1.0.123 and I then create A.nupkg version 1.0.123. But now it' is a chicken and egg problem. I can't do a nuget restore on ProjectAB.sln, because Project B has not been built, let alone published to our nuget server -- nuget restore on A fails.

One could build twice -- once with project references, publish the packages locally, then switch to Project References, rebuild and re-publish to nuget server. (The mechanics of switching from PackageReference to Project references are discussed in this thread).

Is my reasoning OK, or am I missing something really big and stupid. Ideally, I'd like a single build with project references, but outputting an A.nupkg (v1.0.123) that package refs B.nupkg (v1.0.123), like a ProjectReference with an attribute that lets me specify my nuget package version.

How do I best do this? 2 stage compile going back to .nuspec?

TFTomSun commented 5 years ago

@brunzefb I'm not sure if I fully understand your problem. Is it related to the topic of this thread? When you use my solution you will always have project references in when you open your solution that contain both projects. When u pack and push ur packages ur normally pack and push them all together when they are in the same repository. I do a pack operation on the solution that contains all projects of the repository. I even pass one global packageversion to a pack operation of the solution. Once you packed ur solution both packages will be available with the same package version and the correct dependency on each other in ur nuget feed.

brunzefb commented 5 years ago

My problem is related to the "package reference" to "project reference" auto-conversion. What I am observing is that when I use project references in my solution, transitive dependencies do not work for another project consuming my library. If all references, even to projects in my solution are package references, transitive dependencies from a third project work fine.

The package to project solution discussed here helps with my problem, but does not get me out of the chicken and egg problem.

First I have to build my lib using project references. Then I publish nuget packages with version V. Then I revert to package refs, adjusting the version to be consumed to V. Then I re-publish the packages as version V. Result is a set of packages that work with transitive refs.

This is clunky, so I am wondering if there is a better way or if I am just not thinking this thing through right.

TFTomSun commented 5 years ago

you mean that if package x has a dependency on package y you can access symbols of both packages when u just reference package x. and if u reference project x u dont get automatically access ti the symbols of project y? if that is ur problem I'll check it. but i am quite sure that project references behave the same like package references now. (since vs2017)

i also don't understand why u would revert to package references... just define package references with Version 1. from the beginning. The solution i provided will automatically convert them to project references when they are both contained in ur solution . then pack them... and if u just want to open project a then do so and project b will be referenced as the latest 1. package thats available on ur feed

brunzefb commented 5 years ago

I will give that a try, thanks.

andycmaj commented 5 years ago

maybe naive, but i have been using Conditionals like this:

  <ItemGroup>
    <PackageReference Condition="!Exists('../../../../shared/ImsHealth.ApplicationBlocks/ImsHealth.ApplicationBlocks.sln')" Include="ImsHealth.ApplicationBlocks.FrontEnd" Version="$(ImsHealthAppBlocksVersion)" />
    <ProjectReference Condition="Exists('../../../../shared/ImsHealth.ApplicationBlocks/ImsHealth.ApplicationBlocks.sln')" Include="../../../../shared/ImsHealth.ApplicationBlocks/src/ImsHealth.ApplicationBlocks.FrontEnd/ImsHealth.ApplicationBlocks.FrontEnd.csproj" />
  </ItemGroup>

that, combined with another CLI-provided property in the conditional makes for a pretty nice local dev experience and still works when the solution consuming the reference is built in isolation in ci pipelines.

TFTomSun commented 5 years ago

@andycmaj I think it's not feasible to spread this logic all over you project files. The logic I provided is implemented at one central point. The advantage is that u can adapt the logic easily, that u don't have to explain such thing to every developer and that u get rid of paths within ur project files,which means you can easily move projects to other folders or repositories.

TFTomSun commented 5 years ago

@brunzefb regarding wildcard referencing...I have posted a workaround to a current issue here: https://github.com/NuGet/Home/issues/7328

andycmaj commented 5 years ago

thanks @TFTomSun i'll take a look at your solution!

@TFTomSun i'm guessing you're talking about https://github.com/dotnet/sdk/issues/1151#issuecomment-385133284

Looks like this only works if the project for the package you want to replace is actually in the current solution. Going to think about how it could be adapted for other projects that are published as nuget packages from completely separate solutions.

TFTomSun commented 5 years ago

@andycmaj it's technically possible, but why do u want to have a project reference on a project when it's not in one common solution? Can u tell me more about ur use case?

andycmaj commented 5 years ago

Interestingly, I was wondering why you'd depend on a nuget PackageReference for a project that WAS in the same solution as the consumer :)

Basically, we have shared libraries that we publish as nuget packages, and many separate services that each consume the shared libraries.

The services are independently deployable/buildable/etc. but for development environments, the relative path between the shared library and the consuming apps is known, so it helps, in order to short-circuit the publish-package/update-version-consumed/re-build-consumer workflow to just be able to directly reference the shared libraries as a ProjectReference and code in the consumer in real-time.

brunzefb commented 5 years ago

A couple of learnings on this. Dotnet Sdk 2.1 did work and produced correct transitive dependencies, but only if the packages were built with dotnet msbuild /t:pack /p:PackageVersion=1.2.3, after building the package with dotnet build.

TFTomSun commented 5 years ago

@brunzefb using only dotnet pack did not work? What happens then? Or in which case did packing not work as expected?

@andycmaj well I have such a similar solution already for a long time. Before msbuild 15 I used to add SmartReference itemgroups which looked pretty much like PackageReferences today. The idea behind the concept is to define just a reference and not it's type nor it's path. The type and path of the reference (project,assembly,nuget package) is determined dynamically by the centrally provided logic. Just think of a big system. You would probably start to implement everything in one repo or one TFS branch. What might happen? You probably want to

one day.

For all those scenarios a project reference would have to be adapted by changing its path or type.

The idea is to leave out the path completely and since msbuild 15 already provides a reference type that has this feature, why not reusing it? Of course we could also still define SmartReferences and transform them into packagereference, projectreference or (assembly) reference. I just found the idea nice to use reference tags, everybody is already familiar with.

mellinoe commented 5 years ago

@jmarolf Mentioned an issue I reported over in dotnet/project-system was tracked here. I'll paste the contents:

I recently changed how a library and its dependencies are versioned in this repository. I have several projects referenced by one another, and I have a "demo" application which references all of the others. Most of the libraries reference each other via ProjectReferences with relative paths. However, I switched one library to use a PackageReference so that I could lock its dependencies to a particular version. The relationship is, in a simplified view, the following:

Demo.csproj ProjectReference -> LibA.csproj ProjectReference -> LibB.csproj

LibB.csproj PackageReference -> LibA 4.5.0 When I build / run the "Demo" project, I want it to use the latest versions from my local repo. However, the "LibB" reference causes it to use the version 4.5.0 LibA.dll from the NuGet cache, even though Demo.csproj has a direct ProjectReference to LibA.csproj.

Is there some way to get this working?

TFTomSun commented 5 years ago

yes it is possible. You need to dynamize the type of the reference. When (in which context) do you want to build LibB.cspoj against LibA 4.5.0 ?

mellinoe commented 5 years ago

I always want to build LibB against LibA 4.5.0. To clarify, the problem is that I can't get Demo.csproj to deploy the locally-built outputs from LibA.csproj, even though a direct project reference exists.

TFTomSun commented 5 years ago

When you build LibB against LibA 4.5.0, an assembly with this version should also be in the output in the End. When Demo builds against the latest version (LibA project) you would somehow to have both versions (the latest (from project) and 4.5.0 (from package) ) in your output, which is possible but not supported by default. And both version would have to be loaded at runtime, which is also technical possible but can lead to other issues, too. And you would have to take over the assembly loading procedure. In general I would suggest to build one executable artifact always against the same version of an assembly.

To summarize, you run into alot of problems when you do what you try to achieve. Why do you actually build your demo project against 2 different versions of the same code? I don't see any usecase for that.

mellinoe commented 5 years ago

It's perfectly fine to reference libraries that depend on different versions of the same library, as long as the application deploys a version that satisfies them all. In this case, deploying the locally-built assembly would be the correct thing to do, because it actually has a higher version than the one from the NuGet package. Of course, it's still possible that the two assemblies are not compatible with each other, and there are ways to shoot yourself in the foot.

@natemcmaster You describe a workaround above where you can lift a transitive PackageReference to a direct reference and then exclude all of its assets. However, in my scenario, this results in a FileNotFoundException when the application tries to load the assembly. Does that approach not work when the transitive reference is acquired through a ProjectReference?

jlanng commented 5 years ago

@mellinoe it sounds like you need an assembly redirect from LibA 4.5.0 to 1.0.0?

I'm not sure how that can be automated though.

mellinoe commented 5 years ago

@jlanng No, I'm not using .NET Framework; assembly redirects are unrelated.

@dsplaisted @natemcmaster Do you have any recommendations for my scenario?

dsplaisted commented 5 years ago

@mellinoe I don't know why it's not copying the right version of LibA to the demo project's output folder. Investigating the contents of the assets file and possibly a binary build log could help understand what's going on. It's possible that the Package Version of LibA.csproj isn't set correctly (to something later than 4.5.0) so NuGet is getting confused about the latest version.

As a workaround, you may be able to put ExcludeAssets="runtime" on the PackageReference from LibB to LibA.