Skip to content

user3301/dotnetcore_best_practice

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 

Repository files navigation

.Net Core Best Practice

License: GPL

In this repository, you can find some coding and naming conventions in C# (.Net Core 1.x + specifically).

Table of Contents

Introduction

namingmeme

Standardization has a positive impact on any business as well as in the Software industry. There are certain coding standards that are needed for a successful implementation of a program. Good quality software and code is not as easy as it seems to be. Often it requires consistent efforts and sheer focus of the software development team to meet the quality goals. This repository is a C# programming language coding guide to producing readable, reusable and scalable code.

Inspired by clean-code-javascript, clean-code-php and clean-code-dotnet lists.

Coding and Naming Conventions

Naming

Use intention-revealing names A good name does not only improve the readability of the code but also enhance working efficiency as it helps other team members to understand your code.

Bad:

This is a bad naming of variable as it does not reflect what it means in the given context, or at last we need to read other parts of the code to find out.

int d;

Good:

int elapsedTimeInDays;
int daySinceModification;
int daysSinceCreation;
int fileAgeInDays;

These names are good practice as it reveal your intentions.

⬆ Back to top

Avoid disinformation

Sometimes coders need to "hide" some implementation in the back stage, however they tend also to create confusing parts of code.

Bad:

int[] randomNumberList;

"randomNumberList" is not actually a list type data structure.

Good:

int[] randomNumbers;

Real-life example:

//from Netty, use `group` as the naming convention for a collection of objects, it avoid causing misleading and also hide the implementation of actual data structure
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

⬆ Back to top

Good names length

Long variable names are no longer a problem in modern software development as we have powerful IDEs to help us navigate in code. Nevertheless the problem is that this still can introduce a naming chaos in code.

Bad:

Account[] theUserAccountArrayIncludesBothActivatedAndDeactivated;

Good:

Account[] allAccounts;

⬆ Back to top

Keep notation consistent

Notation means a programming language's own "style". Coders should cook code that matches this notation because other programmers probably know it and use it.

Bad:

const int threshold = 100;
public string USERNAME;
private string Name;
void printname();

Good:

const int THRESHOLD = 100;
public string UserName;
private string _name;
void PrintName();

C# variable & function naming conventions:

Object Name Notation Length Plural Prefix Suffix Abbreviation Char Mask Underscores
Class name PascalCase 128 No No Yes No [A-z][0-9] No
Constructor name PascalCase 128 No No Yes No [A-z][0-9] No
Method name PascalCase 128 Yes No No No [A-z][0-9] No
Method arguments camelCase 128 Yes No No Yes [A-z][0-9] No
Local variables camelCase 50 Yes No No Yes [A-z][0-9] No
Constants name PascalCase 50 No No No No [A-z][0-9] No
Field name camelCase 50 Yes No No Yes [A-z][0-9] Yes
Properties name PascalCase 50 Yes No No Yes [A-z][0-9] No
Delegate name PascalCase 128 No No Yes Yes [A-z] No
Enum type name PascalCase 128 Yes No No No [A-z] No

⬆ Back to top

Use pronounceable names

Human are good at memorizing pronounceable words. So make variable names pronounceable.

Bad:

public class DtaRcrd102
{
    public Datetime genymdhms { get; set; } // what the programmer want to tell is generation timestamp in yy-mm-dd-hh-mm-ss format
    public Datetime modymdhms { get; set; } // modeification timestamp in yy-mm-dd-hh-mm-ss format
}

Good:

public class Customer
{
    public Datetime GenerationTimestamp { get; set; }
    public Datetime ModificationTimestamp { get; set; }
}

⬆ Back to top

Use domain name

Remember that the people who read your code will be programmers. So go ahead and use CS terms, algorithm names, math terms and pattern names and so on so that when will know the concept by seeing the names.

Bad:

 public int Fibonacci(int N) {
        var arr = new int[N]; // bad 

        if(N==0) return 0;
        if(N==1 || N == 2) return 1;
            arr[0] = 1;
            arr[1] = 1;

            for (int i = 2; i < N; i++)
            {
                arr[i] = arr[i-1] + arr[i-2];
            }
            return arr[N-1];
    }

Good:

public int Fibonacci(int N) {
        var dp = new int[N]; // good. Programmers with algorithm knowledge will know dynamic programming is used in this function and will understand this array is used to cache previous calculated solution of a sub-problem

        if(N==0) return 0;
        if(N==1 || N == 2) return 1;
            dp[0] = 1;
            dp[1] = 1;

            for (int i = 2; i < N; i++)
            {
                dp[i] = dp[i-1] + dp[i-2];
            }
            return dp[N-1];
    }

