This post continues our knowledge-sharing series, where we look back at topics from 2022. Read our introductory post here:
One of the topics we discussed in our .NET syncs was upgrading to newer versions of .NET and related topics around the impact on projects and their architecture. In this post, we will formulate ten recommendations for approaching this topic.
We have worked with multiple clients to assist them in upgrading their systems to .NET 6. Most of them started from .NET Framework 4.7.x. We have some recommendations to ensure the upgrade process is as efficient, effective, and successful as possible. Note that this is a high-level post without technical details, providing a guidebook to fill in with technical best practices. This way, it also applies to other technology ecosystems. However, these recommendations are mainly for larger projects.
1. Be clear about the reasons and goals for upgrading
Consider factors such as the advantages of .NET 6, developer preferences (we like the new shiny stuff!), the need for new dependencies, addressing tightly-coupled legacy dependencies, and addressing potential security vulnerabilities.
2. Get stakeholder buy-in
Depending on the scope of the upgrade, it will take time and resources, during which the team might not ship any new features. So the stakeholders of the project need to be on board. It needs to make sense from a budget / ROI perspective and align with the product roadmap.
For example, we worked with a client where the development team did the main upgrade during a sprint where the business stakeholders had some time disconnected from the project.
3. Perform impact analysis with Proof of Concepts
Upgrading to .NET 6 can introduce some issues and potentially break legacy dependencies. To assess the upgrade project’s size and scope accurately, conduct Proof of Concepts to identify potential roadblocks, address dependency issues, and inform decision-making. Consider porting your application to .NET Standard to simplify the upgrade process and ensure compatibility with the .NET 6 API set.
4. Make a motivated decision about the version
A shiny new version of the framework and the dependency packages are always on the horizon. Upgrading to very recent versions has benefits (for example, at the time of writing: the increased speed with LINQ in .NET 7), but having to rely on experimental and less-documented upgrades of third-party dependencies might be risky. Of course, this depends on the context of the project. Don’t get sucked into upgrading everything if this can have adverse effects.
5. Keep a clear view of the big picture
Always keep a bird’s-eye view of the software system’s complete architecture. Even if the short-term upgrade changes don’t impact the architecture, it helps set guideposts for the future.
If our upgrade trajectory occurs in a complex multi-service environment, or a colossal monolith, we might need to decide on an approach where components or services are split off, rewritten, converted, and replaced as needed while maintaining the integrity of the whole system.
Utilize tools like Bounded Context Map and C4 Model diagrams for system overviews. Consider concepts like transaction boundaries and eventual consistency to prevent introducing complexity and issues. Favor Hexagonal architecture and ACLs (Anti-Corruption Layer) for restructuring services and enhancing integration points.
6. Decide between breaking- and non-breaking upgrade paths
Depending on the codebase’s size and scope, upgrade approaches may vary. Options include:
- Upgrading everything in place
- Starting with new solutions and moving code while cleaning it up
- Combining techniques when dealing with multiple services
An interesting approach is to follow the Strangler Fig pattern: https://learn.microsoft.com/en-us/azure/architecture/patterns/strangler-fig
Some essential questions to consider:
- Should we perform the conversion all at once or incrementally?
- Will the conversion be a direct one-to-one, or do we plan to clean up legacy code simultaneously? If so, to what extent?
- Is it possible to expedite the process by temporarily accepting short-term pain, such as breaking functionality for faster progress, or is that not an option?
- Can we move (parts of) the codebase to a new solution that better aligns with our target architecture?
- How can we utilize tests, code quality metrics, and other tools to guide our progress and ensure a safety net throughout the process?
- What potential worst-case scenarios should we prepare for, and do we have contingency plans if things don’t go as expected?
The chosen approach can impact the complexity and timelines of the undertaking and the likelihood of introducing bugs.
7. Estimate the effort & scale the dev team accordingly
Ensure that there is enough development power to tackle the project effectively: this could involve temporarily scaling up the team so there are enough resources to focus on the upgrade process and other ongoing development needs.
8. Have clear timelines and expectations around milestones and releases
Beware of allowing the upgrade to take too long, as this can lead to a suboptimal scenario where there are two parallel tracks: one for fixing bugs and developing new features in the “old” codebase while also working on the new one. Of course, this can be an intentional approach in some cases, but at the very least, a plan should be in place that maps out this scenario in advance.
9. Organize a kick-off workshop
Organize a kick-off workshop to discuss the approach, impact, risk analysis, and planning for the project. This approach ensures everyone is on the same page and promotes a proactive approach to tackling potential issues. It’s also essential to avoid complexity and conflicts in the codebase by aligning everyone’s work in the appropriate areas. And last but not least, it gives a clear “go!”.
10. Track upgrade issues & knowledge proactively
Track issues and knowledge during the upgrade process using an issue-tracking or documentation system. There should be an up-to-date overview of potential upgrade issues in the codebase and possible solutions. Implementing fixes should be consistent, and adding bugs or uncontrolled technical debt should be avoided (e.g., commenting out code “for fixing later” and forgetting about it).
After the upgrade, we recommend holding a review or retrospective.
This guide will serve as a high-level guidebook for our own upgrade efforts in the future. We hope it can help others as well.
Are you looking for a sparring partner to discuss your .NET upgrade paths for your project? Contact us!