Common .Net Gotcha
Expert Advice from Tech Elevator Instructor, Matt Eland
Read along with Tech Elevator Instructor, Matt Eland, as he talks “Common .NET Gotchas” in Software Development.
Editor’s Note: This article was originally published to Tech Elevator Instructor Matt Eland’s Blog. You can visit the original post here.
Common .NET Gotchas
.NET Development and C# in particular have come a long way over the years. I’ve been using it since beta 2 in 2001 and I love it, but there are some common gotchas that are easy to make when you’re just getting started.
If you need to throw an Exception, don’t do the following:
Instead do something more targeted to your exception case:
The reason why this is important is explained in the next section.
When a generic
Exception instance is thrown and you want to be able to handle that, you’re forced to do something like the following:
Catching Exception catches all forms of Exceptions and is rarely ever what you actually should do. Instead, you should be looking for a few targeted types of exceptions that you expect based on what you’re calling and should have handlers for those, letting more unexpected exceptions bubble up.
When catching an exception, you sometimes want to rethrow it – particularly if it doesn’t match a specific criteria. The syntax for correctly rethrowing an exception is different than you’d expect since it’s different than originally throwing an exception.
The reason you need to do this is because of how .NET stack traces work. You want to retain the original stack trace instead of making the exception look like a new exception in the catch block. If you are instead using
throw ex (or similar) you’ll miss some of the original context of the exception.
Working with Immutable Types
Some types, like
DateTime are said to be immutable, in that you can create one, but you cannot change it after creation. These classes expose methods that allow you to perform operations that create a new instance based on their own data, but these methods do not alter the object they are invoked on and this can be misleading.
For example, with a DateTime, if you were trying to advance a tracking variable by a day, you would not do this:
This statement would execute and run without error, but the value of
myMeeting.Date would remain what it originally was since
AddDays returns the new value instead of modifying the existing object.
To change myMeeting.Date, you would instead do the following:
Speaking of Dates, TimeSpan exposes some interesting properties that might be misleading. For example, if you looked at a TimeSpan, you might be tempted to look at the milliseconds to see how long something took, but if it took a second or longer, you’re only going to get the milliseconds component for display purposes, not the total milliseconds.
Don’t do this:
Instead, use the
TotalX series of methods like this:
When comparing doubles, it’s easy to think that you could just do:
But due to the sensitivity of double mathematics, those numbers could be slightly off when dealing with fractions.
Instead, either use the
decimal class or compare that the numbers are extremely close by using an Epsilon:
Frankly, I tend to steer away from
double in favor of
decimal to avoid syntax like this.
When working with strings, it can be performance intensive to do a large amount of string appending logic, since new strings need to be created for each combination encountered along the way, leading to higher frequencies of garbage collection and noticeable performance spikes.
If you’re in a scenario where you expect to append to a string more than 3 times on average, you should instead use a
StringBuilder which does techno ninja voo doo trickery internally to optimize the memory overhead for building a string from smaller strings.
When working with
IDisposable instances, it’s important to make sure that
Dispose is properly called – including in cases when exceptions are encountered. Failing to dispose something like a
SqlConnection can lead to instances where databases do not have available connections for new requests, which brings production servers to a sudden halt.
This is the equivalent of a
finally that calls
conn is not null. Note also that database adapters will close connections as part of their
Overall using leads to cleaner and safer code.
When you declare an asynchronous method that doesn’t return anything, syntactically it’s tempting to declare it as follows:
However, if an exception occurs, the information will not correctly propagate to the caller due to how the threading logic works under the cover. This means that any
catch logic in the caller won’t work the way you expect and you’ll have buggy exception handling behavior.
Instead do this:
Task return type allows .NET to send exception information back to the caller as you would normally expect.
Preprocessor statements are just plain old bad. To those unfamiliar, a preprocessor statement allows you to do actions prior to compilation based things defined or not defined in your build options.
The correct use of preprocessor statements is for environment-specific things, such as using a library for x64 architecture instead of another one for x86 architecture, or including some logic for mobile applications but not for other applications sharing the same code.
The problem is that people take this capability and try to bake in customer-specific logic, effectively fragmenting the code for allowing it to compile targeting different targets, but by which set of logical rules or UI styling is desired.
This becomes hard to maintain and hard to test and does not scale well. It also makes it easy to introduce errors while refactoring and overall will slow your team’s velocity over time.
Some people advocate for using the
DEBUG preprocessor definition to allow for testing logic on local development copies, but be very careful with this. I once encountered a production bug related to deserialization where the development version worked fine every time because it had a property setter defined in a
DEBUG preprocessor statement, but deserialization failed in production for that field leading to buggy behavior.
Again, be very careful and lean towards object-oriented patterns like the Strategy or Command pattern for client-specific logic or other types of behavioral logic.
Speaking of deserialization, be mindful of private variables, properties without setters, and logic that exists in property setters or getters. Different serialization / deserialization libraries approach things differently, but these areas tend to introduce obscure bugs where properties will not populate correctly during deserialization.
These are a few of the common .NET mistakes I see people encounter. What others are there out there that I neglected to mention?