We all love to build greenfield projects, but the reality is that in most jobs you have to deal with a lot of legacy code. This doesn't mean the code is bad. It just means that choices were made that were the right ones at that time, or that the developers were not entire up-to-date with modern development practices. And that's exactly what this talk is about. I enjoy taking such a codebase and gradually introduce architectural seems, add a proper build pipeline, introduce temporary tests and then gradually refactor the codebase to combine more maintainable, testable and reliable. So in this talk, I'd like to unfold my extensive toolbox of practices, principles, tools and mindset to help you improve your legacy code without jeopardizing your business.
Understand production environment
See which vinaries are running in production and what config files are used
Verify the code and the production release are in sync, e.g. DotPeek
Analyze logs to understand what features are used
Try to get it to run on a local environment
Understand code
Generate project diagrams and class hierarchies
Find origin calls and destination of a parameter using Rider
Use navigation options to find inheritors, usages, etc.
Use the namespaces and project names to understand the relation
Have Rider's AI Assistant or ChatGPT explain the code
Track all documentation in a single place
Build C4 diagrams
Find dead code
Reduce the scope of types and members to internal to find more dead code
Detect code inefficiencies and dead code using Rider
Remove commented out code
Build a safety net
If therea are tests, add code coverage reporting using Coverlet and use that to find dark spots
(HTTP API) Characteristics tests using FluentAssertions, Bogus, xUnit. Use AI to generate them. It's fine, you'll delete them later on
Use test containers for .net t lo include the databse
Build a couple of end to end UI tests e.g. using Cypress, Playwright, efc
Improve deployability
Add scripts or builds steps to run the code locally, Nuke build pipeline to get consistency
Adopt a version strategy and name branches accordingly
Adopt automatic versioning using GitVersion
Adopt logging and exception strategy, e.g. Serilog
Pulumi to automate PR deployments
Improve code
Treat all warnings as errors and fix thhem
Editorconfig / eslint so auto-format works
Configure a personal Rider/R# clean-up profile to auto-format file, project and solution
Roslyn analyzers
Microsoft.CodeAnalysis.BannedApiAnalyzers
StyleCop.Analyzers
CSharpGuidelinesAnalyzer
Roslynator.Analyzers
Meziantou.Analyzer
Enable nullable types per file
Move new code to .NET 6+ or cross-compile
Switch to newer C# versions, possibly using Polysharp
File-scope namespaces
Target-typed news
switch expression
String interpolation instead of string.format
Use Directory.Build.Props to reduce duplication
Improve naming whenever you can
Encapsulate prim itive types and collections in d
Kotlin Multiplatform & Compose Multiplatform - Starter kit for pragmatics
Tools and practices to help you deal with legacy code
1. tools, practices and principles
My recipe for dealing with legacy code
Dennis Doomen
@ddoomen | The Continuous Improver | Aviva Solutions
2. About Me
Hands-on architect in the .NET space with 27 years of experience on
an everlasting quest for knowledge to build the right software the right
way at the right time
@ddoomen | The Continuous Improver | Aviva Solutions
3.
4. Win a 1-year license for
Jetbrains Rider
twitter: ddoomen
mastodon: @ddoomen.mastodon.social
bluesky: @ddoomen.bsky.social
6. My typical improvement
flow
Understand the
production
environment
Understand the
code base
Find dead and
unused code
Build a safety
net
Improve
deployability
Improve code
quality
Improve code
design
Improve
architecture
39. Move to .NET 6+ and bump C#
version
• file-scoped namespaces
• global usings
• target-type new
• switch expression
• string interpolation
• raw strings
• pattern matching
• or operator
46. Apply DRY within those “boundaries”
Duplicated
Service 1
Duplicated
Service 1
Duplicated
Service 2
Duplicated
Service 2
Duplicated
Service 1
Centralized
Service 3
Extension
Methods
Extension
Methods
Extension
Methods
Extension
Methods
Helpers Helpers Helpers Helpers
48. Avoid technical folders and organize code by
functionalities / capabilities
Functional
Folders
“Unit” of (integration)
testing
DRY within boundaries
Can be used to
clarify “public”
parts
Only unit tested
for specific
reasons
Role-based
interface name
51. Adopt an architecture
style that embraces the
DIP
Order Processing
IStoreOrders<T>
+ Query<T>();
+ Add<T>();
+ Delete<T>();
NHibernate
Repository
Order Processing
IStoreOrders
+ GetIncompleteOrders(minValue);
+ StoreOrder();
+ CompleteOrder();
OrderRepository
VS
52. Main Package
Application
Bunch of blocks that
can be used directly
Uses composition
over inheritance
Convenience
blocks that don’t
hide the magic
Shared Package
Contract
Contract
Only depend on
more abstract
packages…
Stable Package
…or depend on more
stable packages
Auxiliary Package
Blocks that are not
used together do not
belong together
Optional
Dependency
Dependency
Package
Consumers should
not be faced with
optional
dependencies
No cyclic
dependencies
Apply principles of successful package
management
58. Q&A
Join me at 14:30 in room 7
Meet me near room 11
Also find me at
twitter: ddoomen
mastodon: @ddoomen.mastodon.social
bluesky: @ddoomen.bsky.social
email: dennis.doomen@avivasolutions.nl
slack: fluentassertions.slack.com