7 dangerous mistakes that are easy to make in C # /. NET

Translation of the article prepared in advance of the start of the course “C # ASP.NET Core Developer”.


C # – great language, and the .NET Framework is also very good. Strong typing in C # helps to reduce the number of errors that you can provoke, in comparison with other languages. Plus, its overall intuitive design also helps a lot, compared to something like JavaScript (where true is false) However, each language has its own rake that is easy to tread on, along with erroneous ideas about the expected behavior of the language and infrastructure. I will try to describe some of these errors in detail.

1. Do not understand delayed (lazy) execution

I believe that experienced developers are aware of this .NET mechanism, but it may surprise less knowledgeable colleagues. In a nutshell, methods / operators that return IEnumerable<T> and use yield to return each result, they are not executed in the line of code that actually calls them - they are executed when the resulting collection is accessed in some way *. Note that most LINQ expressions end up returning their results c yield.

As an example, consider the egregious unit test below.

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Ensure_Null_Exception_Is_Thrown()
{
   var result = RepeatString5Times(null);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Ensure_Invalid_Operation_Exception_Is_Thrown()
{
   var result = RepeatString5Times("test");
   var firstItem = result.First();
}
private IEnumerable RepeatString5Times(string toRepeat)
{
   if (toRepeat == null)
       throw new ArgumentNullException(nameof(toRepeat));
   for (int i = 0; i < 5; i++)
   {   
       if (i == 3)
            throw new InvalidOperationException("3 is a horrible number");
       yield return $"{toRepeat} - {i}";
   }
}

Both of these tests will fail. The first test will fail, because the result is not used anywhere, so the body of the method will never be executed. The second test will fail for another, slightly more non-trivial reason. Now we get the first result of calling our method to ensure that the method actually runs. However, the delayed execution mechanism will exit the method as soon as it can - in this case, we used only the first element, therefore, as soon as we pass the first iteration, the method stops its execution (therefore i == 3 will never be true).

Delayed execution is actually an interesting mechanism, especially because it makes it easy to chain LINQ queries, retrieving data only when your query is ready for use.

2. Assuming the Dictionary type stores the elements in the same order in which you add them

This is especially unpleasant, and I’m sure that somewhere I have code that relies on this assumption. When you add items to the list List, they are saved in the same order in which you add them - logically. Sometimes you need to have another object associated with an item in a list, and the obvious solution is to use a dictionary Dictionary<TKey, TValue>, which allows you to specify a related value for the key.

Then you can iterate over the dictionary using foreach, and in most cases it will behave as expected - you will access the elements in the same order in which they were added to the dictionary. However, this behavior undefined - i.e. it is a happy coincidence, not something you can rely on and always expect. This is explained in Microsoft documentationbut I think few people have carefully studied this page.

To illustrate this, in the example below, the output will be as follows:

third
second

var dict = new Dictionary();       
dict.Add("first", new object());
dict.Add("second", new object());
dict.Remove("first");
dict.Add("third", new object());
foreach (var entry in dict)
{
    Console.WriteLine(entry.Key);
}

Do not believe me? Check here online yourself.

3. Do not take into account flow safety

Multithreading is great, if implemented correctly, you can significantly improve the performance of your application. However, as soon as you enter multithreading, you should be very, very careful with any objects that you will modify, because you may begin to encounter seemingly random errors if you are not careful enough.

Simply put, many base classes in the .NET library are not thread safe - This means that Microsoft makes no guarantees that you can use this class in parallel using multiple threads. This would not be a big problem if you could immediately find any problems associated with this, but the nature of multithreading implies that any problems that arise are very unstable and unpredictable - most likely, no two executions will produce the same result.

As an example, take this block of code, which uses a simple, but not thread safe, List.

var items = new List();
var tasks = new List();
for (int i = 0; i < 5; i++)
{
   tasks.Add(Task.Run(() => {
       for (int k = 0; k < 10000; k++)
       {
           items.Add(i);
       }
   }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(items.Count);

Thus, we add numbers from 0 to 4 to the list 10,000 times each, which means that the list should eventually contain 50,000 elements. Should I? Well, there’s a small chance that in the end it will be - but below are the results of 5 of my different launches:

28191
23536
44346
40007
40476

You can self check it online here.

In fact, this is because the Add method is not atomic, which implies that the thread can interrupt the method, which can ultimately resize the array while another thread is in the process of adding, or add an element with the same index as the other thread. The IndexOutOfRange exception came to me a couple of times, probably because the size of the array changed during the addition to it. So what do we do here? We can use the lock keyword to ensure that only one thread can add an (Add) item to the list at one time, but this can significantly affect performance. Microsoft, being nice people, provides some great collections that are thread safe and highly optimized in terms of performance. I already posted article describing how you can use them.

4. Abuse lazy (deferred) loading in LINQ

Lazy loading is a great feature for both LINQ to SQL and LINQ to Entities (Entity Framework), which allows you to load related table rows as needed. In one of my other projects, I have a table "Modules" and table "Results" with a one-to-many relationship (a module can have many results).

When I want to get a specific module, I certainly do not want the Entity Framework to return every Result that the Modules table has! Therefore, he is smart enough to execute a query to get results only when I need it. Thus, the code below will execute 2 queries - one to get the module, and the other to get the results (for each module),

using (var db = new DBEntities())
{
   var modules = db.Modules;
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //Производим операции с модулем
   }
}

However, what if I have hundreds of modules? This means that a separate SQL query to get the result records will be executed. for each module! Obviously, this will put a strain on the server and significantly slow down your application. In the Entity Framework, the answer is very simple - you can specify that it include a specific set of results in your query. See the modified code below, where only one SQL query will be executed, which will include each module and each result for this module (combined into one query, which the Entity Framework intelligently displays in your model),

using (var db = new DBEntities())
{
   var modules = db.Modules.Include(b => b.Results);
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //Производим операции с модулем
   }
}

5. Don't understand how LINQ to SQL / Entity Frameworks translates queries

Since we touched on the LINQ topic, I think it's worth mentioning how differently your code will execute if it is inside a LINQ query. Explaining at a high level, all your code inside a LINQ query is translated into SQL using expressions - it seems obvious, but it’s very, very easy to forget the context you are in, and ultimately introduce problems into your code base. Below I have compiled a list to describe some typical obstacles you may encounter.

Most method calls will not work.

So, imagine that you have the query below to separate the name of all modules with a colon and capture the second part.

var modules = from m in db.Modules
              select m.Name.Split(':')[1];

You will get an exception in most LINQ providers - there is no SQL translation for the Split method, some methods may supported, such as adding days to a date, but it all depends on your provider.

Those that work can give unexpected results ...

Take the LINQ expression below (I have no idea why you would do this in practice, but please just imagine that this is a reasonable request).

int modules = db.Modules.Sum(a => a.ID);

If you have any rows in the module table, it will give you the sum of the identifiers. Sounds right! But what if you execute it using LINQ to Objects instead? We can do this by converting the collection of modules into a list before we execute our Sum method.

int modules = db.Modules.ToList().Sum(a => a.ID);

Shock, horror - it will do exactly the same! However, what if you did not have rows in the module table? LINQ to Objects returns 0, and the Entity Framework / LINQ to SQL version throws an exception InvalidOperationExceptionwhich says that it cannot convert “int?” in “int” ... such. This is because when you execute SUM in SQL for an empty set, NULL is returned instead of 0 - therefore, instead it tries to return a nullable int. Here are a few tips on how to fix this if you encounter such a problem.

Know when you just need to use the good old SQL.

If you are executing an extremely complex request, then your translated request may end up looking like something spit out, eaten up again and again spit out. Unfortunately, I have no examples to demonstrate, but judging by the prevailing opinion, I really like using nested views, which makes code maintenance a nightmare.

In addition, if you encounter any performance bottlenecks, it will be difficult for you to fix them because you do not have direct control over the generated SQL. Either do it in SQL, or delegate it to the database administrator, if you or your company has one!

6. Wrong rounding

Now about something a little simpler than the previous paragraphs, but I always forgot about it, and ended up with unpleasant mistakes (and, if it is connected with finances, an angry fin / gene director).

The .NET Framework includes an excellent static method in the class Mathreferred to as Round, which takes a numeric value and rounds it to the specified decimal place. It works perfect most of the time, but what to do when you try to round 2.25 to the first decimal place? I assume that you probably expect it to round to 2.3 - that's what we're all used to, right? Well, in practice, it turns out that .NET uses banker roundingwhich rounds the given example to 2.2! This is due to the fact that bankers are rounded to the nearest even number if the number is at the “midpoint”. Fortunately, this can easily be overridden in the Math.Round method.

Math.Round(2.25,1, MidpointRounding.AwayFromZero)

7. The horrible class 'DBNull'

This can cause unpleasant memories for some - ORM hides this filth from us, but if you delve into the world of naked ADO.NET (SqlDataReader and the like) you will meet DBNull.Value.

I'm not 100% sure of the reason, for which NULL values ​​from the database are processed as follows (please comment below if you know!), but Microsoft decided to present them with a special type DBNull (with a static field Value). I can give one of the advantages of this - you will not get any unpleasant NullReferenceException when accessing a database field that is NULL. However, you should not only support the secondary way of checking for NULL values ​​(which is easy to forget, which can lead to serious errors), but you lose any of the great features of C # that help you work with null. What could be as simple as

reader.GetString(0) ?? "NULL";

what eventually becomes ...

reader.GetString(0) != DBNull.Value ? reader.GetString(0) : "NULL";

Ugh.

Note

These are just some of the non-trivial “rakes” that I have encountered in .NET - if you know more, I would like to hear from you below.


ASP.NET Core: Quick Start


Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *