.NET ecosystem and C# best practices

Translated into:
EN
UA

Ivan Hedz

Ivan workes as Full Stack Web Developer in Binary Studio. Tech stack is C# and Typescript. Hobbies – listening to music, arcade games and yoga. Decided to record a lecture for the Academy for the first time so that it would be easier for you to understand the .NET ecosystem.

Hello!
The lecture about the .NET ecosystem and best practices for writing C# code will start in 5..4..3... But first, 🥁 some disclaimers about the lecture itself.
  1. You have probably already heard (or not) about much of what will be discussed;
  2. Some topics are talked about superficially, without going into details;
  3. The lecture may seem long, but believe me is faster than diving into the MSDN documentation in search of the necessary information.

You will also need a .NET developer starter pack: .NET, Git, Visual Studio or Rider.

Section 1 Top

Overview of the .NET Platform

Difficulty: Easy peasy lemon squeezy. Objective: Read/hear about the .NET platform in general (Won't take long).

Microsoft is on top right now, have you heard about OpenAI company and their products like ChatGPT or DALL-E-2? Well it's 49% Microsoft 😎. .Net ecosystem is also supported by that company, just imagine what would be the best ecosystem in the future.

  • dotnet-platform

    Ways to create .NET applications:

    1. .NET Framework - development of Windows desktop applications on Windows Forms, WPF, web servers on ASP.NET and WCF
    2. .NET Core - Develop cross-platform web apps with ASP.NET Core, build hybrid apps with Universal Windows Platform which allows you to run a program written on this technology on a Windows machine, Xbox, Hololens
    3. Xamarin is a platform for building mobile apps for iOS and Android using C#, XML and XAML

    Code written for a specific framework such as WPF, ASP.NET Core, or Android cannot be reused on another platform because it is tailored to work with the so-called platform-specific API, which is different everyone has. So that you can reuse the code of business logic, helper methods, models, classes, etc. .NET Standard was created. It provides a set of available APIs that work in the same way in all desktop programs, web servers, mobile applications, games and cloud services, regardless of the operating system and platform.

  • dotnet5-platform

    Since November 2021, .NET 6 was released. Which became a serious improvement of the development system as a whole. The main innovation was the provision of support for Linux, macOS, iOS, Android, tvOS, watchOS and WebAssembly. As a result, it became possible to create applications for different platforms on a common code base with the same build process, regardless of the type of application. So now you can develop with the help of Visual Studio, Visual Studio for Mac, Visual Studio Code - or on any other IDE with help of dotnet CLI

  • nuget-logo

    Each programmer sooner or later has to implement functionality that someone has already created or even published in part or in full (usually in the form of an DLL library). Developers refer to such modules as "packages", which contain compiled code, additional asset files, and a manifest that explains the purpose and use of the package. Most programming languages have their own platforms for sharing such useful modules. In .NET, this is the NuGet supported by Microsoft. Developers who have created a cool tool or, for example, a library for working with the file system, can publish their work as a NuGet package in the form of an zip file with a extension. nupkg. You can search and download modules that will speed up the development of your application from the central NuGet Gallery repository - it already has about 250,000 unique packages and you might find something useful there.

Section 2 Top

.NET under the hood

Difficulty: Hard as hell 🔥 Objective: Understand the SDK.
  • The most common programming languages in the .NET world today are C#, F#, and Visual Basic. Each has its own compiler that converts code written in that language into Intermediate Language Code (IL). The latter is a set of instructions for the .NET virtual machine - CLR (Common Language Runtime).

    Basic steps in running a .NET program:

    • Plain C# code
      public void SumTwoNumbers() {
        int firstNumber = 10;
        var secondNumber = 200;
      
        Console.WriteLine(firstNumber + secondNumber);
      }
      
      
      
      
      
      
      
      
      
         
      C# code compiled to IL
      .method public hidebysig
      instance void SumTwoNumbers() cil managed {
        // Method begins at RVA 0x2070
        // Code size 18 (0x12)
        .maxstack 2.locals init([0] int32 firstNumber, [1] int32 secondNumber)
      
        IL_0000: ldc.i4.s 10
        IL_0002: stloc.0
        IL_0003: ldc.i4 200
        IL_0008: stloc.1
        IL_0009: ldloc.0
        IL_000a: ldloc.1
        IL_000b: add
        IL_000c: call void[mscorlib] System.Console::WriteLine(int32)
        IL_0011: ret
      }  
    • And when it comes time to execute a piece of code in a program, the CLR will use the JIT (Just in Time) compiler to turn the IL code into machine code.

    The result of building a .NET program is a file with the extension .exe (Executable) or .dll (Dynamic Link Library).

    It is important to note that when converting IL to native code, only the part of the code that should be executed at the current time will be converted.

  • At the highest level, there are 2 data types in C# - value types and reference types. It is important to understand the differences between them:

    Significant types:
    • Integer types
    • Floating point types
    • decimal
    • bool
    • enums
    • structs
    Reference types:
    • type object
    • string
    • classes
    • interfaces
    • delegates

    Value types are stored on the stack, reference types are stored on the heap. Value types are passed by value, i.e. copied, reference types are passed by reference.

  • stack-and-heap

    In .NET, memory is divided into two types: stack and heap. Stack is a data structure that grows from bottom to top: each new element is placed on top of the previous one. The stack stores value types and refs to reference types, which in turn are stored on the heap.

    A heap can be thought of as an unordered collection of heterogeneous objects. When an object of reference type is created, a reference to the address of this object in the heap is added to the stack. When an reference type object is no longer used, the reference is removed from the stack and the memory is freed.

    In .NET, memory cleanup happens automatically. The Garbage Collector is responsible for this (in our opinion, the garbage collector). When it sees that an object on the heap is no longer referenced, it removes that object and cleans up memory.

  • An important point is how the value and reference type variables are passed to the method.

    Value Type

    static void Main(string[] args) {
      int c = 20;
    
      MethodValue(c);
    
      Console.WriteLine(c); // 20
    }
    
    // pass copy of the value
    static void MethodValue(int variableCopy) {
      variableCopy = 1;
    }
    
    
    
    
    
    
      

    Reference Type

    class City {
      public int code;
      public City(int code) {
        this.code = code;
      }
    }
    
    static void Main(string[] args) {
      City city = new City(12);
    
      MethodReference(city);
    
      Console.WriteLine(city.code); // 0
    }
    
    // pass the reference to the object on heap
    static void MethodReference(City city) {
      city.code = 0;
    }

    ref

    static void Main(string[] args) {
      int d = 30;
    
      MethodValueRef(ref d);
    
      Console.WriteLine(d); // 2
    }
    
    // pass value by reference 
    static void MethodValueRef(ref int variable) {
      variable = 2;
    }

    Out

    static void Main(string[] args) {
      int e;
    
      MethodValueOut(out e);
    
      Console.WriteLine(e); // 3
    }
    
    // pass value by reference
    static void MethodValueOut(out int variable) {
      variable = 3;
    }
  • The boxing operation - boxing - is allocating memory on the heap for an object of a value type - value type, and assigning a reference to this memory area to a variable on the stack. Unboxing - unboxing, on the contrary, allocates memory on the stack for an object obtained from the heap by reference.

    int i = 123;      // a value type
    object o = i;     // boxing
    int j = (int)o;   // unboxing
    boxing-unboxing
Section 3 top

Most Important C# Topics

Difficulty: Not bad. Objective: Understand the SDK.
  • Structures are very similar in appearance to Classes, but there is a fundamental difference that was mentioned earlier. Class is reference type and is passed by reference, while structure is value type and is passed by value — that is, copied.

    Structures are best used for small classes, small data structures, and lightweight objects. Classes can be used in all cases where it is inconvenient for you to use a structure. They are great for being part of an entity hierarchy, having internal state, and containing a lot of business logic.

  • Classes and structures can have static fields, methods and properties. If a member is static, then it refers to the entire class or structure and does not need to be instantiated to refer to.

    class Wallet
    {
        public static int Bonus = 100;
        public int balance;
        public Wallet(int bal)
        {
            balance = bal; 
        }
    
        public int GetBalance()
        {
            return balance + Bonus;
        }
    
        public static int GetPureBalance()
        {
            // Error: Cannot access non-static field in static context
            return balance;
        }
    }
    
    static void Main(string[] args)
    {
        Console.WriteLine(Wallet.Bonus);      // 100
        Wallet.Bonus += 200;
         
        var wallet1 = new Wallet(150);
        Console.WriteLine(wallet1.GetBalance());   // 450
    }

    The example shows that static field is common to all objects of the class and can be used in non-static methods. At the same time, in static methods we do not have access to non-static members of the class.

  • Using the keyword params we can "say" that our method takes an indefinite number of parameters - it can be zero or more, any number.

    public class Program
    {
        public static void UseParams(string str, params int[] list)
        {
            Console.Write(str);
            for (int i = 0; i < list.Length; i++)
            {
                Console.Write(list[i] + " ");
            }
        }
    
        static void Main()
        {
            // You can send a comma-separated list of arguments
            // of the specified type.
            UseParams("params: ", 1, 2, 3, 4);
            // params: 
            // 1 2 3 4
        }
    }

    When a method has a variable number of parameters, we pass arguments to it simply by listing them separated by commas, as shown in the example. It is worth noting that the params argument must be specified last, after the list of all strictly defined method arguments.

  • In C#, abstraction is used to hide implementation details. This means that we are focusing on what an object can do rather than how it does it. This is often used when writing large and complex programs. The main tools for this are abstract classes and interfaces.

    In an abstract class, we can create functionality that is implemented in a class inherited from it.. For its part, an interface allows to define functionality or functions, but cannot implement them..

    A class implements an interface and must implement these methods. Let's look at a few key differences between them:

    1. An interface cannot have member access modifiers - everything in an interface is public by default. For an abstract class, everything remains the same as for a regular class.
      interface TestInterface  
      {  
          // Causes syntax error
          protected void GetMethod();  
      
          public string PublicProp { get; set; } 
      }
      abstract class TestAbstractClass  
      { 
          public abstract string GetStuff();
      
          public abstract void DoSmth();
      }
        
    2. In an interface, we can only describe the signature of a method without implementing it. And in the abstract class there can be both abstract methods and properties, and non-abstract ones - with full or partial implementation.
      interface TestInterface  
      {  
          // Only signature
          void GetMethod();  
      }
      
      
      
        
      abstract class TestAbstractClass  
      { 
          // Complete method implementation
          public string GetStuff()
          {
              Console.WriteLine("Stuff");
              return "Stuff";
          }  
      }
    3. We cannot declare a constructor in the body of an interface with or without an access modifier. In an abstract class, we can declare constructors in the same way as in regular classes. It is mainly used to call in the derived class constructor so as not to duplicate the field or property initialization code of the abstract class.
      interface TestInterface
      {
          // Interfaces cannot contain instance constructors
          TestInterface()
          {
          }
      
          public TestInterface(int s)
          {
          }
      }
      abstract class TestAbstractClass  
      {  
          public int a;  
          public string b;
      
          // Everything is OK - we can do that
          public TestAbstractClass(int a, string b)  
          {  
              this.a = a;
              this.b = b;
          }  
      } 
    4. We cannot explicitly instantiate an interface or abstract class by calling the constructor. Although let me remind you that abstract class can have it.
      static void Main(string[] args)
      {
          // Causes syntax error
          TestInterface testInterface = TestInterface();
      
          // Causes syntax error as well
          TestAbstractClass abstractClass = TestAbstractClass();
      }
    5. An abstract class can have fields and properties, an interface can only have properties.
      interface TestInterface  
      {  
          // Causes syntax error
          int field;
      
          const string name;
      }
      abstract class TestAbstractClass  
      { 
          // 
          public static int n = 1;
          protected const int m = 3;
          private int k = 3;  
      }  
    What, why and where?

    The Interface we use to describe the API for multiple classes that are likely to implement more than one interface. Remember that interface members cannot be static. C#, unlike C++, does not support multiple inheritance, so we use interfaces to implement it.

    Abstract class is used if we want to include it in the inheritance hierarchy and create functionality with a full or partial implementation that the derived class can implement or override. Abstract class allows you to save the state of the class as a whole, and not just its individual object.

    Interface is mainly used when we just want to describe the API usage of the classes that will implement this interface - set the behavior.

  • IDisposable declares a single Dispose method, in which the implementation of the interface in the class should release unmanaged resources such as database connections, file descriptors, network connections, and volumes. similar. Unmanaged resources should be freed as soon as possible, before the object is removed from memory when the Garbage Collector gets to it. For example, our class interacts with the file system - opens a file, reads something from it, writes. And it's better to finish working with this file as soon as possible so that other programs or threads can use it. And another thing, we ourselves need to explicitly call the Dispose method, because the Garbage Collector knows nothing about it. This is best done in a try...finally block so that even if an error occurs, we can free the resources and clean up the memory properly.

    • class MyFile : IDisposable
      {
          private FileStream MyFileStream { get; set; }
          private StreamWriter MyStream { get; set; }
      
          public MyFile(string filePath)
          {
              MyFileStream = File.Create(filePath);
              // Open stream for working 
              MyStream = new StreamWriter(MyFileStream);
          }
      
          public void AddTextToFile(string text) => MyStream?.Write(text);
      
          // Free unmanaged resources
          public void Dispose()
          {
              MyStream?.Dispose();
              MyFileStream?.Dispose();
          }
      }
        
      static void Main(string[] args) 
      {  
          MyFile file = null;
          try 
          {
              file = new MyFile("D://file.txt")
          }
          finally 
          {
              file.Dispose();
          }
      
          // The same - but shorter way
          using (MyFile file = new MyFile("D://file2.txt")) 
          {  
              file.AddTextToFile("Hello"); 
          }
      }
  • Extension methods allow you to "add" methods to existing types without creating a new derived type, recompiling, or modifying the original type. The Extension method is a special static method that must be a member of a static class.

    public static class StringExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
    
    string text = "Something like that!";  
    int amountOfWords = text.WordCount(); // 3

    The example shows the Extension method for the String type. The Static class can have an arbitrary name, while the name of the method must be different from the existing methods in the class we are extending, or have a different signature. In the future, we can use the method we declared in the same way as ordinary methods of the class we are extending.

  • Generics appeared in C# 2.0. They brought the concept of typed parameters to .NET - this allows you to design classes and methods that determine the type of class or method members only at initialization.

    For example, using a generic type parameter T, we can write a single class that will be used by client code without the risk of performing boxing operations (which are heavy operations in their own right). , and abuse them is not good).

    • class MyGenericClass<T>
      {
          private T genericMemberVariable;
          public MyGenericClass(T value)
          {
              genericMemberVariable = value;
          }
      
          public T genericProperty { get; set; }
      
          public T GenericMethod(T genericParameter)
          {
              Console.WriteLine("Parameter type: {0}, value: {1}", 
                                typeof(T).ToString(),genericParameter);
              Console.WriteLine("Return type: {0}, value: {1}", 
                                typeof(T).ToString(), genericMemberVariable);
      
              return genericMemberVariable;
          }
      }

      As you can see from the code above, MyGenericClass is defined with <T>. <T> indicates that MyGenericClass is a generic, and the T type will be defined later. You can use any letter or word instead of T, it doesn't matter.

    • class TestGenericClass
      {
          static void Main()
          {
              // Declare an object of type int.
              var intGenericClass = new MyGenericClass<int>(10);
      
              int val = intGenericClass.GenericMethod(100);    
              /* Output:
                  Parameter type: int, value: 100 
                  Return type: int, value: 10
              */
          }
      }

      The compiler now infers the type of class members based on the type passed in by the programmer when the class was created. For example, the following code uses the data type int.

    • C# has Constraints to restrict the types that can be used in a generic class. For example, if through Constraint we indicate that the type T can only be reference type, that is, classes, then we will not be able to use value type to instantiate the generic class. Accordingly, after that we cannot use structural types such as int - this will cause a compilation error.

      class MyGenericClass<T> where T: class 
      {
      }
      
      class City 
      {
      }
      
      class Program
      {
          static void Main()
          {
              // Compile Time Error
              MyGenericClass<int> intGenericClass = new MyGenericClass<int>(10);
      
              MyGenericClass<City> cityGenericClass = new MyGenericClass<City>(new City());
          }
      }
    • Here are all possible Constraint-s that can be used to constrain types for use in generic classes:

      constraints-types
  • Obviously structural data type we cannot assign null values. To do this, we need to declare a variable with the ? modifier. This modifier is an alias of the Nullable<T>

    structure
    int? f = null;
    Nullable<int> g = null; 

    Signature Nullable<T>:

    public struct Nullable<T> where T : struct

    When we wrap a variable in a Nullable type, we have a new API to interact with this variable:

    • Property HasValue, returns true if the variable has a value and false if it is null
    • int? f = 8;
      if (f.HasValue)
      {
          Console.WriteLine($"f is {f.Value}");
      }
      else
      {
          Console.WriteLine("f does not have a value");
      }
    • Value returns the actual value stored in the variable if HasValue is equal to true. Otherwise, Value throws InvalidOperationException if the variable is null.
  • Delegates are objects that point to methods; with them we can call the methods assigned to the delegate. Delegates allow you to represent methods as objects and pass them to functions, use them as callbacks.

    Events are delegate objects that report that some event (action) has occurred.

    Lambda expressions are shorthand for anonymous methods. This allows you to create concise methods that can return some value.

    • lambdaExpressionStructure
    • namespace DotNetLecture
      {
          // Declate delegate signature
          delegate void LogMessage(string messageToLog);
      
          public class Program
          {
              static void LogRedMessage(string message)
              {
                  Console.ForegroundColor = ConsoleColor.Red;
                  Console.WriteLine(message);
              }
              
              static void LogGreenMessage(string message)
              {
                  Console.ForegroundColor = ConsoleColor.Green;
                  Console.WriteLine(message);
              }
      
              static void Main(string[] args)
              {
                  // Create delegate variable and assign method's address to it
                  LogMessage logMessage = LogRedMessage;
                  
                  // Invoke method assigned to this delegate(Invoke syntax)
                  logMessage.Invoke("Hello world!\n");
      
                  // Add another method to delegate's invocation list
                  logMessage += LogGreenMessage;
                  
                  // Invoke all methods assigned to this delegate
                  logMessage("Second message");
      
                  // Remove the method from the delegate
                  logMessage -= LogGreenMessage;
              }
          }
      }
    • Instead of defining a new delegate type, you can use the already defined Action, Func, and Predicate delegates.

        The
      • generic delegate Action <T> is intended to refer to a method that returns void. You can pass up to 16 parameters of any type to this delegate class.
        static void Main(string[] args)
        {
            Action<string, int> printString = (str, num) => Console.WriteLine(str + num);
               
            printString("Printed by Action: ", 19);
            // Printed by Action: 19
        }
      • The Func delegates can be used in a similar way. Func allows you to call methods that return something. It can also be passed up to 16 types of parameters and 1 type that it returns.
        // Accepts 2 `double` parameters, returns `int`
        Func<double,double, int> add = (a, b) => Convert.ToInt32(a + b);
        
        int result = add(4.5, 10.0);
        
        Console.WriteLine(result); // 15
      • The Predicate delegate is used to compare whether some T object meets a certain condition. It returns true if the object satisfies the condition, and false if it doesn't.
        Predicate<int> isPositive = x => x > 0;
         
        Console.WriteLine(isPositive(10)); // True
        Console.WriteLine(isPositive(-10)); // False
    • delegateEventFlow

      Events allow you to tell the system that a specific action has taken place.

      There is this model: Publisher-Subscriber(Publisher-Subscriber). The Subscriber subscribes to the event, defines a handler, and waits for the Publisher to execute the event before triggering it.

  • In C#, there are arrays that store sets of similar objects, but working with them is not always convenient. Since an array stores a fixed number of objects, in cases where we do not know in advance how many we will have, it will be much more convenient to use collections.

    When choosing collections, it can be decisive that some of them implement standard data structures, such as:

    • stack
    • queue
    • dictionary
    • hash table

    ...which can be useful for various special tasks. The basis for creating all collections is the implementation of the interfaces IEnumerator and IEnumerable.

    The IEnumerator interface represents an enumerator that makes it possible to iterate through a collection, for example in a foreach loop, or by means of LINQ. And the IEnumerable interface, through its GetEnumerator method, provides an enumerator to all classes that implement this interface. Therefore, the IEnumerable interface is the base interface for all collections.

    Specific methods and uses may differ from one collection class to another, but the general principles will be the same for all collection classes.

    • collectionsHierarchy
    • This example uses two collections: non-generic is ArrayList and generic is List. It is now considered good practice to use the generic version of collections wherever possible due to strong typing and ease of use. Most collections support adding items.

      class Program
      {
          static void Main(string[] args)
          {
              // non-generic collection ArrayList
              ArrayList objectList = new ArrayList() { 0, "someString", 'c', 19.0d };
              
              // generic collection - List
              List<string> countries = new List<string>() { "Lviv", "Kyiv", "Odessa", "Dnipro" };
      
              object obj = 12.3;
              objectList.Add(obj);
      
              // Error - because object is not a string type
              countries.Add(obj);
      
              objectList.RemoveAt(0); // delete element with index 0 - first element of collection
              foreach (object o in objectList)
              {
                  Console.WriteLine(o);
              }
              Console.WriteLine($"Total amount of Collection's elements: {objectList.Count}");
          }
      }

      For example, in this case, adding is done using the Add method, but for other collections, the name of the method may differ. Also, most collections implement removal (in this example, it is done using the RemoveAt method, which removes an element from the collection by index). Using the Count property, you can see the number of elements in the collection.

    • Stack<T> represents a collection that uses the LIFO - last in - first out - algorithm. With this data organization, each next element is placed on top of the previous one. Elements are retrieved from the collection in the reverse order - the element that is highest on the stack is retrieved.

      stack

      In the Stack class, there are two main methods that allow you to manage elements - these are:

      • Push: Pushes an element onto the stack in first place
      • Pop: Gets the first element from the stack
      • Peek:* simply returns the first element from the stack without removing it
      static void Main(string[] args)
      {
          var stackCities = new Stack<string>();
          stackCities.Push("Lviv");
          stackCities.Push("Kyiv");
          stackCities.Push("Odessa");
          
          var odessa = stackCities.Pop();
          
          Console.WriteLine("Deleted city: " + odessa)
      
          foreach(string city in stackCities) {
              Console.WriteLine(city);
          }
          /* Output:
              Deleted city: Odessa
              Lviv
              Kyiv
          */
      }
      The example shows how we instantiate the ribbon stack collection. Add 3 cities — "Lviv", "Kyiv", "Odessa" using the Push method. We pull out the element that we added last using the Pop method and display the results on the screen.
    • Dictionary (dictionary) stores objects that represent a key-value pair. It is very handy to use to organize the correspondence of something to something.

      Each such object is an instance of the KeyValuePair<TKey, TValue> structure. Thanks to the Key and Value properties that this structure has, we can get the key and value of the element in the dictionary.

      Dictionary<char, City> cities = new Dictionary<char, City>();
      cities.Add('l', new City() { Name = "Lviv" });
      cities.Add('k', new City() { Name = "Kyiv" });
      cities.Add('o', new City() { Name = "Odessa" });
      
      foreach (KeyValuePair<char, City> keyValue in cities)
      {
          // keyValue.Value is instance of class City                     
          Console.WriteLine(keyValue.Key + " - " + keyValue.Value.Name);
      }
      /*
          l - Lviv
          k - Kyiv
          o - Odessa
      */
      
      foreach (char c in cities.Keys)
      {
          Console.WriteLine(c);
      }
      
      foreach (City c in cities.Values)
      {
          Console.WriteLine(c.Name);
      }
      
      // get element by key
      City city = cities['o'];
      
      // object modification
      cities['k'] = new City() { Name = "Kharkiv" };
      
      // remove by key
      cities.Remove('l');
  • If you need to glue two values together to return them to a function, or put two values into a hashset, you can use the System.ValueTuple

    types
    • class City
      {
          public int code;
          public string name;
      
          public City(int code, string name)
          {
              this.code = code;
              this.name = name;
          }
      
          public (int codeNumber, string name) MethodTuple(string namePrefix)
          {
              return (codeNumber: code, name: namePrefix + name);
          }
      }
      
      static void Main(string[] args)
      {
          City yorkCity = new City(12, "York");
      
          var tupleObject = yorkCity.MethodTuple("New ");
      
          Console.WriteLine($"City: {tupleObject.name}, code: {tupleObject.codeNumber}");
          /* Output: 
              City: New York, code: 12
          */
      }
    • // Constructing the tuple instance
      var tpl = (1, 2);
                  
      // Using tuples with a dictionary
      var d = new Dictionary<(int x, int y), (byte a, short b)>();
       
      // Tuples with different names are compatible
      d.Add(tpl, (a: 3, b: 4));
       
      // Tuples have value semantic
      if (d.TryGetValue((1, 2), out var r))
      {
          // Deconstructing the tuple ignoring the first element
          var (_, b) = r;
                      
          // Using named syntax as well as predefined name
          Console.WriteLine($"a: {r.a}, b: {r.Item2}"); // `a: 3, b: 4`
      }
      1. Creating a tuple
      2. Using a tuple to type Dictionary
      3. Adding tuple elements to Dictionary
      4. Returning a dictionary value by key
      5. Destructuring a tuple element
      6. Access tuple members by name
  • Sometimes when running a program, errors occur that are difficult or impossible to predict (for example, when transferring a file over a network, the Internet connection may be interrupted). Such situations are called Exceptions. The C# language provides developers with the ability to handle such situations using the try...catch...finally

    construct
    • static void Main(string[] args)
      {
          StreamReader file = new StreamReader(@"D:\test.txt");
          char[] buffer = new char[10];
          try
          {
              file.ReadBlock(buffer, 123, buffer.Length);
          }
          catch (Exception e)
          {
              Console.WriteLine(e.Message);
          }
          finally
          {
              file?.Close();
          }
      }
      1. When using a try...catch...finally block, all statements in the try block are executed first.
      2. If no Exceptions occurred in this block, then the finally block will be executed after it and the try..catch..finally construction will complete your work.
      3. If an Exception occurs in a try block, then the normal flow of execution stops and the CLR starts looking for a catch block that can handle this Exception.
      4. If the catch block is found, then it is executed, and after its completion, the finally block is executed.
      5. If the required catch block is not found, the program crashes.
    • In C#, all Exception types are inherited from the Exception parent class, which is further divided into two branches SystemException and ApplicationException .

      SystemException is the base class for all CLR or code errors such as DivideByZeroException or NullReferenceException and so on.

      exceptionClassHierarchy
    • ApplicationException is used for application related exceptions. This type of exception is very convenient to use to create your own custom Exception-s. To do this, you just need to inherit from the Exception class and add what you want there. Further in this class, you can define additional fields, properties, methods, etc.

      public class HttpStatusCodeException : Exception
      {
          public HttpStatusCode StatusCode { get; set; }
          public string ContentType { get; set; } = @"application/json"; // @"text/plain"
      
          public HttpStatusCodeException(HttpStatusCode statusCode)
          {
              StatusCode = statusCode;
          }
      
          public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
          {
              StatusCode = statusCode;
          }
      }
    • DataIDictionary containing data in key-value pairs.
      HelpLinkMay contain a URL (or URN) to a help file that provides comprehensive information about the cause of the error.
      InnerExceptionThis property can be used to create and store an error chain when processing Exception-a. You can use it to create a new exception that contains pre-Exception-s.
      MessageProvides detailed information about the reason for the exception.
      SourceContains the name of the program or object in which the error occurred.
      StackTraceContains stack trace which can be used to determine where the error occurred. The Stack trace includes the name of the source file and the line number of the program, if available debug information.
    • After the throw statement, there is a Exception-a object, in whose constructor we can pass an error message. Instead of the generic Exception type, we can specify an object of any other type Exception.

      catch (DivideByZeroException e)
      {
          // thow new exception
          throw new HttpStatusCodeException(400, "Can't divide by 0");
      }

      Similarly, we can throw Exceptions anywhere in the program. But there is also another form of using the throw operator, when nothing is specified after this operator.

      catch (DivideByZeroException e)
      {
          //TODO: log error
          Console.WriteLine("Can't divide by 0");
          throw;
      }

      In this form, the throw statement can only be used in a catch block. The difference between them is that throw without anything keeps the original stack trace, while throw ex resets the stack trace code> to the method currently processing Exception.

