(Print this page)

Finding conflicting references in your solution
Published date: Wednesday, October 7, 2020
On: Moer and Éric Moreau's web site

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 Try
End 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 Class

End 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!


(Print this page)