Bad:

 class GumballMachine
 {
     public IEntity Sold;
     public IEntity SoldOut;
 }

Good:

class GumballMachine
 {
     // State pattern is used here
     public IState SoldState;
     public IState SoldOutState;
 }

⬆ Back to top

Final words Here are some `Do` s and `Don't` s that may help you name variable better:
  1. Do use PascalCasing for class names and method names; Example:
public class Foo
{
    public void GetFooDetails()
    {
      //...
    }
}
  1. Do use camelCasing for method arguments and local variables; Example:
public class FooBar
{
  public void Save(FooBar fooBar)
   {
       // ...
   }
} 
  1. Don't use Abbreviations; Example:
// Correct
DocumentProfile documentProfile;
 
// Avoid
DocumentProfile docProf;
  1. Don't use Underscores in identifiers; Example:
// Correct
FooBar fooBar;
 
// Avoid
FooBar foo_Bar; 
  1. Do prefix interfaces with letter I; Example:
//Correct
public interface IFooBar{}

//Avoid
public interface FooBar{};
  1. Do declare all member variables at the top of a class, with static variables at very top;
public class Contact
{
  public static string Name;

  public string Address{set;get;}
}

⬆ Back to top

Variables

Avoid mental mapping

Clean code should be easy to read, understand and should leave no room for guesswork. The following code checks if a user can withdraw a certain amount of money from his/her bank account. It's messy as there is a lengthy condition statement.

Bad:

public bool Withdraw(User user, double amount)
{
    if(user.Balance > amount && user.ActiveLoan == 0)
    {
        user.Balance -= amount;
        return true;
    }
    else return false;
}

Good:

public bool Withdraw(User user, double amount)
{
    if(HasNoActiveLoan(user) && CanWithdrawAmount(double amount))
    {
        user.Balance -= amount;
        return true;
    }
    else false;
}

private bool HasNoActiveLoan(User user) => user.ActiveLoan ==0;
private bool HasNoActiveLoan(double amount) => user.Balance > amount;

This is not only easier to understand but also easier to test.

Bad:

var a = FruitBasket.GetApple();
var o = FruitBasket.GetOrange();
var s = Blender.MakeSmoothie(a,o);

Good:

var apple = FruitBasket.GetApple();
var orange = FruitBasket.GetOrange();
var smoothie = Blender.MakeSmoothie(apple, orange);

⬆ back to top

Avoid using "magic" number or string

A magic number or string is basically a hard-coded value that might change at a later stage. Since it has the chances of changing at a later stage, it can be said that the number is hard to update.

Bad

public class Foo {
    public void SetPassword(string password) {
         // 7 is the magic number
         if (password.Length() > 7) {
              throw new InvalidArgumentException("password");
         }
    }
}

Good

public class Foo {
    public const int MAX_PASSWORD_SIZE = 7;

    public void SetPassword(string password) {
         if (password.Length() > MAX_PASSWORD_SIZE) {
              throw new InvalidArgumentException("password");
         }
    }
}

It improves readability of the code and it's easier to maintain. Once there are multiple places that the length of the password needs to be checked, when the max size is changed, programmers need to change in all code locations with that magic number. Missing any one will lead to inconsistencies.

⬆ back to top

Don't add gratuitous context

Shorter names are generally better than longer ones, so long as they are clear. Add no more context to a name than is necessary.

Bad:

public class User
{
    public string UserEmail { get; set; }
    public string UserFirstName { get; set; }
    public string UserLastName { get; set; }

    //...
}

Imagine you type 'U' and press the completion key and are rewarded with all the properties in this object. Does this make your life easier?

Good:

public class User
{
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    //...
}

⬆ back to top

Use searchable names

One might easily grep for DAYS_IN_KNUCKLE_MONTH, but the number 31 could be more troublesome. Searches may turn up the digit as part of file names, other constant definition, and in various expressions where the value is used with different intent.

Bad:

int s = 0;
for(int i =1; i<= 31; ++i)
{
  s += (t[i]*4)
}

Good:

int realDaysPerIdealDay = 4;
public const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;

for(int i=1; i<= NUMBER_OF_TASKS;++i)
{
  int realTaskDays = taskEstimate[i] * realDaysPerIdealDay;
  int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
  sum += realTaskWeeks;
}

