TL:DR;
Is there a way to resolve a "partial" circular dependency between C# projects using MSBuild wizardry? 🧙‍♂️
Long version
We have a project A that depends on project B. A.csproj looks like this:
<ItemGroup>
<ProjectReference Include="..\projectB\B.csproj" />
</ItemGroup>
Now project B needs a type from Project A. VS prevents us from adding a reference to project A (circular dependency) but it doesn't complain when we add a link to the specific file, like so in B.csproj:
<ItemGroup>
<Compile Include="..\projectA\ClassA.cs" Link="ClassA.cs" />
</ItemGroup>
The problem appears when we use ClassA on project A. Now VS complains (rightfully) about multiple declarations of ClassA (CS0436 - we have set warnings as errors), one from each project.
I am aware that this shows that our design is flawed and as other answers here in SO indicate the right solution would be to refactor. If this is not possible due to time constraints, is there any MSBuild trick that would allow projectA to compile?
For reference, things that I tried and don’t work
In A.csproj: set ReferenceOutputAssembly=false from project B.
=> Would work if project A doesn’t need anything from B.
In B.csproj: include and then remove 🥴
<ItemGroup>
<Compile Include="..\circdependency\ClassA.cs" Link="ClassA.cs" />
<Compile Remove="ClassA.cs" />
</ItemGroup>
In B.csproj: CopyToOutputDirectory=Never
Other options that work but are not ideal:
Suppressing the warning.
Duplicating ClassA in project B and using a different namespace, making it internal etc.
Related
I'm trying to experiment with a specific source generator library and I wanted to try and modify some parts of it. To do that cloned the repository for that library and referenced two .csproj files in it from my own .NET 7 project. This looks at first like it would work, VS Code recognized the imports and doesn't complain, but the actual source generator part does not seem to work.
The specific library I'm using is Mapperly and I referenced it in my .csproj file as following:
<ItemGroup>
<ProjectReference Include="..\mapperly\src\Riok.Mapperly.Abstractions\Riok.Mapperly.Abstractions.csproj" />
<ProjectReference Include="..\mapperly\src\Riok.Mapperly\Riok.Mapperly.csproj" />
</ItemGroup>
The error I get is
Partial method 'MyMapper.MapToReadModel(MyEntity)' must have an implementation part because it has accessibility modifiers.
This code compiles if I add the complete nuget package of this library, and it stops working with this error if I remove the nuget package and reference the projects as I've shown. My understanding of this error would be that it indicated that the source generator either did not generate any source, or the build process doesn't use the generated source for some reason. The way this library works is that you define partial methods for mapping, and the source generator fills in the actual code, so that last step seems to fail in this case.
Obviously the way I'm referencing this library makes it behave differently than what happens when I add the full nuget package. The repository has more .csproj file, but those are all tests or samples. It also has a .sln file, but it seems I can't reference that directly.
I'm also using VS Code while the information I could find on this often assume Visual Studio, not sure if there are any limitations here with VS Code only.
What is the proper way to reference a local library that contains a source generator so that it will work properly in VS Code? Or am I misunderstanding the problem here and the cause is something else entirely?
The library you are referencing is not just regular library but a source generator, you need to reference them in a special way. You can see an example in documentation for source generators. Also note how authors of this library reference their source generator project in their tests:
<!-- For local development, simply reference the projects. -->
<ItemGroup Condition="'$(MapperlyNugetPackageVersion)' == ''">
<ProjectReference Include="..\..\src\Riok.Mapperly\Riok.Mapperly.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\..\src\Riok.Mapperly.Abstractions\Riok.Mapperly.Abstractions.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
</ItemGroup>
I have a Visual Studio 2022 solution, with multiple projects, but four in particular are interesting here.
Provider1 which is based on .NET Framework 4.
Provider2 which is based on .NET 6.
Provider1Test which is based on .NET Framework 4.
Provider2Test which is based on .NET 6.
The Provider1 project has a number of classes, all in the Provider.Data namespace, one of them being Class1. This is my source code. The Provider1.csproj file:
<ItemGroup>
<Compile Include="Class1.cs">
<SubType>Code</SubType>
</Compile>
...
</ItemGroup>
The Class1.cs file:
namespace Provider.Data
{
public class Class1
{
...
}
}
The Provider2 project has links to these source files, i.e. "Add"->"Existing item"->"As link". It compiles with different conditional compilation symbols, so the output is not the same as for the Provider1 project.
The Provider2.csproj file:
<ItemGroup>
<Compile Include="..\Provider1\Class1.cs" Link="Class1.cs" />
</ItemGroup>
The Provider1Test project is an NUnit test project, that tests Provider1. It has multiple test classes, one of them is TestClass1.
The Provider2Test project is also a NUnit test project, with a ProjectReference to Provider2. It links to the test classes in Provider1Test in the same way as the source code does. The Provider2Test.csproj file:
<ItemGroup>
<ProjectReference Include="..\Provider2\Provider2.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Provider1Test\TestClass1.cs" Link="TestClass1.cs" />
</ItemGroup>
The TestClass1.cs file:
using Provider.Data;
namespace ProviderTests
{
public class TestClass1
{
...
}
}
Now, this builds and runs just fine inside Visual Studio, but if I navigate to the Provider2Test folder and try to build with the dotnet build command, it doesn't find the source code.
C:\dev\DataProvider\Provider2Test>dotnet build
MSBuild version 17.3.1+2badb37d1 for .NET
Determining projects to restore...
All projects are up-to-date for restore.
Provider2 -> C:\dev\DataProvider\Provider2\bin\x64\Debug\net6.0\Provider.Data.dll
1 file(s) copied.
C:\dev\DataProvider\Provider1Test\TestClass1.cs(14,7): error CS0246: The type or namespace name 'Provider' could not be found (are you missing a using directive or an assembly reference?) [C:\dev\DataProvider\Provider2Test\Provider2Test.csproj]
Build FAILED.
What is the issue here, why doesn't dotnet build follow the reference path here, and how do I solve it?
I tried to create a TestClass2.cs file directly in Provider2Test, that is not a link but a standard compile include, and also using the Provider.Data namespace. It produces the same error.
I found a workaround, so I'm posting it here and I'm going with it for now, but I don't think it's a good solution, and it doesn't explain the original issue, so I'm not going to mark this as the accepted answer.
In Provider2.csproj, I added that if it is built with dotnet build, it has a post-build event that copies its source code dll to Provider2Test. This is not run if the project is build within Visual Studio ("$(MSBuildRuntimeType)" == "Full").
if "$(MSBuildRuntimeType)" == "Core" XCOPY "$(OutDir)Provider.Data.dll" "$(ProjectDir)..\Provider2Test\$(OutDir)" /Y /F
In Provider2Test.csproj I added a conditional assembly reference.
<ItemGroup>
<Reference Include="Provider.Data" Condition="$(MSBuildRuntimeType) == 'Core'">
<HintPath>$(OutDir)Provider.Data.dll</HintPath>
</Reference>
</ItemGroup>
I kept the ProjectReference in all cases (both "Full" and "Core"), in order to trigger a Provider2 build whenever Provider2Test is built.
I have a library that relies on a source generator to work correctly. In the MyLibrary.csproj, I reference the generator like so.
<ItemGroup>
<ProjectReference
Include="..\MyLibrary.Generators\MyLibrary.Generators.csproj"
PrivateAssets="contentfiles;build"
ReferenceOutputAssembly="false"
OutputItemType="analyzer"/>
</ItemGroup>
I need this analyzer reference to be transitive, i.e. projects that reference MyLibrary should get the MyLibrary.Generators analyzer transitively.
A simple reference like so does not seem to reference the analyzer, only MyLibrary
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
I want to stress that I am not looking for MyLibrary.Generators to be consumed as a regular assembly reference, but as a Roslyn Analyzer so my source generator can run as intended during compile time.
I have received an answer to this question on GitHub.
It is sufficient for the library project to have the following reference to the generator project:
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Remove="$(OutputPath)/$(AssemblyName).Generator.dll" />
<None Include="$(OutputPath)/$(AssemblyName).Generator.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
(I am not sure if the Remove line does anything. It might be unnecessary.)
When the library is consumed as a NuGet package, the generator is automatically included as an analyzer, i.e. the analyzer reference is transitive.
However, when the library is consumed through a project reference (such as in the library's own unit test project), we need the following:
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<ProjectReference Include="..\MyLibrary.Generator\MyLibrary.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
The examples in the original post all handle project references. From what I have gathered, a transitive project reference to an analyzer seems impossible, unfortunately.
Is it possible to exclude auto-generated files when importing classes from one C# project to another?
I have one project that implements a GRPC service based on classes generated from the proto file.
Also, I have another project where I test classes of the first project like GRPC service. In the second project, I implemented a GRPC client to test the GRPC service with the same copy of the proto file.
The problem is that I import all classes of the first project and sorrowfully some of them are generated with the same proto file and there are a lot of warnings about class names collisions.
Is there a way to import only specific files between projects of the same solution?
I use the next directives in project files to generate classes:
In the first project:
<ItemGroup>
<Protobuf Include="Protos\Proto.proto" GrpcServices="Server" />
</ItemGroup>
In the second one:
<ItemGroup>
<Protobuf Include="Protos\Proto.proto" GrpcServices="Client" />
</ItemGroup>
And I import classes of the first project to the second one like this:
<ItemGroup>
<ProjectReference Include="..\core\First.csproj" />
</ItemGroup>
And the warnings are like this:
TestClient.cs(53,29): warning CS0436: The type 'Type1' in '/folder1/solution/test_project/obj/Debug/netcoreapp3.1/Class1.cs' conflicts with the imported type 'Type1' in 'Project1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in '/folder1/solutiion/test/obj/Debug/netcoreapp3.1/Class1.cs'. [/folder1/solution/test_project/Project2.csproj]
I usually create my proto files and their resulting generated code in a single assembly, then reference that assembly from the service and the client assemblies. This keeps the code generated once, and eliminates the duplicated code generation.
You can't conditionally import the types, but you can add an extern alias to the types in your First project when importing it to the second project.
Add the following snippet to your second .csproj
<Target Name="ChangeAliasesOfStrongNameAssemblies" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
<ItemGroup>
<ReferencePath Condition="'%(FileName)' == 'First'">
<Aliases>FirstExtern</Aliases>
</ReferencePath>
</ItemGroup>
</Target>
you can now explicitly define which type you want to use:
add this above all the usings in your .cs code:
extern alias FirstExtern;
and reference the types from the first project by specifying the defined alias:
FirstExtern::First.Type1 variable;
Suppose I have two versions of an assembly with the exact same public interfaces, but slightly different implementations. I reference one version during development to compile against. During my program’s startup, I would like somehow to select which one of these two assemblies to run against.
Is there a way to do this not using reflection and that is compatible with all .NET Standard 2.1 implementations?
Assembles are not loaded before first use - so as long as you copy the version you like to location where it will be loaded from before any types from that assembly are used it will be loaded.
Indeed using reflection (Assembly.Load/LoadFrom) is more usual solution, but it has exactly the same restriction - you must do that before any type is used, otherwise default loading logic will pick whatever is available (or simply fail).
I would try to use conditions in csproj.
<ItemGroup Condition="'$(CONFIG)'=='DEBUG'>
<ProjectReference Include="{PATH_TO_DEV_PROJECT}">
...
</ProjectReference>
</ItemGroup>
<ItemGroup Condition="'$(CONFIG)'!='DEBUG'>
<Reference Include="{PATH_TO_RELEASE_REFERENCE}" />
</ItemGroup>
That might help to copy correct assembly. You may use <PackageReference>...</PackageReference> too.