Working as a .NET Professional is a tumultuous rollercoaster ride of emotional highs and crushing lows. It’s likely the same for other communities, with different flavors of success and failures. I have over a decade of .NET development work, and I am here to share some general mantras that have served me well. I hate to call it advice because all advice is context-specific. Readers should take any general counsel with a healthy dose of skepticism and critical thinking. That said, I hope folks reading this will find value through the prism of my experiences.

In this post, we’ll go through technical and non-technical ideas that have helped me through some of my toughest projects.

Strings, Strings, Strings

At its lowest level, we can boil programming to 1’s and 0’s. A few levels up from that is our human abstraction comprising mainly of string values. The string class in .NET is arguably the most used in all of .NET. While we can utilize it like a primitive type, it does not behave like a primitive during runtime. Most memory leaks I’ve encountered were due to the creation of unique strings. We can see this happen in some of the following situations:

  • Logging with custom messages
  • HTTP request building
  • Exception messages with custom messages
  • String concatenation (using + instead of StringBuilder)
  • ORM SQL generation out of control

The CLR will store string constants in an in-memory data structure called the intern pool.

The common language runtime conserves string storage by maintaining a table, called the intern pool, that contains a single reference to each unique literal string declared or created programmatically in your program. Microsoft

The behavior usually is something we want in our applications, but we need to be mindful of it as we hit any meaningful scale. It’s a good idea to follow these guidelines:

  • Use StringBuilder when building large strings.
  • Use structured logging techniques and avoid “custom messages.”
  • Use string constants when possible.

If we’re experiencing memory leaks, it is a good idea to look through our dependencies to see what could be generating strings. Here is a past experience with a leak in Windows Azure Application Insights.

Decompiling Dependencies

Adding a dependency to a project is a hopeful act. We hope that the package will solve our problems with little or no effort. Often that’s the case, but sometimes it is a complete disaster. A decade ago, it would have been a frustrating experience, not understanding what the heck is happening.

With today’s tools, we can step into our dependencies to understand the underlying behavior, especially in those situations where it’s all gone wrong. Productivity tools like ReSharper and Rider have helped millions of developers better understand their dependencies. Visual Studio has also added this feature recently. Debugging into a third-party package can help expedite a solution, allowing developers to solve their issues.

There are cases where the decompilation loses some clarity. In these cases, when possible, we can go to the authors’ GitHub repository. Reading more code, especially other folks’ code, makes us better developers.

Nothing Is Free

The following statement may seem obvious on face value, but many developers still fall into this trap.

In the case of third-party packages, they are abstracting away a problem for our convenience. Let’s take an absurd example of an ultra-convenient abstraction.

public static void Main() {
  DoEverything();
}

How expensive should we expect DoEverything to be? Is it making any network calls? Is it bound to I/O? Let’s take a less absurd example using Entity Framework.

var results = db.Products.ToList();

Most developers would expect this to “work,” and it might work for a bit. Then the product table grows from 100 to 100,000, and now we’re in trouble. Abstractions are convenient, but we still have to understand the power we are wielding in many cases. Pausing and asking simple questions can save a developer, team, and company days of debugging under pressure.

Task.Awesome

Asynchronous programming is pretty commonplace in the .NET ecosystem. We’ve recently been able to make everything async, and that’s awesome for everyone.

public class Program
{
    public async Task Main(string[] args)
    {
        await CreateHostBuilder(args).Build().RunAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}

It’s essential to at least have a shallow under the class at the center of all of it, and that’s Task. The asynchronous programming model in .NET is a state machine where every call to await creates a new step in state-machine. For the most part, the async/await pair work, but understanding how the abstraction works helps us make decisions around cancellation tokens and proper edge-case handling.

Learn LINQ

The Language Integrated Query language is an elegant addition to any .NET developers toolkit. I could wax poetic about all the ways I love LINQ, but that is time wasted not learning LINQ.

Take It Easy On The Patterns

Impostor syndrome is a difficult struggle for many of us. It’s easy to fall into the trap of adding more abstractions into our codebase to compensate for our internal conflicts. We should consider each pattern appropriately and understand what it brings to a project and what burden it may introduce on ourselves and team members. In the majority of my experience, adding more abstractions was a way to delay the need to solve the real problems facing us.

One Project Is Fine*

Likely, the most controversial opinion on the list, but commonly, one project (excluding a test project), is fine for many solutions. .NET allows us to have a mind-boggling number of projects in a solution. Many of us see that as a challenge to pump our solutions with as many assemblies as humanly possible. Starting with one project gets us out of practicing Project Feng Shui and back to solving the problem at hand. Projects make sense for teams delivering assemblies, less so for teams iterating on a service.

*Developers should use their judgment to make the ultimate decision.

Integration Tests > Unit Tests

The systems we build have many complex external dependencies. The hubris of thinking we could mock away decades of investment in a database, web service, or any other technology has been the source of many frustrating arguments. In my opinion, one good integration test is worth 1,000 unit tests. Here is a previous post describing how to write database integration tests. With the rise of containerization, the pain of writing integration tests has never been lower.

Unit tests still have their place in our testing insfrastructure, but in my experience, tests that rely heavily on mocking external behaviors will erode and become unmanageable over time.

Use Tools Other Than .NET

I love what .NET allows developers to accomplish, and with its ability to be cross-platform, its never been a better time to be a .NET developer. The power of .NET may tempt us into bending everything we do towards our preferences, but that means we’re closing out a world of wonderful possibilities.

By exploring other ecosystems, we might find more fitting solutions to our problems. The JavaScript community has many great projects to offer, and so does the Ruby and Python communities. We limit ourselves when we only look in one place for the solutions to our problems.

This blog is written primarily using Jekyll and Ruby, and I’ve written a few posts to help folks looking to get into blogging.

People > Technology

Technology can help us overcome many technical hurdles, but many of the problems teams face are not technological: vision, communication, vision, and planning. A cohesive team with dated technology will outperform a dysfunctional team with the latest tech every time. Nothing beats working with folks we respect and that we can have spirited and productive debates with for the ultimate goal of building cool things.

What’s The Community Think

I mentioned that I was writing this post to the broader community, and the response was overwhelmingly positive.

I suggest reading the tweets if you’re interested in adding to the conversation or seeing what other folks think. Here are some of my favorites.

There are more 💎gems in the thread, and definitely worth a read and follow.

Conclusion

Being a professional is a moving target at best, and we are all trying to do our best. Approaching our careers with humility and a passion for learning and getting better will put us in a position to advance towards our goals. We need to understand that the hurdles we’ll face on our journey will be a mixture of technical and non-technical and that finding balance is critical for our success. The technical advice I gave will likely change over time, but the non-technical information is timeless. Ultimately, we’ve chosen a profession designed to help folks solve their problems. To do that, we first need to understand and solve our problems.

I offer office hours above if you’re interested in having a chat or debating any points in this post.

Thank You.