Section 4 Top

Clean Code Principles

Difficulty: Measure twice, cut once. Objective: Understand how to write code that people want to read.
  • Translated into human language, generally accepted coding standards and agreed upon rules for how to name variables, functions, etc. This is the C# grammar and spelling adopted by most .NET developers so that other developers (you through X time) could easily and quickly understand what is happening in your code and use it without getting confused in all possible ways to name, say, the argument (and such spelling rules exist in absolutely all programming languages, not only in C#). It makes no sense to talk about each of the rules for a long time, the main thing for you is to familiarize yourself with the list of most common C# standards.

    If you want to be a civilized developer and earn respect among your colleagues, read a few paragraphs with examples of well-formed code and stick to this format when performing tasks small or large.
  • Don't repeat yourself when writing code = don't write multiple times something that can be coded once and called referring to a specific module. An example is a web application containing several blocks of the same design, where each has its own (identical to others!) style descriptions. What is the probability that when all these blocks need to be edited in the same way (manually, because we repeat the same set of styles several times), the developer will miss one or more of them? When this principle is violated and the implementation of a method or even a class is duplicated without a real need, and several hundred thousand lines of code are written (as in any real project), then for refactoring, changing business logic, or even simple changes in the interface, you have to search for a long time by name method is an unfortunate piece of code, often in order to change only 1 digit in it.

    In short, it doesn't work that way. To achieve DRY in your code - divide it into small pieces; you see that part of the logic is repeated - take it out right away, combine functions. Why is DRY needed? The less code the better. It's easier to maintain, it takes less time to figure it out, and it also reduces bugs.
  • This principle speaks for itself - simple and concise code is easier to understand for other developers and for you when you return to it after a while. It is formulated as follows - "each method should solve only one small problem, and not have many different means of consumption." If there are many conditions in the method, then break them into smaller methods. Such code is easier to read, maintain, and it also helps to find bugs much faster. To demonstrate KISS, the most common example is to define the day of the week:

    Simple
    string weekday(int day) {
      switch (day) {
      case 1:
        return "Monday";
      case 2:
        return "Tuesday";
      case 3:
        return "Wednesday";
      case 4:
        return "Thursday";
      case 5:
        return "Friday";
      case 6:
        return "Saturday";
      case 7:
        return "Sunday";
      default:
        throw new Exception("Day must be in range 1 to 7");
      }
    }
    Stupid
    string weekday(int day) {
      if ((day < 1) || (day > 7)) {
        throw new Exception("Day must be in range 1 to 7");
      }
    
      string[] days = {
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
        "Sunday"
      };
    
      return days[day - 1];
    }
    
    
       
    The slide shows two methods for solving this problem:
    1. The first solution is as simple as doors — a simple switch with a default case if the day is not found.
    2. The second method is also working, but in order to understand it, you need to read it longer
    This kind of code exists everywhere, but it's really awkward and looks unprofessional, 99% of programmers would choose to work with something similar to the first option.
    To achieve KISS - try to write as simple code as possible. If you see a complex (unreadable) piece of code, look for a more concise solution to the same problem, and by refactoring what you have written, you will be surprised that a piece of 200 lines is actually not so necessary!
  • SOLID are 5 principles of object-oriented programming that describe software architecture:

    In simple terms, these are the rules by which you will write easy-to-understand, edit, or reuse code.

    • Single Responsibility Principle. It means that each class or struct should have only one task.. All members of the class are written to perform the task given to it, and there is not a single line of code in it that does not apply to the task specified for this block. If we adhere to this principle, then we define classes by their tasks at the design stage of the program.

      😢

      class Task {
        public string Title { get; set; }
        
        public string Description { get; set; }
      
        // Adds task in Database
        public bool Add(Task tast) {
          // Internal realization (Insert into DB)
        }
      
        // Estimate Task's duration
        public void EstimateTaskDuration(Task tast) {
          // Calculation Task's difficulty, estimating
        }
      }
      
         

      😎

      class Task {
        public string Title { get; set; }
        
        public string Description { get; set; }
      
        // Adds task in Database
        public bool Add(Task tast) {
          // Internal realization (Insert into DB)
        }
      }
      
      class TaskEstimator {
        // Estimate Task's duration
        public void EstimateTaskDuration(Task tast) {
          // Calculation Task's difficulty, estimating
        }
      }
      I gave the Task class as an example - it saves the task to our database and calculates the time required to solve the task.
      We conclude that it does not comply with the Single Responsibility Principle. Why don't we want it to perform other useful functions, such as determining the time needed to complete a task? Because if after some time the customer's task execution parameters change (for example, due to a release or a change in the composition of the programming team), we will have to rewrite the Task class in accordance with the changes in the original data, and test whether other functionality that Task performs is broken. According to the Single Responsibility Principle, we should create a separate class for calculating the time to complete tasks, which will already be guided by business logic and other incoming data.
    • Principle of openness / closeness. Our class should be open for scaling, but closed for modifications. Our module should be designed in such a way that it is added only when new requirements are created - but related to the initial task. “Closed for modifications” means that the class is already completely ready and viable, its tasks and purposes do not change, therefore we do not rewrite it significantly, except in case of fixing bugs. In C#, this is achieved by the principle of inheritance.

      👎

      class Mockup {
        public string ImageType { get; set; }
      
        public Image ConvertImage(Image img) {
          if (ImageType == "tiff") {
            // Convert Image to the tiff format
          }
      
          if (TypeReport == "cdr") {
            // Convert Image to the cdr format
          }
        }
      }
      
      
      
          

      👍

      abstract class Mockup {
        public virtual void ConvertImage(Image img) {
          // Base realization that common for each format
        }
      }
      
      class MockupTiff: Mockup {
        public override Image ConvertImage(Image img) {
          // Convert Image to the tiff format
        }
      }
      
      class MockupCdr: Mockup {
        public override Image ConvertImage(Image img) {
          // Convert Image to the cdr format
        }
      }

      Let's consider an example with a mockup - a page prototype. The problem with this class is that when a customer wants to look at a mockup created by designers, but cannot open an image in tiff or cdr format, the developer will need to introduce a new image format, for example png. Therefore, we will be forced to add a new if condition, which contradicts the Open Closed Principle.

      The second example shows how this can be solved - there is a base abstract class Mockup that partially implements image conversion, and child classes implement image conversion to the required format. And if we want to add another format, we just need to create another class that inherits from Mockup and implements the conversion method we need.

    • According to the principle of Liskov substitution, we must use any child class instead of the parent in the same way, without making any changes. A child class cannot violate the type definition given in the parent class and contradict its behavior with its own functionality.

      🤦‍♂️

      abstract class Developer {
        public virtual string CodeWebApp() {
          return "Coding Front-End Web App";
        }
        public virtual string CodeServer() {
          return "Coding Back-End Server";
        }
      }
      class JavaScriptDeveloper: Developer {
        public override string CodeWebApp() {
          return "Coding Front-End with Angular";
        }
        public override string CodeServer() {
          return "Coding Back-End with Node.js";
        }
      }
      class CSharpDeveloper: Developer {
        // C# Developer can't  create Front-End App
        public override string CodeWebApp() {
          throw new NotImplementedException();
        }
        public override string CodeServer() {
          return "Coding Back-End with ASP.Net";
        }
      }

      💁‍♂️

      interface IFrontend {
        string CodeWebApp();
      }
      
      interface IBackend {
        string CodeServer();
      }
      
      class JavaScriptDeveloper: IFrontend,
      IBackend {
        public string CodeWebApp() {
          return "Coding Front-End with Angular";
        }
        public string CodeServer() {
          return "Coding Back-End with Node.js";
        }
      }
      
      class CSharpDeveloper: IBackend {
        public string CodeServer() {
          return "Coding Back-End with ASP.Net";
        }
      }
      
         
      Here's how it can be illustrated: Developer is the parent class of JavaScriptDeveloper and CSharpDeveloper. Our Developer class can create backend and frontend applications. It would seem that everything is fine. JavaScriptDeveloper successfully implements 2 methods. But with CSharpDeveloper it's not so simple, he can write a server in ASP.NET, but he can't do it in frontend at all. And if we still try to get a frontend from it, we will catch an error - an exception. In a good way, we need to divide the functionality of Developer into 2 parts: IFrontend and IBackend, and implement them in accordance with the purpose of derived classes: JavaScriptDeveloper implements both IFrontend and IBackend, while CSharpDeveloper only implements IBackend.
    • The principle of separation of interfaces says that you should not pack all the interfaces together in a row, you should separate them by purpose so that users can selectively implement only those that use and not all in a row that are available in program.

      💩

      interface IDeveloper {
        string CodeDesktop();
        string CodeServer();
      }
      
      
         

      🎉

      interface IDesktop {
        string CodeDesktop();
      }
      
      interface IBackend {
        string CodeServer();
      }

      Let's assume we have an IDeveloper interface that now knows how to create a server and desktop application. As before, we have JavaScriptDeveloper and CSharpDeveloper that can use this functionality as intended. For JavaScript, the application would be written under Electron, and in C# it would be a WPF application. Everything is great, everyone is happy, but no, because our boss suddenly says that his applications on Electron are lagging and it’s generally expensive to pay these JavaScript developers. We are cutting back on JavaScript desktop projects, writing only in WPF now. And in this way we break the Interface Segregation principle, because our class cannot but perform its functionality, and it turns out that JavaScript developers are still writing desktop projects.

      The solution to this problem will again be to split the interface into several: IDesktop and IBackend. This is similar to the previous example, but here we are solving a different problem - preventing the class from doing more than it needs to.

    • And now the last and perhaps the most difficult principle to understand is dependency inversion.

      1. High-level classes must not depend on lower-level classes, but both must depend on abstractions.
      2. Abstractions should not depend on details, but details should depend on abstractions.

      What does this mean? And this means that high-level classes implement business rules or logic in the system. The lower-level classes deal with more detailed operations, such as working with a database, passing messages to the operating system, and so on. To achieve dependency inversion, we need to keep these high-level and low-level classes as loosely coupled as possible. And just for this, we write them dependent on abstractions, and not on each other.

      😭

      class Email {
        public void Send() {
          // Code to send email-letter
        }
      }
      
      class Notification {
        private Email email;
        public Notification() {
          email = new Email();
        }
      
        public void EmailDistribution() {
          email.Send();
        }
      }  
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
        

      🤩

      interface IMessenger {
        void Send();
      }
      
      class Email: IMessenger {
        public void Send() {
          /* Code to send email-letter */
        }
      }
      
      class SMS: IMessenger {
        public void Send() {
          /* Code to send SMS */
        }
      }
      
      class Notification {
        public IMessenger Messenger { get; set; }
      
        public Notification(IMessenger mess) {
          Messenger = mess;
        }
      
        public void Notify() {
          Messenger.Send();
        }
      }
      
      static void Main(string[] args) {
        var notification = new Notification(new Email());
        notification.Notify(); // Sent email
        notification.Messenger = new SMS(); // Change the provider
        notification.Notify(); // Sent SMS
      }

      Let's look at this principle using the example of sending messages. In the first code example, the Notification class is completely dependent on the Email class because it only sends one type of message. What if we want to send in some other way? Then we'll have to dig into the entire messaging system. This is a sign that the system is too tightly coupled.

      To make it loosely coupled in this case, we need to abstract away the Email message send provider. To do this, we create an interface IMessenger with a method Send and implement it in two classes - Email and SMS. We write the Notification class in such a way as to get rid of the specific implementation of the message distribution. In this case, we can use the Dependency Injection principle by passing the Messenger object through the constructor. And as a result, we will send messages of the class with which we are currently working. If we create Notification with Email Messenger, an email is sent. Next, we wanted to change the provider and assigned the Messenger properties to the SMS class, so the next call to the Notify method will already send SMS-ku .

    Each SOLID principle offers us a way to write logical, reliable, and understandable code, and C#, when followed by these principles, allows you to write large programs and easily extend them.

― That's all, folks! 🐷

I can talk about C# and .NET for a long time, but in this lecture I shared the main, in my opinion, for you at the moment. That's all, thanks for your attention, rate the lecture in your personal accounts, leave feedback, ask questions, do your homework and see you at the code review! 👋