We have a couple of projects that make use of a shared common library (internal, not public). (C#.net projects, using Visual Studio, setup as solutions)
Solution A ---|
|----> Common Library (Nuget Package)
Solution B ---|
This works fine in general. The common library is published to an internal nuget feed which can then be consumed by both projects, both when running locally, and on the build servers.
The problem is when we want to make changes to the common library. It seems the only way to test the changes is to rebuild the common library, publish the updated package to the feed, upgrade the packages in Project A (And as there are lots of projects within the solution it takes a while) and then test. This cycle takes a very long time.
What I'm looking for ideally is a way to be able to include the common library projects in the same solution as Project A (or B), and make the changes to the library and then run and debug the code all as one solution. But doing that takes ages. It requires going through all of the csproj files in Project A and changes all the references to project ones rather than package references. I've tried using conditions in the csproj files to make it easy to switch between using the local projects, and using the nuget packages for the build, but it doesn't seem to be effective. Visual studio just seems to get confused over the references and the builds fail.
Are there any alternatives? I feel like this should be a fairly common situation, so is there an accepted typical way of setting this up so it's possible to both use a nuget package for the common library, but also be able to connect them together and debug easily.
In response to @vernou, this is something we tried to setup with conditional properties in the csproj files. We setup a configuration type to control which reference was used.
<ItemGroup Condition=" '$(Configuration)' == 'DebugWithLocalCommon' ">
<ProjectReference Include="$(CommonPath)Common.Project.Name\Common.Project.Name.csproj">
<Project>{11111111-1111-1111-1111-111111111111}</Project>
<Name>Common.Project.Name</Name>
</ProjectReference>
...
Multiple project references continue here
...
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'DebugWithLocalCommon' ">
<Reference Include="Common.Project.Name, Version=4.0.26.0, Culture=neutral, PublicKeyToken=1111111111111111, processorArchitecture=MSIL">
<HintPath>..\packages\Common.Project.Name.4.0.26\lib\netstandard2.0\Common.Project.Name.dll</HintPath>
</Reference>
...
Multiple package references continue here
...
</ItemGroup>
We actually have some projects that use the classic csproj reference format (as above), and some that use the newer 'package reference' format for referencing nuget packages. In those cases the second block would more like this:
<ItemGroup Condition=" '$(Configuration)' != 'DebugWithLocalCommon' ">
<PackageReference Include="Common.Project.Name">
<Version>4.0.26</Version>
</PackageReference>
...
Multiple package references continue here
...
</ItemGroup>
The problems we had with this is that it seems to confuse visual studio. Sometimes multiple references should up in the UI (which is perhaps just annoying rather than a total deal breaker), but sometimes it also seems to fail to understand the reference entirely and refuses to build or recognize that the package is being referenced and just gives "You may be missing a reference" type errors.
We've also tried the same using within the csproj, but the resultant behaviour was exactly the same.
CodePudding user response:
This is a challenge and the solution is quite complicated. It is also dependent on how your source control and development environment is set up. But there is a potential solution which I can explain.
In brief, you need a custom tool, which can just be a command line exe project, that will update the references in the csproj files without the need for all the manual changes.
Dev Environment set-up:
You will find that developers, as individuals, may all clone source to different locations on their machine. So each person has their code in a different place. This is ok as long as everyone has the same structure within a main folder so that the references can be added using a relative location.
EG, if everyone uses:
C:\Source\Common
C:\Source\App1\
Or:
C:\Code\Common
C:\Code\App1\
That's fine, but if the Common folder is called 'cmn' on someones machine, it will be a problem for them.
So now, in VS you can have a project reference in App1 to your Common project using a relative location, EG: ..\Common\Common.csproj
Project Type Issues:
You'll need to consider what type of projects you have. If you create a new project in Visual Studio and use .Net Framework 4.xx for example, you'll have what is now known as the 'old' project type. If you create with .Net Standard or .Net Core, you'll get the new project type (which is much nicer especially for doing all this nuget stuff).
The old project type has all nuget packages referenced in a packages.config for each project. It also has a reference to the actual project including exact path and version in the csproj. This is what makes the process more cumbersome.
The new project type has only a reference to the nuget packages in the csproj file (super duper).
But it doesn't matter if you still have the old project type, it is still possible for a custom tool to handle this.
The Tool:
This is just the basics, so not a full solution but hopefully you can get enough from this to implement.
Ensure you test this on a copy, or start a new branch first to ensure nothing is permanently broken. This will only cater for the old style project which I assume you're using.
Create a new Console Application project and add some basic classes for handling files:
PackageConfig.cs
[Serializable]
[DesignerCategory("code")]
[XmlType(AnonymousType = true, IncludeInSchema = true)]
[XmlRoot(ElementName = "packages", IsNullable = false)]
public class PackageConfig
{
[XmlElement("package")]
public Package[] Packages { get; set; }
}
[Serializable]
[DesignerCategory("code")]
public class Package
{
[XmlAttribute("id")]
public string Id { get; set; }
[XmlAttribute("version")]
public string Version { get; set; }
[XmlAttribute("targetFramework")]
public string TargetFramework { get; set; }
}
XmlSerializer.cx
class XmlSerializer
{
private System.Xml.Serialization.XmlSerializer GetSerializer<T>()
{
return new System.Xml.Serialization.XmlSerializer(typeof(T));
}
public string Serialize<T>(T instance, bool omitXmlDeclaration = false)
{
System.Xml.Serialization.XmlSerializer xmlSerializer = GetSerializer<T>();
StringBuilder stringBuilder = new StringBuilder();
XmlWriterSettings settings = new XmlWriterSettings
{
OmitXmlDeclaration = omitXmlDeclaration,
Encoding = Encoding.UTF8
};
using (XmlWriter writer = XmlWriter.Create(new StringWriter(stringBuilder), settings))
{
xmlSerializer.Serialize(writer, instance);
}
return stringBuilder.ToString();
}
public T Deserialize<T>(string xml)
{
System.Xml.Serialization.XmlSerializer xmlSerializer = GetSerializer<T>();
using (XmlTextReader reader = new XmlTextReader(new StringReader(xml)))
return (T)xmlSerializer.Deserialize(reader);
}
}
Update your program.cs as:
class Program
{
static void Main(string[] args)
{
try
{
string packageVersion = null;
if (args != null && args.length == 1)
packageVersion = args[0];
UpdateProjects(packageVersion);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString());
}
}
const string _packageId = "Common";
delegate string UpdateContent(string content);
static void UpdateProjects(string packageVersion)
{
if (packageVersion == null)
UseLocalReferences();
else
UsePackageReferences(packageVersion);
}
static void UseLocalReferences()
{
RemoveNugetPackage();
ProcessProjects(content =>
{
content = RemoveReference(content);
content = AddReference(content, null);
return content;
});
}
static void UsePackageReferences(string packageVersion)
{
AddNugetPackage(packageVersion);
ProcessProjects(content =>
{
content = RemoveReference(content);
content = AddReference(content, packageVersion);
return content;
});
}
static void ProcessProjects(UpdateContent update)
{
string folder = Environment.CurrentDirectory;
string[] files = Directory.GetFiles(folder, "*.csproj", SearchOption.AllDirectories);
foreach (string fileName in files)
{
try
{
string content = File.ReadAllText(fileName);
content = update(content);
using (StreamWriter writer = new StreamWriter(fileName))
{
writer.Write(content);
}
Console.WriteLine("Updated: " fileName);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error updating {fileName}:");
Console.Error.WriteLine(ex.ToString());
}
}
}
static void RemoveNugetPackage()
{
string folder = Environment.CurrentDirectory;
Console.WriteLine($"Updating all packages.config files under {folder}");
XmlSerializer xmlSerializer = new XmlSerializer();
string[] packageConfigFiles = Directory.GetFiles(folder, "packages.config", SearchOption.AllDirectories);
foreach (string packageConfigFile in packageConfigFiles)
{
PackageConfig packageConfig = xmlSerializer.Deserialize<PackageConfig>(File.ReadAllText(packageConfigFile));
List<Package> packages = new List<Package>(packageConfig.Packages);
if (packages.Any(x => x.Id == _packageId))
packages.First(x => x.Id == _packageId).Version = packageVersion;
else
packages.Add(new Package { Id = _packageId, Version = packageVersion, TargetFramework = "net462" }); // may need to change the framework here
packages.Sort((x, y) => string.Compare(x.Id, y.Id));
packageConfig.Packages = packages.ToArray();
File.WriteAllText(packageConfigFile, xmlSerializer.Serialize(packageConfig));
Console.WriteLine($"{packageConfigFile} updated");
}
Console.WriteLine("Update of packages.config files complete");
}
static void AddNugetPackage(string packageVersion)
{
string folder = Environment.CurrentDirectory;
Console.WriteLine($"Updating all packages.config files under {folder}");
XmlSerializer xmlSerializer = new XmlSerializer();
string[] packageConfigFiles = Directory.GetFiles(folder, "packages.config", SearchOption.AllDirectories);
foreach (string packageConfigFile in packageConfigFiles)
{
PackageConfig packageConfig = xmlSerializer.Deserialize<PackageConfig>(File.ReadAllText(packageConfigFile));
List<Package> packages = new List<Package>(packageConfig.Packages);
Package package = packages.FirstOrDefault(x => x.Id == _packageId);
if (package != null)
packages.Remove(package);
packageConfig.Packages = packages.ToArray();
File.WriteAllText(packageConfigFile, xmlSerializer.Serialize(packageConfig));
Console.WriteLine($"{packageConfigFile} updated");
}
Console.WriteLine("Update of packages.config files complete");
}
static string RemoveReference(string content)
{
string[] lines = content.Split(new string[] { "\r\n" }, StringSplitOptions.None);
StringBuilder sb = new StringBuilder();
bool removing = false;
foreach (string line in lines)
{
if (!removing && line.Trim().StartsWith($"<Reference Include=\"{_packageId}\""))
removing = true;
else if (removing)
{
if (line.Trim() == "</Reference>")
removing = false;
}
else
sb.AppendLine(line);
}
return sb.ToString();
}
static string AddReference(string content, string packageVersion)
{
string[] lines = content.Split(new string[] { "\r\n" }, StringSplitOptions.None);
StringBuilder sb = new StringBuilder();
foreach (string line in lines)
{
if (line.Trim() == "<Reference Include=\"System\" />") // find where the references need to be inserted
{
if (packageVersion == null)
{
sb.AppendLine($" <Reference Include=\"{_packageId}\">");
sb.AppendLine($" <SpecificVersion>False</SpecificVersion>");
sb.AppendLine($" <HintPath>..\\Common\\bin\\$(Configuration)\\{_packageId}.dll</HintPath>");
sb.AppendLine($" </Reference>");
}
else
{
sb.AppendLine($" <Reference Include=\"{_packageId}, Version = {packageVersion}, Culture = neutral, processorArchitecture = MSIL\">");
sb.AppendLine($" <HintPath>..\\packages\\{_packageId}.{packageVersion}\\lib\\net462\\{_packageId}.dll</HintPath>"); // again, framework may need updating
sb.AppendLine($" </Reference>");
}
}
sb.AppendLine(line);
}
return sb.ToString();
}
}
Using the tool:
When you compile the tool, copy the exe to your main project folder, EG C:\Source\App1 - then you can run it using:
updatetool
(to set the local references)
or
updatetool 1.0.1
(to set to the nuget package and references)
This is all untested but based on a solution I use for the same!
CodePudding user response:
If you use git, maybe git-submodule.
I have a similar case. I develop a custom provider to Entity Framework Core and to help the development, I debug with the EF Core code source.
The csproj look like :
<Project Sdk="Microsoft.NET.Sdk">
...
<ItemGroup Condition=" '$(Configuration)'!='Debug' ">
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[3.1,4)" />
</ItemGroup>
<ItemGroup Condition=" '$(Configuration)'=='Debug' ">
<ProjectReference Include="..\efcore\src\EFCore.Relational\EFCore.Relational.csproj" PrivateAssets="contentfiles;build" />
</ItemGroup>
</Project>
When I start my project in Debug, I can also debug the EF Core code source, but when I start in Release, the NuGet package is used and I can't access the EF Core code source.
Bonus with git :
We use git to versioning our code source. If my colleague get the code source and try to debug, this fail because EF Core is absent.
Then I added EF Core as submodule in our git repository :
git submodule add https://github.com/dotnet/efcore.git
# Load a specific version
cd efcore
git checkout v3.1.18
cd ../
git commit -m "Add the submodule efcore"
Now, when my colleague clone our provider repository, the EF Core code source is automatically loaded (just need to check the case 'load submodule' when you clone from Visual Studio).