Industrial programming - izznogooood/dotnet-wiki GitHub Wiki

Coding guidelines

Following a consistent set of naming conventions in developing code can be a major contribution to the code's usability. It allows the code to be used by many developers on widely separated projects. Beyond consistency of form, names of elements must be easily understood and must convey the function of each element.

Coding conventions serve the following purposes:

  • They create a consistent look to the code, so that readers can focus on content, not layout.
  • They enable readers to understand the code more quickly by making assumptions based on previous experience.
  • They facilitate copying, changing, and maintaining the code.
  • They demonstrate C# best practices.

All code must be in English!

Tip: Use a spellchecker to get the spelling right ✓

Identifier names

An identifier is the name you assign to a type (class, interface, struct, delegate, or enum), member, variable, or namespace. Valid identifiers must follow these rules:

  • Identifiers must start with a letter, or _.
  • Identifiers may contain Unicode letter characters.

Capitalize identifiers as follows:

  • The PascalCasing convention, used for all identifiers except parameter names, capitalizes the first character of each word (including acronyms over two letters in length):

    FileStream StringBuilder Days
    
  • The camelCasing convention, used only for parameter names, (private) fields and method variables, capitalizes the first character of each word except the first word:

    public User(DateTime dateOfBirth, string userName) : base(dateOfBirth) { UserName = userName; }
    

See the detailed naming guidelines for more information on the subject.

Naming conventions

Use the identifier naming conventions:

  • Interface names start with a capital I.
  • Add "Exception" as the suffix of every Exception class name you write.
  • Add "Async" as the suffix of every async method name you write.
  • Attribute types end with the word Attribute.
  • Enum types use a singular noun for non-flags, and a plural noun for flags.
  • Identifiers shouldn't contain two consecutive _ characters. Those names are reserved for compiler-generated identifiers.

Layout conventions

Good layout uses formatting to emphasize the structure of your code and to make the code easier to read. Follow these conventions:

  • Use the default Code Editor settings (smart indenting, four-character indents, tabs saved as spaces). For more information, see Options, Text Editor, C#, Formatting.
  • Write only one statement per line.
  • Write only one declaration per line.
  • If continuation lines are not indented automatically, indent them one tab stop (four spaces).
  • Add at least one blank line between method definitions and property definitions.
  • Use parentheses to make clauses in an expression apparent.