Bad:

It's hard to navigate to the location where transcriptionStatus is error and see what happens unless 0 is mentally "mapped" to error.

var transcriptionStatus = DocumentDB.GetTranscriptStatus();
if(transcriptionStatus < 0) return StatusCode(500);

Good:

public enum TranscriptionStatusEnum:short
{
   error = -1,
   analyzing = 1,
   done = 2,
   received = 3
}

var transcriptionStatus = DocumentDB.GetTranscriptStatus();
if(transcriptionStatus == TranscriptionStatusEnum.error) return StatusCode(500);

⬆ Back to top

Functions

Keep function arguments as fewer as possible The ideal number of arguments for a function is zero. Next comes one, followed closely by two. Three arguments should be avoided where possible.

Bad:

DataBase.InitializeConnection(string endpointURL, string authorizationKey, string databaseGuid, string collectionName)
{
  // establish database connection
}

Good:

var dbConfig = new DatabaseConfiguration
{
  EndpointURL = "http:://azure.com:443",
  authorizationKey = "sdasdasd24nsodfj1o234sadn",
  DatabaseGuid = "213d-3dfsdf-12asdd-123a",
  CollectionName = "UserDocument"
}

DataBase.InitialieConnection(DatabaseConfiguration dbConfig)
{
  // establish database connection
}

⬆ Back to top

Keep indentation consistent Keep your indentation style consistent is a good way to improve your code readability and keep it nice and concise.

Style 1:

void Foo() {
  if(condition) {
    //do something
  } else {
    //do something
  }
}

Style 2:

void Foo()
{
  if(condition)
  {
    //do something
  }
  else
  {
    //do something
  }
}

Style 3:

void Foo()
{ if(condition)
  { //do something
  }
  else
  { //do something
  }
}

Indentation of choice is only a matter of preference unless your language of choice has a strict rule of indentation(Golang enforce curly bracket to not be on the next line). Based on my personal experience Style 2 is preferred by .Net and .Net Core developer whereas Style 1 is more for Java guys.

⬆ Back to top

Function should do one thing

Bad:

public void SendEmailToClients(int[] groupsOfClientIds)
{
  foreach(var clientId in groupsOfClientIds)
  {
      var record = DocumentEngine.FindRecordById(clientId)
      if(record.Status == RecordStatusEnum.Active) SendEmail(record.Email);      
  }
}

Good:

public void SendEmailToClients(int[] groupsOfClientIds)
{
  foreach(var clientId in groupsOfClientIds)
  {
      var record = DatabaseClient.FindRecordById(clientId)
      var emails = GetActiveClientIds(groupsOfClientIds);
      foreach(var email in emails) SendEmail(email);     
  }
}

private string[] GetActiveClientIds(int[] groupsOfClientIds)
{
   return DocumentEngine.FindAllRecords(groupsOfClientIds).Where(s=>s.Status == RecordStatusEnum.Active).Select(x=>x.Email);
} 

⬆ Back to top

Use local functions when necessary C# supports local functions since C# 7.0. Local functions are private methods of a type that are nested in another member. They can only be called from their containing member. Local functions can be declared in and called from:
  1. Methods, especially iterator methods and async methods;

  2. Constructors;

  3. Property accessors;

  4. Event accessors;

  5. Anonymous expressions;

  6. Finalizers;

  7. Other local functions;

Bad:

