Within this tutorial, you will learn all about dependency hell, circular dependencies and how to avoid them within .NET 7. Let us first start this conversation with a quick fun story that highlights a potential risk that you may encounter with using a third-party dependency.
This story is about how Trump's donation website could have potentially been hacked in 2016 by referencing issues. This is a picture of what the Trump campaign website looked like at the time:
This is a screenshot of the source code:
The site was referencing an external dependency called jQuery-Mask-Plugin by Igor Escobar. The issue with this code is that it directly references a file in GitHub. To potentially hack the Trump website at the time, all someone would have to do is submit a malicious PR here and if the PR was accepted an attack could have been made on Trumps sites within minutes. If you want to learn more about that story read here.
I share this story to simply highlight that things aren't all plain sailing when we use dependencies. As the software industry evolves, our applications are becoming increasingly reliant on dependencies.
The consequence of this transition within the .NET world translates into an ever-increasing number of errors due to clashes and conflicts between those dependencies. So how can we strategically solve these issues?
When it comes to thinking about potential frictions with dependencies, there are several different types of relationships that you might need to consider:
- Internal references, where the code we can control links to other dependencies within our control
- External references, using dependencies created by a third party
- Direct references: Libraries or packages your code calls directly.
- Transitive references: Transitive references are dependencies of dependencies. These are libraries or packages that are referenced by your direct references
When it comes to solving a dependency issue, the relationship type will likely determine the severity and the scale of the issue. There are three different types of dependency-related conflicts that you may encounter within your site:
Circular dependencies: A circular dependency will occur when package 1 depends on some code within package 2. In parallel, package 2 depends on some code located within package 1
Dependency conflicts: A conflict can occur when two packages reference the same dependency, however, each reference targets a different version of the dependency.
Diamond dependencies: Within .NET, it is pretty common for the dependency tree to contain multiple references to the same package. Let us say that you import two NuGet packages, each package reference the same package, however, they both target versions. This is a diamond dependency. Within .NET I only tend to encounter diamond deputies when using NuGet.
Microsoft has a useful blog post on this topic which can be found here, however, in summary, avoid using NuGet packages that do not define a minimum version, avoid packages that demand an exact version, and finally, avoid packages with an upper version limit.
Tips to solve dependency conflicts
Binding Redirects: Within the .NET world dependency conflicts with anything you pull in via NuGet can be solved using binding redirects.
Avoid the problem: Whenever you bump into an issue the most common solution is to use some common sense. Before spending potentially hours and sometimes even days trying to solve an issue, first, consider if you really need to do what you are trying to do.
In many dependency conflicts that I've personally encountered, simply tidying up the code, or, using another similar package can often completely avoid the problem. Sometimes the most obvious solution isn't the easiest option to spot as your mindset is so nested with fixing what is right in front of you!
Remove Unused Dependencies: Whenever you encounter an issue, first figure out what causes that conflict and more importantly, double-check that the code or dependency that is causing the conflict is still being used. If it's not, delete it. Problem solved!
The good news is that despite dependencies being unavoidable in most situations, there are ways to mitigate the risk of potential dependency hell. Whenever you can minimize the number of dependencies your code references, either by refactoring the code or deleting unused dependencies, your code will be safer. Sometimes a conflict can occur within a package referenced by code that is no longer used. Instead of trying to fix needed issues, simply deleting some code can fix it! Other actions that you can take include:
Tips to resolve circular dependencies
Let's say you have bumped into a circular dependency issue, what are the options?
Create a new library to break the reference chain: When you get a conflict, the easiest way to solve the conflict is to create a third-party package. Package 1 and 2 can then both reference package 3 and the circular dependency is fixed.
Interfaces: Instead of having code that concrete classes directly, a better approach is to write your classes to interfaces. You can then add your interfaces within a third class library, you could simply put the shared interfaces within their own class library. Using dependency injection, you can then define which concrete classes will be created on each request
Good practice tips
Finally, before we part ways, let us consider some good practice tips and recommendations that you can ally to attempt to get yourself out of trouble:
- Always avoid using deprecated code
- Follow semantic versioning
- Use automation to patch your dependencies
- Regularly update your dependencies up-to-date
- Define a Software Dependency Management Policy
- Use a static code analysis tool to understand your dependencies graph
Avoiding dependency issues within your software career is unavoidable, however, armed with the correct strategies it shouldn't be too painful to get out of dependency hell 🔥🔥🔥
If you want some further resources, check out these links:
Be sure to follow the tips in this guide to minimize the risks and keep your software protected. Happy Coding 🤘