Commenting conventions

  • Place the comment on a separate line, not at the end of a line of code.
  • Begin comment text with an uppercase letter.
  • End comment text with a period.
  • Insert one space between the comment delimiter (//) and the comment text.
  • Don't create formatted blocks of asterisks around comments.
  • Ensure all public members have the necessary XML comments providing appropriate descriptions about their behaviour.

Tip: Use a documentation tool like GhostDoc to generate XML comments.

Coding conventions

  • Use string interpolation to concatenate short strings, as shown in the following code.

    string displayName = $"{LastName}, {FirstName}";
    
  • Use implicit typing for local variables when the type of the variable is obvious from the right side of the assignment, or when the precise type is not important. Don't use var when the type is not apparent from the right side of the assignment. Don't assume the type is clear from a method name. A variable type is considered clear if it's a new operator or an explicit cast.

  • Use implicit typing to determine the type of the loop variable in for loops.

  • Don't use implicit typing to determine the type of the loop variable in foreach loops.

  • In general, use int rather than unsigned types. The use of int is common throughout C#, and it is easier to interact with other libraries when you use int.

  • Use Func<> and Action<> instead of defining delegate types.

  • Use a try-catch statement for most exception handling.

  • Simplify your code by using the C# using statement.

    using Font font3 = new Font("Arial", 10.0f);
    byte charset3 = font3.GdiCharSet;
    
  • To avoid exceptions and increase performance by skipping unnecessary comparisons, use && instead of & and || instead of | when you perform comparisons.

  • LINQ queries

    • Use meaningful names for query variables.
    • Use aliases to make sure that property names of anonymous types are correctly capitalized, using Pascal casing.
    • Rename properties when the property names in the result would be ambiguous.
    • Use implicit typing in the declaration of query variables and range variables.
    • Align query clauses under the from clause.
    • Use where clauses before other query clauses to ensure that later query clauses operate on the reduced, filtered set of data.
    • Use multiple from clauses instead of a join clause to access inner collections.
    var scoreQuery = from student in students
                     from score in student.Scores
                     where score > 90
                     select new { Last = student.LastName, score }  ;
    

Don't repeat yourself (DRY)

The DRY principle is stated as "Every piece of knowledge must have a single, unambiguous, authoritative representation within a system". Rather than duplicating logic, encapsulate it in a programming construct. Make this construct the single authority over this behaviour, and have any other part of the application that requires this behaviour use the new construct.

Violations of DRY are typically referred to as WET solutions, commonly taken to stand for "write everything twice" (alternatively "write every time", "we enjoy typing" or "waste everyone's time").

UX guidelines

Part of the course is to build an app, a windows desktop app. Microsoft offers several technologies to create an app for Windows 11, see Overview of app development options.

The Windows UI Library (WinUI) 3 is the latest and recommended user interface (UI) framework for Windows desktop apps.

The WinUI 3 technology incorporates the Fluent Design System into all experiences, controls, and styles, WinUI provides consistent, intuitive, and accessible experiences using the latest UI patterns.

Extensive UX guidelines can be found in the Design and code Windows apps documentation.

We recommend using the WinUI 3 Controls Gallery app and the Microsoft To Do app as inspiration for good UX design.

WinUI 3 Controls Gallery app

We will also be using the Windows Template Studio extension for Visual Studio to create the WinUI 3 app, giving a well-formed starting point, with readable code that incorporates great development features while implementing proven patterns and best practices.

Quality measures

The Visual Studio IDE offers a number of features to guide you to high quality coding.

Code clean-up

You can define code style settings per-project, or for all code you edit in Visual Studio on the text editor Options page. For C# code, you can also configure Visual Studio to apply these code style preferences using the Code Cleanup command. You can also enforce the .NET coding conventions on build for all .NET projects. At build time, .NET code style violations will appear as warnings or errors with an "IDE" prefix. This enables you to strictly enforce consistent code styles in your codebase.

Code analysis

The .NET compiler inspect your code for code quality issues. If rule violations are found by an analyzer, they're reported as a suggestion, warning, or error, depending on how each rule is configured. Code analysis violations appear with the prefix "CA" or "IDE" to differentiate them from compiler errors.

analysis violations

Analyzer rules have default severity and suppression state that can be overwritten and customized for your project, see setting analyzer severities and suppressing analyzer violations for more information.

Code metrics

Code metrics is a set of software measures that provide insight into code. Bad metrics indicate that the code should be reworked or might require thorough testing. Visual Studio calculates the following measures:

  • Maintainability Index - Calculates an index value between 0 and 100 that represents the relative ease of maintaining the code. A high value means better maintainability. For more information, see Maintainability index range and meaning.

  • Cyclomatic Complexity - Measures the structural complexity of the code. It is created by calculating the number of different code paths in the flow of the program. Lower numbers are better.

  • Depth of Inheritance - Indicates the number of different classes that inherit from one another, all the way back to the base class. Depth of Inheritance is similar to class coupling in that a change in a base class can affect any of its inherited classes. The higher this number, the deeper the inheritance and the higher the potential for base class modifications to result in a breaking change. For Depth of Inheritance, a low value is good and a high value is bad.

  • Class Coupling - Measures the coupling to unique classes through parameters, local variables, return types, method calls, generic or template instantiations, base classes, interface implementations, fields defined on external types, and attribute decoration. Good software design dictates that types and methods should have high cohesion and low coupling. High coupling indicates a design that is difficult to reuse and maintain because of its many interdependencies on other types. For more information, see Class coupling.

  • Lines of Source code - Indicates the exact number of source code lines that are present in your source file, including blank lines.

  • Lines of Executable code - Indicates the approximate number of executable code lines or operations. This is a count of number of operations in executable code.

Metrics data can be calculated on your code by enabling metrics rules in the analyzer (described above), running it from the menu or the command line.

The thresholds given in the table below will give a good indication if your code has issues. Stay in the green! Keep clear of the red! 🤓

Metric Green Yellow Red
Maintainability Index 100-60 60-20 <20
Cyclomatic complexity <10 10-20 >20
Depth of Inheritance <10 10-20 >20
Class Coupling <10 10-20 >20
Lines of Executable Code <10 10-30 >30

Robustness

Defensive programming

  • Safeguard input data

    • Use a control that only allows values of the correct type to be entered, e.g., use the DatePicker to enter a date, the ToggleSwitch to enter a boolean or the RatingControl to enter a rating.
    • Validate any data that the user enters before the data is processed. Inform the user of any discrepancies in the data quality (and make sure they are corrected before proceeding).
    • Validate any data that are received through your APIs (REST endpoints). Return a 400 Bad Request to inform the client of any discrepancies in the data quality.
    • Validate parameters that enter your library code before any execution of business code. Throw an ArgumentException or any of the derived exception classes to inform the client of any discrepancies in the parameters provided.
  • Handle anticipated deviations (potential exceptions)

    The creation of an exception is an expensive task in .NET, it is therefore better practice to avoid the exception by checking for situations that might cause an exception beforehand. This is crucial when working with code that must be performant. Typical exception situations could be:

    • Missing Directory / File
    • Lack of database connectivity
    • Lost database connection
    // Safeguard parameter
    if (string.IsNullOrEmpty(filePath))
        throw new ArgumentException("The filePath cannot be empty.", nameof(filePath));
    
    // Verify that the file exists, or at least the directory
    if (!File.Exists(filePath))
    {
        directoryPath = Path.GetDirectoryName(filePath);
        if (string.IsNullOrEmpty(filePath))
            throw new ArgumentException("The directory cannot be empty.", nameof(filePath));
    
        if (!Directory.Exists(directoryPath))
        {
            // We'll let the caller handle any issues with creation of the directory
            Directory.CreateDirectory(directoryPath);
        }
    }
    
    // The file or at least the directory exists, it is now safe to append text to the file
    using StreamWriter sw = File.AppendText(filePath);
    sw.WriteLine(text);
    
  • Guard against SQL injection

    SQL injection is a code injection technique used to attack data-driven applications, in which malicious SQL statements are inserted into an entry field for execution, see Technical implementations for more information.

    Frameworks like Entity Framework Core used in this course, can help guard against these attacks, but are not fool proof,see Prevent SQL injection attacks.

    The solution is to use parameterized queries see passing parameters in EF Core raw sql queries and ADO.NET passing parameters.

Exception handling

Exceptions are used to indicate that an error has occurred while running the program. Exception handling uses the try, catch, and finally keywords to try actions that may not succeed, to handle failures when you decide that it's reasonable to do so, and to clean up resources afterward.

Use a try block around the statements that might throw exceptions, see Exception Handling. You can find out what exceptions a method might throw by examining the exceptions section, see e.g., the exceptions section for the File.WriteAllText.

See the Exceptions Overview for good guidance.

If the statement that throws an exception isn't within a try block or if the try block that encloses it has no matching catch block, the runtime checks the calling method for a try statement and catch blocks. The runtime continues up the calling stack, searching for a compatible catch block. After the catch block is found and executed, control is passed to the next statement after that catch block.

A try statement can contain more than one catch block. The first catch statement that can handle the exception is executed; any following catch statements, even if they're compatible, are ignored. Order catch blocks from most specific (or most-derived) to least specific. Before the catch block is executed, the runtime checks for finally blocks. Finally blocks enable the programmer to clean up any ambiguous state that could be left over from an aborted try block, or to release any external resources.

  • User defined exceptions

    Define your own exception classes to identify errors specific to your code, and that can contain details about the exception. When creating an exception class, this class must derive from Exception and the name of the class must end with Exception. The derived classes should define at least four constructors: one parameter less constructor, one that sets the message property, and one that sets both the Message and InnerException properties. The fourth constructor is used to serialize the exception. New exception classes should be serializable.

    [Serializable]
    public class InvalidDepartmentException : Exception
    {
        public InvalidDepartmentException() : base() { }
        public InvalidDepartmentException(string message) : base(message) { }
        public InvalidDepartmentException(string message, Exception inner) : base(message, inner) { }
    
        // A constructor is needed for serialization when an
        // exception propagates from a remoting server to the client.
        protected InvalidDepartmentException(System.Runtime.Serialization.SerializationInfo info,
            System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
    }
    

    Add new properties to the exception class when the data they provide is useful to resolving the exception. If new properties are added to the derived exception class, ToString() should be overridden to return the added information.

    In a localized application, you want to have different messages depending on user culture, follow this guide to create localized exception messages.

  • Throwing exceptions

    Exceptions are used to indicate that an error has occurred while running the program. Exception objects that describe an error are created and then thrown with the throw keyword. See this list of conditions that should throw an exception. Always remember to safeguard arguments:

    _ = filePath ?? throw new ArgumentNullException("Parameter cannot be null", nameof(filePath));
    

    Things to avoid when throwing exceptions.

    Do not throw System.Exception, System.SystemException, System.ApplicationException, System.NullReferenceException, or System.IndexOutOfRangeException intentionally from your own source code.

  • Re-throwing exceptions

    Throw can also be used in a catch block to re-throw an exception handled in a catch block.

    It is good coding practice to add information to an exception that is re-thrown to provide more information when debugging.

    FileStream fs;
    try
    {
        // Opens a text tile.
        fs = new FileStream(@"C:\temp\data.txt", FileMode.Open);
        var sr = new StreamReader(fs);
    
        // A value is read from the file and output to the console.
        string line = sr.ReadLine();
        Console.WriteLine(line);
    }
    catch(FileNotFoundException e)
    {
        Console.WriteLine($"[Data File Missing] {e}");
        throw new FileNotFoundException(@"[data.txt not in c:\temp directory].", e);
    }
    finally
    {
        if (fs != null)
            fs.Close();
    }
    

    It is also useful to re-throw an exception caught in a catch-block after logging the exception. Note that when the exception is re-thrown you just use the throw statement (without the exception), this preserves the call stack:

    catch(FileNotFoundException e)
    {
        LogError("Could not read the database file.", e);
        throw;
    }
    

Make sure to always handle exceptions in your client code. An unhandled exception will cause your code to be terminated. Take special care in the following situations:

  • The user interface of an app typically initiates actions based on user interaction. Make sure to handle any exceptions thrown while performing these actions. E.g., wrap code in a button pressed handler in a try-catch block, and inform the user of any issues while gracefully handling the exception.

  • REST endpoints should handle any exceptions gracefully, and return sensible information of the problem, rather than returning the exception to the client.

See best practices for exceptions.

Handling resources

For the majority of the objects that your code creates, you can rely on the .NET garbage collector to handle memory management. However, when you create objects that include unmanaged resources, you must explicitly release those resources when you finish using them. Samples of objects that include unmanaged resources are:

  • Database connections (e.g., SqlConnection)
  • Files
  • Fonts
  • Windows
  • Network connections

The using statement provides a convenient syntax that ensures the correct use of IDisposable and IAsyncDisposable objects. When the lifetime of an IDisposable object is limited to a single method, you should declare and instantiate it in the using statement. The using statement calls the Dispose method on the object in the correct way, and (when you use it as shown earlier) it also causes the object itself to go out of scope as soon as Dispose is called. Within the using block, the object is read-only and can't be modified or reassigned. If the object implements IAsyncDisposable instead of IDisposable, the using statement calls the DisposeAsync and awaits the returned ValueTask.

using SqlConnection connection = new(connectionString);
SqlCommand command = new(queryString, connection);
command.Connection.Open();
command.ExecuteNonQuery();

Tip: Examine if a class implements IDisposable and/or IAsyncDisposable, if that is the case you should use using!