Lately I had serious issues. One of my solutions was refusing to compile after I upgraded one of the libraries I am using to its latest version. The compiler was complaining about conflicting references. It was not easy to find since my solution contains 28 projects and the error message returned by the compiler was not explicit on which of the many components was throwing the issue nor for which project.
I needed to find a solution!
Available source code
As most of the time, both VB and C# projects are provided this month. The solution was created using Visual Studio 2019 but should also work in earlier versions.
Figure 1: The demo application in action
What does your favorite search engine have to say?
I spent quite sometimes to look for an answer to my problem.
Most of the time, the solution was to pick a more verbose level for the MSBuild project but with 28 projects in my solutions (and a lot of references in each one), because I tried, I was just bloated with too many details leading nowhere!
Figure 2: MSBuild verbosity
Found code on GitHub
I have searched the Internet to find something other than the verbosity and found a class provided by Brian Low on GitHub.
The class is in the form of a Unit Test. I have used it almost as is to fix this specific issue. But because all the developers in the solution are not using the same path to save the code locally, it was a bit tough to use the unit test by more than one developer.
So, I have decided to build a reusable standalone tool from Brian’s code to expose the results of the various references in use through a solution. It is also easier to copy the output from my little project instead of from one of the Visual Studio output boxes.
Building the UI
As you can see in figure 1, the UI is simple as always.
The first textbox is for you to provide the path of the solution (the folder containing all the various projects).
In the second textbox, you need to provide the part of the path where the compiled assemblies will be found. Usually, it will be something like bin\debug or bin\release.
When the checkbox is checked, only references with possible conflicts will be shown. When it is not checked, all the references are shown. This is useful to leave it uncheck when there are no conflicts (otherwise the results are almost empty).
When you push the button, the first folder provided will be inspected to find all your projects, and in these projects, the second folder will be used to find the various assemblies you have. The big textbox at the bottom will show the results found. This textbox is using a fixed font so that everything is properly aligned. Scrollbars are also available for you to scroll through the results.
The code
There are 2 portions of code to show.
The first one is the code behind my form which after having validated that the 2 textboxes have some values, call the FindReferences method of the class (available next) and display the result in the textbox at the bottom of the form.
Private Sub btnSearchReferences_Click(sender As Object, e As EventArgs) Handles btnSearchReferences.Click Dim strSolutionFolder As String = txtSolutionFolder.Text If (String.IsNullOrWhiteSpace(strSolutionFolder) OrElse (Not IO.Directory.Exists(strSolutionFolder))) Then txtResults.Text = "Solution folder does not exist. Please provide a valid one!" Return End If Dim strBinFolder As String = txtBinFolder.Text If (String.IsNullOrWhiteSpace(strSolutionFolder) OrElse (Not IO.Directory.Exists(strSolutionFolder))) Then txtResults.Text = "Bin folder is empty. Please provide a valid one!" Return End If Try Cursor.Current = Cursors.WaitCursor btnSearchReferences.Enabled = False txtResults.Text = "Searching for references..." Application.DoEvents() Dim strResults As String = cConflictingReferences.FindReferences(strSolutionFolder, strBinFolder, chkOnlyConflicting.Checked) txtResults.Text = strResults Catch ex As Exception txtResults.Text = $"An exception occured: {ex}" Finally btnSearchReferences.Enabled = True Cursor.Current = Cursors.Default End TryEnd Sub
private void btnSearchReferences_Click(object sender, EventArgs e){ string strSolutionFolder = txtSolutionFolder.Text; if (string.IsNullOrWhiteSpace(strSolutionFolder) || !System.IO.Directory.Exists(strSolutionFolder)) { txtResults.Text = "Solution folder does not exist. Please provide a valid one!"; return; } string strBinFolder = txtBinFolder.Text; if (string.IsNullOrWhiteSpace(strSolutionFolder) || !System.IO.Directory.Exists(strSolutionFolder)) { txtResults.Text = "Bin folder is empty. Please provide a valid one!"; return; } try { Cursor.Current = Cursors.WaitCursor; btnSearchReferences.Enabled = false; txtResults.Text = "Searching for references..."; Application.DoEvents(); string strResults = cConflictingReferences.FindReferences(strSolutionFolder, strBinFolder, chkOnlyConflicting.Checked); txtResults.Text = strResults; } catch (Exception exception) { txtResults.Text = $"An exception occured: {exception}"; } finally { btnSearchReferences.Enabled = true; Cursor.Current = Cursors.Default; }}
The real code can be found in the cConflictingReferences class. I would say that the bulk of the code is from Brian (GitHub). I made it a bit more generic.
Public Class cConflictingReferences ' Reference https://gist.github.com/brianlow/1553265 Public Shared Function FindReferences(ByVal pSolutionFolder As String, ByVal pBinFolder As String, ByVal pOnlyConflicting As Boolean) As String Dim strResults As New StringBuilder(10000) Dim directories As String() = Directory.GetDirectories(pSolutionFolder) Dim projectFolders As IEnumerable(Of String) = directories.Where(Function(x) x.Contains("")) For Each folder As String In projectFolders Dim targetFolder As String = $"{folder}{pBinFolder}" If Directory.Exists(targetFolder) Then Dim assemblies As IEnumerable(Of Assembly) = GetAllAssemblies(targetFolder) Dim references As IEnumerable(Of Reference) = GetReferencesFromAllAssemblies(assemblies) Dim groupsOfConflicts = If(pOnlyConflicting, FindReferencesWithTheSameShortNameButDifferentFullNames(references), FindAllReferences(references)) Dim sectionHeader As String = $"-----{folder}-----" strResults.AppendLine(sectionHeader) For Each group As IGrouping(Of String, Reference) In groupsOfConflicts Dim padding As Integer = group.Max(Function(x) x.ToString().Length) strResults.AppendLine($"Assemblies referencing {group.Key}") For Each reference As Reference In group strResults.AppendLine($" {reference.Assembly.Name.PadRight(padding)} references {reference.ReferencedAssembly.FullName}") Next Next strResults.AppendLine(String.Concat(Enumerable.Repeat("-", sectionHeader.Length)) & vbLf & vbLf) End If Next Return strResults.ToString() End Function Private Shared Function FindReferencesWithTheSameShortNameButDifferentFullNames(ByVal pReferences As IEnumerable(Of Reference)) As IEnumerable(Of IGrouping(Of String, Reference)) Return pReferences. GroupBy(Function(r) r.ReferencedAssembly.Name). Where(Function(r) r.ToList().Select(Function(s) s.ReferencedAssembly.FullName).Distinct().Count() > 1) End Function Private Shared Function FindAllReferences(ByVal pReferences As IEnumerable(Of Reference)) As IEnumerable(Of IGrouping(Of String, Reference)) Return pReferences. GroupBy(Function(r) r.ReferencedAssembly.Name) End Function Private Shared Function GetReferencesFromAllAssemblies(ByVal pAssemblies As IEnumerable(Of Assembly)) As IEnumerable(Of Reference) Dim references As New List(Of Reference)() For Each assembly In pAssemblies For Each referencedAssembly In assembly.GetReferencedAssemblies() references.Add(New Reference With { .Assembly = assembly.GetName(), .ReferencedAssembly = referencedAssembly }) Next Next Return references End Function Private Shared Function GetAllAssemblies(ByVal pPath As String) As IEnumerable(Of Assembly) Dim files As New List(Of FileInfo)() Dim directoryToSearch As New DirectoryInfo(pPath) files.AddRange(directoryToSearch.GetFiles("*.dll", SearchOption.AllDirectories)) files.AddRange(directoryToSearch.GetFiles("*.exe", SearchOption.AllDirectories)) Return files.ConvertAll(Function(file) Assembly.LoadFile(file.FullName)) End Function Private Class Reference Public Property Assembly As AssemblyName Public Property ReferencedAssembly As AssemblyName End ClassEnd Class
public class cConflictingReferences{ public static string FindReferences(string pSolutionFolder, string pBinFolder, bool pOnlyConflicting) { StringBuilder strResults = new StringBuilder(10000); string[] directories = Directory.GetDirectories(pSolutionFolder); IEnumerable<string> projectFolders = directories.Where(x => x.Contains("")); foreach (string folder in projectFolders) { string targetFolder = $"{folder}{pBinFolder}"; if (Directory.Exists(targetFolder)) { IEnumerable<Assembly> assemblies = GetAllAssemblies(targetFolder); IEnumerable<Reference> references = GetReferencesFromAllAssemblies(assemblies); var groupsOfConflicts = pOnlyConflicting ? FindReferencesWithTheSameShortNameButDifferentFullNames(references) : FindAllReferences(references); string sectionHeader = $"-----{folder}-----"; strResults.AppendLine(sectionHeader); foreach (IGrouping<string, Reference> group in groupsOfConflicts) { int padding = group.Max(x => x.ToString().Length); strResults.AppendLine($"Assemblies referencing {group.Key}"); foreach (Reference reference in group) { strResults.AppendLine($"\t{reference.Assembly.Name.PadRight(padding)} references {reference.ReferencedAssembly.FullName}"); } } strResults.AppendLine(string.Concat(Enumerable.Repeat("-", sectionHeader.Length)) + "\n\n"); } } return strResults.ToString(); } private static IEnumerable<IGrouping<string, Reference>> FindReferencesWithTheSameShortNameButDifferentFullNames(IEnumerable<Reference> pReferences) { return from reference in pReferences group reference by reference.ReferencedAssembly.Name into referenceGroup where referenceGroup.ToList().Select(reference => reference.ReferencedAssembly.FullName).Distinct().Count() > 1 select referenceGroup; } private static IEnumerable<IGrouping<string, Reference>> FindAllReferences(IEnumerable<Reference> pReferences) { return from reference in pReferences group reference by reference.ReferencedAssembly.Name into referenceGroup select referenceGroup; } private static IEnumerable<Reference> GetReferencesFromAllAssemblies(IEnumerable<Assembly> pAssemblies) { List<Reference> references = new List<Reference>(); foreach (var assembly in pAssemblies) { foreach (var referencedAssembly in assembly.GetReferencedAssemblies()) { references.Add(new Reference { Assembly = assembly.GetName(), ReferencedAssembly = referencedAssembly }); } } return references; } private static IEnumerable<Assembly> GetAllAssemblies(string pPath) { List<FileInfo> files = new List<FileInfo>(); DirectoryInfo directoryToSearch = new DirectoryInfo(pPath); files.AddRange(directoryToSearch.GetFiles("*.dll", SearchOption.AllDirectories)); files.AddRange(directoryToSearch.GetFiles("*.exe", SearchOption.AllDirectories)); return files.ConvertAll(file => Assembly.LoadFile(file.FullName)); } private class Reference { public AssemblyName Assembly { get; set; } public AssemblyName ReferencedAssembly { get; set; } }}
Conclusion
It turned out to be that one of the projects did not have the references properly updated. Without something like this, I could probably still be searching for the issue!
While testing, I also found that this tool can also inspect WPF solutions. Turns out to be an unbelievably valuable to keep in your toolbox!