public static partial class Utility
{
  #region  private method
  private static string GetText(string path, string filename)
  {
    var sr = File.OpenText(AppendPathSeparator(path) + filename);
         var text = sr.ReadToEnd();
         return text;
  }

// this method is only called inside GetText method
  private static string AppendPathSeparator(string filepath)
  {
    if (! filepath.EndsWith(@"\"))
               filepath += @"\";

            return filepath;   
  }
  #endregion
}

As method GetText is a private method in the Utility class, The AppendPathSeparator method is only used inside of GetText method. We could move AppendPathSeparator method declaration into GetText method to avoid contaminate the global environment.

Good:

 private static string GetText(string path, string filename)
    {
         var sr = File.OpenText(AppendPathSeparator(path) + filename);
         var text = sr.ReadToEnd();
         return text;
         
         // Declare a local function.
         string AppendPathSeparator(string filepath)
         {
            if (! filepath.EndsWith(@"\"))
               filepath += @"\";

            return filepath;   
         }
    } 

⬆ Back to top

Replace conditional with polymorphism when possible

Bad:

class Car 
{
  public MakeEnum Make {get; set;}
  //...
  double GetSpeed()
  {
    switch(Make)
    {
      case MakeEnum.European:
        return GetBaseSpeed();
      case MakeEnum.Asian:
        return GetBaseSpeed() + GetCylinders() * MAXSPEEDPERCYLINDER;
      case MakeEnum.American:
        return (Is4WD)? 100: GetBaseSpeed();
    }
    throw new NotImplementedException();
  }
}

Good:

abstract class Car
{
  abstract double GetSpeed();
}

class European: Car
{
  public double GetBaseSpeed() => 100d;

  override double GetSpeed()
  {
    return GetBaseSpeed();
  }
}

class Asian:Car
{
  public int Cylinders {get; set;}

  public double GetBaseSpeed()=> 110d;

  public const double MAXSPEEDPERCYLINDER = 20;

  override double GetSpeed()
  {
    return GetBaseSpeed() + Cylinders * MAXSPEEDPERCYLINDER;
  }
}

class American:Car
{
  public double GetBaseSpeed() => 90d;

  override double GetSpeed()
  {
    return Is4WD? 100: GetBaseSpeed();
  }
}


//somewhere in client side's code

var speed = car.GetSpeed();

⬆ Back to top

Use named argument when method argument list is long

Bad:

PrintEmailDetails("Kanye West", "Jay-Z", "Dr.Dre")

static void PrintEmailDetails(string sender, string recipient, string cc)
{
  Console.WriteLine($"Sender:{sender}, Recipient: {recipient}, CC: {cc}");
}

Good:

PrintEmailDetails(sender:"Kanye West", recipient:"Jay-Z", cc:"Dr.Dre")

static void PrintEmailDetails(string sender, string recipient, string cc)
{
  Console.WriteLine($"Sender:{sender}, Recipient: {recipient}, CC: {cc}");
}

Named arguments free you from the need to remember or to look up the order of parameter in the parameter lists of called methods.

⬆ Back to top

Use default arguments to reduce conditionals in code block The definition of a method, constructor, indexer, or delegate can specify that its parameters are required or that they are optional. Any call must provide arguments for all required parameters, but can omit arguments for optional parameters.

Each optional parameter has a default value as part of its definition. If no argument is sent for that parameter, the default value is used.

Bad:

public class Contact
{
  public string Name{get;set;}
  public string Address{get; set;}
  public string Email{get;set;}
  
  public Contact(string name, string address, string email)
  {
    if(string.IsNullOrWhiteSpace(name)) throw new ArgumentException(message: "Name cannot be null or empty.", paramName: nameof(name));
    else Name = name;

    if(string.IsNullOrWhiteSpace(address)) throw new ArgumentException(message: "Address cannot be null or empty.", paramName: nameof(address));
    else Address = address;

    email = string.IsNullOrEmpty(email)? "Not Available": email; 
  }
}

Good:

public class Contact
{
  public string Name{get;set;}
  public string Address{get; set;}
  public string Email{get;set;}
  
  public Contact(string name, string address, string optionalEmail = "Not Available")
  {
    if(string.IsNullOrWhiteSpace(name)) throw new ArgumentException(message: "Name cannot be null or empty.", paramName: nameof(name));
    else Name = name;

    if(string.IsNullOrWhiteSpace(address)) throw new ArgumentException(message: "Address cannot be null or empty.", paramName: nameof(address));
    else Address = address;

    email = optionalEmail;
  }
}

⬆ Back to top

Use Async and Await for IO bound functions

Summary of Asynchronous Programming Guidelines

Name Description Exceptions
Avoid async void Prefer async Task methods over async void methods Event handlers
Async all the way Don't mix blocking and async code Console main method (C# <= 7.0)
Configure context Use ConfigureAwait(false) when you can Methods that require con­text

The Async Way of Doing Things

To Do This ... Instead of This ... Use This
Retrieve the result of a background task Task.Wait or Task.Result await
Wait for any task to complete Task.WaitAny await Task.WhenAny
Retrieve the results of multiple tasks Task.WaitAll await Task.WhenAll
Wait a period of time Thread.Sleep await Task.Delay

Best practice

The async/await is the best for IO bound tasks (networking communication, database communication, http request, etc.) but it is not good to apply on computational bound tasks (traverse on the huge list, render a hugge image, etc.). Because it will release the holding thread to the thread pool and CPU/cores available will not involve to process those tasks. Therefore, we should avoid using Async/Await for computional bound tasks.

For dealing with computational bound tasks, prefer to use Task.Factory.CreateNew with TaskCreationOptions is LongRunning. It will start a new background thread to process a heavy computational bound task without release it back to the thread pool until the task being completed.

Know Your Tools

There's a lot to learn about async and await, and it's natural to get a little disoriented. Here's a quick reference of solutions to common problems.

Solutions to Common Async Problems

Problem Solution
Create a task to execute code Task.Run or TaskFactory.StartNew (not the Task constructor or Task.Start)
Create a task wrapper for an operation or event TaskFactory.FromAsync or TaskCompletionSource<T>
Support cancellation CancellationTokenSource and CancellationToken
Report progress IProgress<T> and Progress<T>
Handle streams of data TPL Dataflow or Reactive Extensions
Synchronize access to a shared resource SemaphoreSlim
Asynchronously initialize a resource AsyncLazy<T>
Async-ready producer/consumer structures TPL Dataflow or AsyncCollection<T>

Read the Task-based Asynchronous Pattern (TAP) document. It is extremely well-written, and includes guidance on API design and the proper use of async/await (including cancellation and progress reporting).

There are many new await-friendly techniques that should be used instead of the old blocking techniques. If you have any of these Old examples in your new async code, you're Doing It Wrong(TM):

Old New Description
task.Wait await task Wait/await for a task to complete
task.Result await task Get the result of a completed task
Task.WaitAny await Task.WhenAny Wait/await for one of a collection of tasks to complete
Task.WaitAll await Task.WhenAll Wait/await for every one of a collection of tasks to complete
Thread.Sleep await Task.Delay Wait/await for a period of time
Task constructor Task.Run or TaskFactory.StartNew Create a code-based task

Source https://gist.github.com/jonlabelle/841146854b23b305b50fa5542f84b20c

⬆ back to top

Classes

Unit Testing

Tools

Source control

Source control is an absolute necessity for any software development project. If you are not using one yet, start using one.

  • GitHub - allows for unlimited public repositories, and unlimited private repositories with up to 3 collaborators.
  • Bitbucket - allows for unlimited private repositories with up to 5 collaborators, for free.
  • SourceForge - open source hosting only.
  • GitLab - allows for unlimited public and private repositories, unlimited CI Runners included, for free.
  • Visual Studio Online (http://www.visualstudio.com/what-is-visual-studio-online-vs) - allows for unlimited public repositories, must pay for private repository. Repositories can be git or TFVC. Additionally: Issue tracking, project planning (multiple Agile templates, such as SCRUM), integrated hosted builds, integration of all this into Microsoft Visual Studio. Windows only.
  • Gitee - allows for unlimited private repositiories up to 5G file storage

⬆ Back to top

Continuous integration

Once you have picked your build tool, set up a continuous integration environment.

Continuous Integration (CI) tools automatically build the source code as changes are pushed to the repository. These can be hosted privately or with a CI host.

  • Travis CI
    • works well with C#
    • designed for use with GitHub
    • free for public repositories on GitHub
  • AppVeyor
    • supports Windows, MSVC and MinGW
    • free for public repositories on GitHub
  • Hudson CI / Jenkins CI
    • Java Application Server is required
    • supports Windows, OS X, and Linux
    • extendable with a lot of plugins
  • TeamCity
    • has a free option for open source projects
  • Decent CI
    • simple ad-hoc continuous integration that posts results to GitHub
    • supports Windows, OS X, and Linux
    • used by ChaiScript
  • Visual Studio Online (http://www.visualstudio.com/what-is-visual-studio-online-vs)
    • Tightly integrated with the source repositories from Visual Studio Online
    • Uses MSBuild (Visual Studio's build engine), which is available on Windows, OS X and Linux
    • Provides hosted build agents and also allows for user-provided build agents
    • Can be controlled and monitored from within Microsoft Visual Studio
    • On-Premise installation via Microsoft Team Foundation Server
  • GitLab
    • has free shared runners
    • has trivial processing of result of coverage analyze

If you have an open source, publicly-hosted project on GitHub:

These tools are all free and relatively easy to set up. Once they are set up you are getting continuous building, testing, analysis and reporting of your project. For free.

⬆ Back to top

Releases

No releases published

Packages

No packages published