The S.O.L.I.D Design Principles in C# and .Net

  • Eric Lu
  • 11/12/2020
  • 66
  • 82
The S.O.L.I.D Design Principles in C# and .Net

In Object-Oriented Programming, S.O.L.I.D refers to the first five design principle for the purpose of making software designs more understandable, flexible, and maintainable. The principles was first introduced by Robert C. Martin in his paper Design Principles and Design Patterns in 2000. Michael Feathers introduced the S.O.L.I.D acronym later in that year.

  S.O.L.I.D stands for:

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Understanding each principle individually helps developers to combat the rigidity, fragility, immobility, and viscosity of complex applications. Let's take a look!

1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

This means that everything in a class should be related to a single purpose. However, it doesn't mean that the class is allowed to have only one method or property. Instead, it can have many members that serve a single responsibility.

In an early phase of software development, we should think of all the possibilities that a class can change and identify the responsibility of each. The way of thinking like this is the initial implementation of the Single Responsibility Principle. Once you have a clear picture of the application, it will be easier to identify the responsibility for each class.

Consider below example when you are developing an application which has User Registration functionality.

public class UserManager  
{  
   SmtpClient _smtpClient;  
   public UserManager  (SmtpClient smtpClient)  
   {  
      _smtpClient = smtpClient;  
   }  
   public void Register(string email, string password)  
   {  
      if (!ValidateEmail(email))  
         throw new ValidationException("Email is not an email");  
         var user = new User(email, password);  
   
         SendEmail(new MailMessage("[email protected]", email) { Subject="Welcome!" });  
   }
   public virtual bool ValidateEmail(string email)  
   {  
     return email.Contains("@gmail.com");  
   }  
   public bool SendEmail(MailMessage message)  
   {  
     _smtpClient.Send(message);  
   }  
}

The code is totally fine! However, it doesn't implement the Single Responsibility Principle because the UserManager class is serving 2 responsibilities for User and Email. Let's make this piece follow the SRP:

public class UserManager  
{  
   EmailManager _emailManager;  
   public UserManager  (EmailManager emailManager)  
   {  
      _emailManager = emailManager;  
   }  
   public void Register(string email, string password)  
   {  
      if (!_emailManager.ValidateEmail(email))  
         throw new ValidationException("Email is not an email");  
         var user = new User(email, password);  
 
         emailManager.SendEmail(new MailMessage("[email protected]", email) {Subject="Welcome!"});  
   
      }  
   }  
} 
 
public class EmailManager
{
    SmtpClient _smtpClient;
    public EmailManager(SmtpClient smtpClient)
    {
        _smtpClient = smtpClient;
    }
    public bool virtual ValidateEmail(string email)
    {
        return email.Contains("@gmail.com");
    }
    public bool SendEmail(MailMessage message)
    {
        _smtpClient.Send(message);
    }
}

So, by adding EmailManager class, we separated the responsibilities. UserManager is serving User and EmailManager is serving Email.

2. Open-Closed Principle (OCP)

Classes should be open for extension but closed for modification.

A class should be "open for extension" means it should be able to be added new features when there are new requirements. However, it also should be "closed for modification, which means all completed functionalities should not be modified unless there is a bug. A good way to approach this is by using inheritance. 

Let's dive in an example of a program to calculate Employee and Contractor salary below. We have 2 classes called Employee and Contractor:

public class Employee {  
   public string FullName { get; set;}  
   public double AnnualRate { get; set; }  
}
public class Contractor {  
   public string FullName { get; set;}  
   public double HourlyRate { get; set; }  
}

So, let's follow the SRP that we discussed above to create a separate class to calculate an expense the company has to spend on salary, called BudgetManager.

public class BudgetManager  
{  
   public double MonthlySalaryExpense(List<object> objects)  
   {  
      double monthlySalary = 0;

      foreach (var o in objects)
      {
         if (o is Employee)  
         {    
            monthlySalary += o.AnnualRate/12;  
         }  
         else
         {  
             monthlySalary += o.HourlyRate * 8 * 5 * 4; 
         }  
      }
      return monthlySalary ;  
   }  
}

As we can see, the 3 classes are perfectly applying the SRP. However, if we have a new role, Intern for example, requires a different way to calculate salary, it will violate the OCP because we need to modify the if else statement in BudgetManager class to achieve this goal.

One solution to resolve it is to use abstract or interface for inheritance. Let's see below:

public abstract class Staff {  
   public abstract double MonthlySalary();
}
public class Employee : Staff {  
   public string FullName { get; set;}  
   public double AnnualRate { get; set; } 
 
   public override double MonthlySalary()  
   {  
      return AnnualRate/12;  
   }  
}
public class Contractor : Staff {  
   public string FullName { get; set;}  
   public double HourlyRate { get; set; }  
 
   public override double MonthlySalary()  
   {  
      return HourlyRate * 8 * 5 * 4;  
   }  
}
public class BudgetManager  
{  
   public double MonthlySalaryExpense(List<object> objects)  
   {  
      double monthlySalary = 0;
 
      foreach (var o in objects)
      {
         monthlySalary += o.MonthlySalary(); 
      }
      return monthlySalary ;  
   }  
}

Now, when you have a class called Intern, you don't need to go back to the existing classes to modify the MonthlySalary function.

public class Intern : Staff {  
   public string FullName { get; set;}  
   public double HourlyRate { get; set; }  
   public double WorkingDaysPerWeek {get; set; }
   public double HoursPerDay {get; set; }
 
   public override double MonthlySalary()  
   {  
      return HourlyRate * HoursPerDay * WorkingDaysPerWeek * 4;  
   }  
}


3. Liskov Substitution Principle (LSP)

The program should be able to substitute a base type for a sub type and have it behaves the same without any modification.

According to the third principle, a derived class should be able to be used instead of it's parent class and the program behaves in the same manner without any modification. The goal is to make sure that the derived class doesn't affect the behavior of the parent class.

Considering the BudgetManager example discussed above. It actually implements the LSP when we use Employee, Contractor, and Intern types to pass into MonthlySalaryExpense function which take a list of Staff as a parameter. See below the BudgetManager class again and how I call the function to calculate the MonthlySalaryExpense.

public class BudgetManager  
{  
   public double MonthlySalaryExpense(List<Staff> staffs)  
   {  
      double monthlySalary = 0;
      foreach (var s in staffs)
      {
         monthlySalary += s.MonthlySalary(); 
      }
      return monthlySalary ;  
   }  
}
static void Main(string[] args)
{
    Staff employee1 = new Employee()
    {
        FullName = "Emp1",
        AnnualRate = 92000.0
    };
 
    Staff contractor1 = new Contractor()
    {
        FullName = "Con1",
        HourlyRate = 45
    };
 
    Staff intern1 = new Intern()
    {
        FullName = "Int1",
        HourlyRate = 28,
        WorkingDaysPerWeek = 3,
        HoursPerDay = 7
    };
 
    List<Staff> staffs = new List<Staff>();
    staffs.Add(employee1);
    staffs.Add(contractor1);
    staffs.Add(intern1);
 
    BudgetManager budManager = new BudgetManager();
    var monthlySalaryExpense = budManager.MonthlySalaryExpense(staffs);
}


4. Interface Segregation Principle (ISP)

Interfaces should be split as mush as possible to avoid problems that a client is forced to implement unused functions.

The Interface Segregation Principle states "that clients should not be forced to implement interfaces they don't use. Instead of one fat interface, many small interfaces are preferred based on groups of methods, each one serving one submodule."

We can define it in another way. An interface should be more closely related to the code that uses it than code that implements it. So the methods on the interface are defined by which methods the client code needs rather than which methods the class implements. So clients should not be forced to depend upon interfaces that they don't use.

Like classes, each interface should have a specific purpose/responsibility (refer to SRP). You shouldn't be forced to implement an interface when your object doesn't share that purpose. The larger the interface, the more likely it includes methods that not all implementers can do. That's the essence of the Interface Segregation Principle. Let's start with an example that breaks the ISP. Suppose we need to build a system for an IT firm that contains roles like TeamLead and Programmer where TeamLead divides a huge task into smaller tasks and assigns them to his/her programmers or can directly work on them.

Based on specifications, we need to create an interface and a TeamLead class to implement it. 

public Interface ILead  
{  
   void CreateSubTask();  
   void AssginTask();  
   void WorkOnTask();  
}  

public class TeamLead : ILead  
{  
   public void AssignTask()  
   {  
      //Code to assign a task.  
   }  
   public void CreateSubTask()  
   {  
      //Code to create a sub task  
   }  
   public void WorkOnTask()  
   {  
      //Code to implement perform assigned task.  
   }  
}

OK. The design looks fine for now. Later another role like Manager, who assigns tasks to TeamLead and will not work on the tasks, is introduced into the system. Can we directly implement an ILead interface in the Manager class, like the following?

public class Manager: ILead  
{  
   public void AssignTask()  
   {  
      //Code to assign a task.  
   }  
   public void CreateSubTask()  
   {  
      //Code to create a sub task.  
   }  
   public void WorkOnTask()  
   {  
      throw new Exception("Manager can't work on Task");  
   }  
}

Since the Manager can't work on a task and at the same time no one can assign tasks to the Manager, this WorkOnTask() should not be in the Manager class. But we are implementing this class from the ILead interface, we need to provide a concrete Method. Here we are forcing the Manager class to implement a WorkOnTask() method without a purpose. This is wrong. The design violates ISP. Let's correct the design.

Since we have three roles, 1. Manager, that can only divide and assign the tasks, 2. TeamLead that can divide and assign the tasks and can work on them as well, 3. The programmer that can only work on tasks, we need to divide the responsibilities by segregating the ILead interface. An interface that provides a contract for WorkOnTask().

public interface IProgrammer  
{  
   void WorkOnTask();  
}

An interface that provides contracts to manage the tasks:

public interface ILead  
{  
   void AssignTask();  
   void CreateSubTask();  
}

Then the implementation becomes:

public class Programmer: IProgrammer  
{  
   public void WorkOnTask()  
   {  
      //code to implement to work on the Task.  
   }  
}  

public class Manager: ILead  
{  
   public void AssignTask()  
   {  
      //Code to assign a Task  
   }  
   public void CreateSubTask()  
   {  
   //Code to create a sub taks from a task.  
   }  
}

TeamLead can manage tasks and can work on them if needed. Then the TeamLead class should implement both of the IProgrammer and ILead interfaces.

public class TeamLead: IProgrammer, ILead  
{  
   public void AssignTask()  
   {  
      //Code to assign a Task  
   }  
   public void CreateSubTask()  
   {  
      //Code to create a sub task from a task.  
   }  
   public void WorkOnTask()  
   {  
      //code to implement to work on the Task.  
   }  
}

Here we separated responsibilities/purposes and distributed them on multiple interfaces and provided a good level of abstraction too.

5. Dependency Inversion Principle (DIP)

High level modules should not depend on lower level modules.

The Dependency Inversion Principle (DIP) states that high-level modules/classes should not depend on low-level modules/classes. Both should depend upon abstractions. Secondly, abstractions should not depend upon details. Details should depend upon abstractions.

High-level modules/classes implement business rules or logic in a system (application). Low-level modules/classes deal with more detailed operations; in other words they may deal with writing information to databases or passing messages to the operating system or services.

A high-level module/class that has a dependency on low-level modules/classes or some other class and knows a lot about the other classes it interacts with is said to be tightly coupled. When a class knows explicitly about the design and implementation of another class, it raises the risk that changes to one class will break the other class. So we must keep these high-level and low-level modules/classes loosely coupled as much as we can. To do that, we need to make both of them dependent on abstractions instead of knowing each other. Let's start with an example.

Suppose we need to work on an error logging module that logs exception stack traces into a file. Simple, isn't it? The following are the classes that provide the functionality to log a stack trace into a file. 

public class FileLogger  
{  
   public void LogMessage(string aStackTrace)  
   {  
      //code to log stack trace into a file.  
   }  
}  

public class ExceptionLogger  
{  
   public void LogIntoFile(Exception aException)  
   {  
      FileLogger objFileLogger = new FileLogger();  
      objFileLogger.LogMessage(GetUserReadableMessage(aException));  
   }  
   private GetUserReadableMessage(Exception ex)  
   {  
      string strMessage = string. Empty;  
      //code to convert Exception's stack trace and message to user readable format.  
      ....  
      ....  
      return strMessage;  
   }  
}

A client class exports data from many files to a database.

public class DataExporter  
{  
   public void ExportDataFromFile()  
   {  
      try
      {  
         //code to export data from files to database.  
      }  
      catch(Exception ex)  
      {  
         new ExceptionLogger().LogIntoFile(ex);  
      }  
   }  
}

Looks good. We sent our application to the client. But our client wants to store this stack trace in a database if an IO exception occurs. Hmm... okay, no problem. We can implement that too. Here we need to add one more class that provides the functionality to log the stack trace into the database and an extra method in ExceptionLogger to interact with our new class to log the stack trace.

public class DbLogger  
{  
   public void LogMessage(string aMessage)  
   {  
      //Code to write message in database.  
   }  
}  
public class FileLogger  
{  
   public void LogMessage(string aStackTrace)  
   {  
      //code to log stack trace into a file.  
   }  
}  
public class ExceptionLogger  
{  
   public void LogIntoFile(Exception aException)  
   {  
      FileLogger objFileLogger = new FileLogger();  
      objFileLogger.LogMessage(GetUserReadableMessage(aException));  
   }  
   public void LogIntoDataBase(Exception aException)  
   {  
      DbLogger objDbLogger = new DbLogger();  
      objDbLogger.LogMessage(GetUserReadableMessage(aException));  
   }  
   private string GetUserReadableMessage(Exception ex)  
   {  
      string strMessage = string.Empty;  
      //code to convert Exception's stack trace and message to user readable format.  
      ....  
      ....  
      return strMessage;  
   }  
}  
public class DataExporter  
{  
   public void ExportDataFromFile()  
   {  
      try {  
         //code to export data from files to database.  
      }  
      catch(IOException ex)  
      {  
         new ExceptionLogger().LogIntoDataBase(ex);  
      }  
      catch(Exception ex)  
      {  
         new ExceptionLogger().LogIntoFile(ex);  
      }  
   }  
}

Looks fine for now. But whenever the client wants to introduce a new logger, we need to alter ExceptionLogger by adding a new method. If we continue doing this after some time then we will see a fat ExceptionLogger class with a large set of methods that provide the functionality to log a message into various targets. Why does this issue occur? Because ExceptionLogger directly contacts the low-level classes FileLogger and DbLogger to log the exception. We need to alter the design so that this ExceptionLogger class can be loosely coupled with those classes. To do that we need to introduce an abstraction between them so that ExcetpionLogger can contact the abstraction to log the exception instead of depending on the low-level classes directly.

public interface ILogger  
{  
   void LogMessage(string aString);  
}

Now our low-level classes need to implement this interface.

public class DbLogger: ILogger  
{  
   public void LogMessage(string aMessage)  
   {  
      //Code to write message in database.  
   }  
}  
public class FileLogger: ILogger  
{  
   public void LogMessage(string aStackTrace)  
   {  
      //code to log stack trace into a file.  
   }  
}

Now, we move to the low-level class's initiation from the ExcetpionLogger class to the DataExporter class to make ExceptionLogger loosely coupled with the low-level classes FileLogger and EventLogger. And by doing that we are giving provision to DataExporter class to decide what kind of Logger should be called based on the exception that occurs.

public class ExceptionLogger  
{  
   private ILogger _logger;  
   public ExceptionLogger(ILogger aLogger)  
   {  
      this._logger = aLogger;  
   }  
   public void LogException(Exception aException)  
   {  
      string strMessage = GetUserReadableMessage(aException);  
      this._logger.LogMessage(strMessage);  
   }  
   private string GetUserReadableMessage(Exception aException)  
   {  
      string strMessage = string.Empty;  
      //code to convert Exception's stack trace and message to user readable format.  
      ....  
      ....  
      return strMessage;  
   }  
}  
public class DataExporter  
{  
   public void ExportDataFromFile()  
   {  
      ExceptionLogger _exceptionLogger;  
      try {  
         //code to export data from files to database.  
      }  
      catch(IOException ex)  
      {  
         _exceptionLogger = new ExceptionLogger(new DbLogger());  
         _exceptionLogger.LogException(ex);  
      }  
      catch(Exception ex)  
      {  
         _exceptionLogger = new ExceptionLogger(new FileLogger());  
         _exceptionLogger.LogException(ex);  
      }  
   }  
}

We successfully removed the dependency on low-level classes. This ExceptionLogger doesn't depend on the FileLogger and EventLogger classes to log the stack trace. We don't need to change the ExceptionLogger's code anymore for any new logging functionality. We need to create a new logging class that implements the ILogger interface and must add another catch block to the DataExporter class's ExportDataFromFile method.

public class EventLogger: ILogger  
{  
   public void LogMessage(string aMessage)  
   {  
      //Code to write message in system's event viewer.  
   }  
}

And we need to add a condition in the DataExporter class as in the following:

public class DataExporter  
{  
   public void ExportDataFromFile()  
   {  
      ExceptionLogger _exceptionLogger;  
      try {  
         //code to export data from files to database.  
      }  
      catch(IOException ex)  
      {  
         _exceptionLogger = new ExceptionLogger(new DbLogger());  
         _exceptionLogger.LogException(ex);  
      }  
      catch(SqlException ex)  
      {  
         _exceptionLogger = new ExceptionLogger(new EventLogger());  
         _exceptionLogger.LogException(ex);  
      }  
      catch(Exception ex)  
      {  
         _exceptionLogger = new ExceptionLogger(new FileLogger());  
         _exceptionLogger.LogException(ex);  
      }  
   }  
}

Looks good. But we introduced the dependency here in the DataExporter class's catch blocks. Yeah, someone must take the responsibility to provide the necessary objects to the ExceptionLogger to get the work done.

Let me explain it with a real-world example. Suppose we want to have a wooden chair with specific measurements and the kind of wood to be used to make that chair. Then we can't leave the decision making on measurements and the wood to the carpenter. Here his job is to make a chair based on our requirements with his tools and we provide the specifications to him to make a good chair.

So what is the benefit we get by the design? Yes, we definitely have a benefit with it. We need to modify both the DataExporter class and ExceptionLogger class whenever we need to introduce a new logging functionality. But in the updated design we need to add only another catch block for the new exception logging feature. Coupling is not inherently evil. If you don't have some amount of coupling, your software will not do anything for you. The only thing we need to do is understand the system, requirements, and environment properly and find areas where DIP should be followed.

Great, we have gone through all five SOLID principles successfully. And we can conclude that using these principles we can build an application with tidy, readable and easily maintainable code.

Here you may have some doubt. Yes, about the quantity of code. Because of these principles, the code might become larger in our applications. But my dear friends, you need to compare it with the quality that we get by following these principles. Hmm, but anyway 27 lines are much fewer than 200 lines. 

Free SSL Certificate with Cloudflare and GoDaddy

1. Create a Cloudflare account2. Enter your site URL3. Select a Plan4. Change your Nameservers5. Enable SSL in GoDaddy Window Hosting Plesk6. Complete your Cloudflare settingsWhen you purchase a hosting plan on GoDaddy, they do offer SSL certificate for $6.67 a month. However, there are some services like Cloudflare or Let’s Encrypt which give you SSL certificates for free and compatible with GoDaddy Windows Hosting server. Let’s dive in!Cloudflare is one of the biggest networks operating on the Internet. People use Cloudflare services for the purposes of increasing the security and performance of their web sites and services.Please refer to my previous post regarding how to host your website on GoDaddy. If you didn’t select their SSL option, below is how you can get a free one from CloudFlare.1. Create a Cloudflare accountGo to Cloudflare.com to register yourself an account.2. Enter your site URL3. Select a PlanCloudflare offers different plans including a free one. I am using a free plan and so far so good. Below is detail of each plan.Select Continue, Cloudflare will scan your site DNS. Review it and Continue:4. Change your NameserversCloudflare will ask you to login to your GoDaddy account and change the name server. Follow their instructions:Login to your GoDaddy account, go to My Products and then click on DNS:Scroll down to Nameservers and update it. There will be a warning but you can update it back to the original anytime. It may take a while to update so your website may not reflect the change right away.5. Enable SSL in GoDaddy Window Hosting PleskYou need to enable SSL from your GoDaddy server otherwise your website won’t be accessible. You may encounter HTTP 404 error.Go to GoDaddy > My Products > Web Hosting > Manage > Plesk Admin > Hosting Settings, check both checkboxes under Security and Apply.Check both checkboxes under Security:6. Complete your Cloudflare settingsGo back to Cloudflare, click on Done, check nameservers and complete the settings.Select Full mode under SSL/TLS:Now, your website is more secure with SSL! Please note that it may take a while for Cloudflare and GoDaddy update on their side so check your website in few hours later.

Cloudflare SSL + GoDaddy: HTTP Error 404

Enable SSL in GoDaddy Window Hosting PleskCheck both checkboxes under SecurityAfter your website’s SSL is activated using Cloudflare, you may encounter 404 http error. Below is the screenshot of the error:So, how to resolve this problem? See below:Enable SSL in GoDaddy Window Hosting PleskYou need to enable SSL from your GoDaddy server otherwise your website won’t be accessible. Go to GoDaddy > My Products > Web Hosting > Manage > Plesk Admin > Hosting Settings, check both checkboxes under Security and Apply.Check both checkboxes under SecurityClick on Apply and then Ok to save the settings. Go to your website again, the problem should be resolved.

.Net Core: HTTP Error 502.5 - Process Failure

1. Step 1: Double click on solution file to open .csproj file2. Step 2: Add below code in the .csproj file, inside PropertyGroupAfter publishing my website, I encountered the 502.5 error when opening the website on browser. Below is the screenshot of the error details:How to resolve the 502.5 error?1. Step 1: Double click on solution file to open .csproj file2. Step 2: Add below code in the .csproj file, inside PropertyGroup<PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> <PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest> </PropertyGroup>Save the file and publish the website again, the issue will be resolved.

.Net Core: Error Certificate Validation Failed

When I tried to publish the first time without modifying the publish profile file, I encountered the certificate validation error. Below is the details of the error message:Connected to the remote computer ("XXXXXXXXX") using the specified process ("Web Management Service"), but could not verify the server's certificate. If you trust the server, connect again and allow untrusted certificates.How to resolve the Certification Validation Error?1. Step 1:Open the Publish Profile xml file under Properties\PublishProfile folder2. Step 2: Add below xml tag inside PropertyGroup<PropertyGroup> <AllowUntrustedCertificate>True</AllowUntrustedCertificate> </PropertyGroup>Save the file and then publish the website again, the issue will be resolved.

How to Host your .Net Core Application on GoDaddy Windows Hosting

1. Select a Hosting Plan from GoDaddy2. Customize your Selected Plan3. Download Web Deploy Publishing Settings4. Publish your Application to GoDaddy Hosting server5. Some common errors while publishing your .Net Core ApplicationIf you have a Website or a Web API developed by using .Net Core and looking for a way to publish your applications, this post will explain how to do it using GoDaddy Windows Hosting.Note: at this moment, GoDaddy supports .Net Core 1.0 and 2.0. Any later versions are not compatible with GoDaddy Windows Hosting plan.Update 6/2023: GoDaddy is now supporting .Net Core 3.1 and up to .Net 6.1. Select a Hosting Plan from GoDaddyGo to GoDaddy.com and select a Windows hosting plan that you would like to purchase for your applications.There are 3 plans. I select the Deluxe plan because I may have more than 1 application to host on GoDaddy. For the details of each plan, please see below:2. Customize your Selected PlanIt depends on your purpose, you can choose the term length and back up option. The longer the term length is, the cheaper the rate is. However, since the period is long so you will end up paying more in front. Note: it’s up to you to add SSL option which charge you $6.67/month. However, there are many ways to get a free SSL certificate for your website like using CloudFlare or LetsEncrypt. I have a post guiding how to implement Cloudflare SSL certificate with GoDaddyOnce you finish customizing your plan, you will be able to get a free domain with this order.After purchasing, you can set up your web hosting by connecting it to your domain.3. Download Web Deploy Publishing SettingsFollow below steps to download publishing profile which is used to publish the .Net Core application from Visual Studio.a. You can sign in to GoDaddy to review your products:b. Then, scroll down to Web Hosting and click on Manage:c. Click on Plesk Admin to go to set up and manage website page:d. Click on Web Deploy Publishing Settings to download publish profile:4. Publish your Application to GoDaddy Hosting serverOnce you downloaded the publish profile from Plesk Admin, you are now able to publish your website using Visual Studio. Open your project in Visual Studio and start to publish. In this tutorial, I am using Visual Studio 2019 to publish my website: Lucasology.a. Open your application in Visual Studio 2019, right click on your project solution and select Publish:b. Select Import Profile to import the downloaded publish profile on Publish panel:c. Browse the downloaded publish profile and select the file and click on Open:d. Publish your Application by clicking on Publish:Then, if there is no error while publishing your website, you are succeeded.5. Some common errors while publishing your .Net Core ApplicationI have encountered few errors while trying to publish my website to GoDaddy. Below are the errors I talked about.a. ERROR_CERTIFICATE_VALIDATION_FAILEDWhen I tried to publish the first time without modifying the publish profile file, I encountered the certificate validation error. Below is the details of the error message:Connected to the remote computer ("XXXXXXXXX") using the specified process ("Web Management Service"), but could not verify the server's certificate. If you trust the server, connect again and allow untrusted certificates.How to resolve the Certification Validation Error? Step 1:Open the Publish Profile xml file under Properties\PublishProfile folder:Step 2: Add below xml tag inside <PropertyGroup><PropertyGroup><PropertyGroup> <AllowUntrustedCertificate>True</AllowUntrustedCertificate> </PropertyGroup>Save the file and then publish the website again, the issue will be resolved.b. HTTP Error 502.5 - Process FailureAfter publishing my website, I encountered the 502.5 error when opening the website on browser. Below is the screenshot of the error details:How to resolve the 502.5 error?Step 1: Double click on solution file to open .csproj file:Step 2: Add below code in the .csproj file, inside <PropertyGroup></PropertyGroup><PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> <PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest> </PropertyGroup>Save the file and publish the website again, the issue will be resolved.

Space Complexity

1. What is Space Complexity2. Space Complexity CausesBeside time complexity, space complexity is another measurement to determine if you code is a good one. When we talk about time complexity, we are referring to the speed of your algorithm, or in other words, how many operations does it cost. For space complexity, it is about the memory your algorithm needs to execute.1. What is Space ComplexitySpace complexity is a measure of the amount of working storage an algorithm needs. That means how much memory, in the worst case, is needed at any point in the algorithm. As with time complexity, we're mostly concerned with how the space needs grow, in big O terms, as the size N of the input problem grows.When a program executes, the are two ways to remember things: the heap and the stack. The heap is usually where the computers store variables the we assign values to and the stack is usually where we keep tracks of the function calls.2. Space Complexity CausesSo, what causes space complexity? Below are the causes:VariablesData StructuresFunction CallAllocationsWe will understand more the causes by looking at the example below:function Func1(array) { for (let i = 0; i < array.length; i++) { // O(1) for let i = 0 console.log('hello'); } } Func1([1,2,3,4,5]); //Space Complexity: O(1)When we talk about space complexity, we talk about additional space needed to execute the function. Therefore, we don't care about space taken up by the input. For the above function, we don't really add anymore memory beside adding a variable i = 0 in a for loop. So, the above function costs O(1) space complexity. Let's consider another function below:function Func2(array) { let anotherArray = []; for (let i = 0; i < array.length; i++) { // O(1) for let i = 0 anotherArray[i] = 'Hello'; // O(n) for adding value to anotherArray n times (n is size of array) } return anotherArray; } Func2([1,2,3,4,5]); //Space complexity is O(n)In the above function, we create a new data structure which is an array. Then, we add value to the new array n times where n is the size of the input. So, the above function costs O(n) space complexity.

Big O Rules

1. How to Calculate Big O?3. Big O Rule 1: Worst Case4. Big O Rule 2: Remove Constants5. Big O Rule 3: Different Terms for Inputs6. Big O Rule 4: Drop Non DominantsWe have been talking about and have a better understanding of what is Big O Notation and different time complexities. In this post, we will learn more about how to calculate Big O, how to simplify it, and the 4 Big O Rules.1. How to Calculate Big O?You may not be asked about how to do Big O calculation step by step while doing your interview. Instead, the interviewers may just ask what is the time/space complexity of your algorithm. So, it's better to know a basic idea of how to calculate Big O to come up with a quick answer in your interview.We will go with an example to know how we can determine the Big O for time complexity of an algorithm. Please consider my comments in below code to see the calculation step by step.function Func1(inputArray) { let a = 1; //This will take O(1) because this step will run only once when execute the function. a = 1 + 12; //This will take O(1) because this step will run only once when execute the function. for (let i = 0; i < inputArray.length; i++) { //This will take O(n) because this step will run n times where n is the size of inputArray. let b = i; //This will take O(n) because this step will run n times where n is the size of inputArray. a += b; //This will take O(n) because this step will run n times where n is the size of inputArray. } return a; //This will take O(1) because this step will run only once when execute the function. } Func1([1,2,3,4,5]); //Based on the calculation in each step above, we can say this function costs: //O(1 + 1 + n + n + n + 1) = O(3n + 3)2. Simplifying Big OIn the previous example we just did, it was kind of annoying where we had to add things up then have a not so nice Big O Notation: O(3n + 3). Why it is O(3n + 3)? It doesn't look like any of the common time complexities (well, similar to one of them) that we mentioned before! Well, that is just for you to understand how Big O is calculated! Most of the time, when you go into interviews, there is the set of rules, we call it Big O Rules, that you are going to follow where you don't need to do step by step like above. Instead, you just need to look into your function and say what type of Big O it is.And by following the Big O Rules, we can simplify the above Big O we calculated to below:O(3n + 3) = O(n) //Linear Time Complexity. So, let's dive in these Big O Rules to find out what these rules are about!3. Big O Rule 1: Worst CaseWhen calculating Big O, we always think about the worst case.So, what does it mean by thinking about the worst case. Let's look at the function below:const pokemon = ['Pikachu', 'Meowth', 'Bulbasaur']; const morePokemon = ['Meowth', 'Bulbasaur', 'Charmander', 'Pikachu']; function catchPikachu (array) { for (let i = 0; i < array.length; i++) { if (array[i] === 'Pikachu') { console.log('Catch Pikachu!'); break; } } } catchPikachu(pokemon); catchPikachu(morePokemon);In the example above, the first array has Pikachu as the first element. So, the function just need to loop into the array once since Pikachu has been found the first time. However, the second array has Pikachu as the last element. That means, this function needs to loop the entire input array to find out Pikachu! This is the worst case that the Big O Rule 1 mentions.No matter how the elements are ordered in the input, we always consider the worst scenario to calculate Big O. In this case, the function takes O(n) time complexity.4. Big O Rule 2: Remove ConstantsAfter calculating Big O, we need to ignore all constants in the final result to get Big ORemember how we simplify Big O above? That's is when we remove all constants in the final result to get the final answer of what is the Big O for our function.Consider another example below:function aRandomFunction(inputArray) { console.log("Starting ..."); var middle = Math.floor(items.length/2); var index = 0; while (index < middle) { console.log(items[index]); index++; } for (let i = 0; i < 100; i++) { console.log('ending ....'); } } aRandomFunction([1,2,3,4,5,6]); //The above function cost O(1 + n/2 + 100) = O(n) time complexity.5. Big O Rule 3: Different Terms for InputsWe should have different terms to represent different input size in an algorithmLet's say, if you have a function that take 2 array inputs which have different size. You should have a different terms representing the size of each input. Please consider below function:function randomFunction(array1, array2) { //let's say array1 has size of a items, array 2 has size of b items array1.forEach(function(item) { //This takes O(a) console.log(item); }); array2.forEach(function(item) { //This takes O(b) console.log(item); }); } randomFunction([1,2,3,4,5,6],[1,2]); //This function cost O(a * b) time complexityInstead of having 1 term for both input as n, which will result in O(n) time complexity, we should have different terms for each input like a and b. So, the final outcome of time complexity for the above function is O(a * b).6. Big O Rule 4: Drop Non DominantsWe should worry about the most dominant term and drop othersThe most dominant term is the mathematical term which is greater in absolute value than any other (as in a set) or than the sum of the others. Let's consider below example:function anotherRandomFunction(array) { console.log(array[0]); // O(1) array.forEach(function(item) { // O(n) console.log(item); }); array.forEach(function(item1) { // O(n^2) array.forEach(function(item2) { console.log(item1 + item2); }); }); } anotherRandomFunction([1,2,3,4,5]); //O(n^2 + n + 1) = O(n^2)In the above example, n2 is the most dominant term among others because it is the greatest in absolute value in a set. We can apply any number in the function, n2 is always return the biggest value compare to n and 1. So, we can simplify this to O(n2) as time complexity for this function.

Time Complexities (Part 2)

1. Constant Time - O(1)2. Logarithmic Time - O(log n)3. Linear Time - O(n)4. Quasilinear Time - O(n log n)5. Quadratic Time - O(n2)7. Factorial - O(n!)In the previous post, we have discussed the definitions of Computational Complexity, Big O Notation, and have been introduced to some common time complexities. We will discuss in details each of the common time complexities and example to understand more about how to calculate it.1. Constant Time - O(1)Algorithms that have constant time complexity are those do not depend on the size of the input data. The run time will remain the same not matter how big or small of the input data size.For example, consider below example of an algorithm to get the first item:constant pokemons = ['Bulbasaur', 'Squirtle', 'Charmander', 'Caterpie', 'Weedle']; function getFirst(data) { console.log(data[0]); } getFirst(pokemons);The function always have the same running time regardless of the input data size because it always get the first item of the list. An algorithms with constant time complexity is the best since we don't need to worry about the input size.2. Logarithmic Time - O(log n)Algorithms that have logarithmic time complexity are those do not need to iterate through every single element of the input data. In other word, an algorithm, which reduces the number of elements that it has to iterate through after each step, is considered to have logarithmic time complexity.Binary search is a perfect example for logarithmic time complexity. If you don't know binary search, it's an algorithms to search for a number in a sorted list. Below are steps of the algorithm:Go to the middle element of the list first.If the searched value is lower than the value in the middle of the list, set a new right bounder.If the searched value is higher than the value in the middle of the list, set a new left bounder.If the search value is equal to the value in the middle of the list, return the middle (the index).Repeat the steps above until the value is found or the left bounder is equal or higher the right bounder. Here is how it looks like in code:const binarySearch = (array, target) => { let startIndex = 0; let endIndex = array.length - 1; while(startIndex <= endIndex) { let middleIndex = Math.floor((startIndex + endIndex) / 2); if(target === array[middleIndex) { return console.log("Target was found at index " + middleIndex); } if(target > array[middleIndex]) { console.log("Searching the right side of Array") startIndex = middleIndex + 1; } if(target < array[middleIndex]) { console.log("Searching the left side of array") endIndex = middleIndex - 1; } else { console.log("Not Found this loop iteration. Looping another iteration.") } } console.log("Target value not found in array"); }If you are still confused, watch below video to understand the algorithm. Notice how the dancers are in a line with numbers on their backs. The man with the number seven on his back is looking for the woman who matches. He doesn’t know where she is but he knows all the ladies are in sorted order.His process is to go to the dancer in the middle and ask her which side of her number seven is on. When she says: “She’s to the left of me,” he can rule out everyone to her right.Next, he asks the woman in the middle of the left side the same question. This lets him rule out another half of the candidates and so on until he finds number seven.As we can see, the binary search algorithm doesn't loop through each number of the input array. Instead, the iterated size reduced by half after each step of the algorithm. So, we can say this algorithm has logarithmic time complexity.3. Linear Time - O(n)An algorithm is said to have a linear time complexity when the running time increases at most linearly with the size of the input data. This is the best possible time complexity when the algorithm must examine all values in the input data. For example:const data = [1,2,3,4,5,6]; function printAll(array) { for (let i = 0; i < array.length; i++) { console.log(array[i]); } } printAll(data);Note that in this example, we need to look at all values in the list to print the value. So, we can say this function has linear time complexity.4. Quasilinear Time - O(n log n)An algorithm is said to have a quasilinear time complexity when each operation in the input data have a logarithmic time complexity. It is commonly seen in sorting algorithms.Merge sort is a great example of an algorithm which has quasilinear time complexity. In merge sort, we perform by below steps to sort an array of number: Divide the unsorted list into N sub-lists, each containing 1 element. Take adjacent pairs of two singleton lists and merge them to form a list of 2 elements. N will now convert into N/2 lists of size 2. Repeat the process till a single sorted list of obtained. The list of size N takes maximum log N times to be halved into a singleton list, and the merging of all sublists into a single list takes N time, the worst case run time of this algorithm is O(N log N).5. Quadratic Time - O(n2)An algorithm is said to have a quadratic time complexity when it needs to perform a linear time operation for each value in the input data, for example:const boxes = [1,2,3,4,5]; function logAllPairsOfArray(array){ for(let i = 0; i < array.length; i++) { for(let j = 0; j < array.length; j++) { console.log(i, j); } } } logAllPairsOfArray(boxes);If you see a nested loops like above, we use multiplication. So, the time complexity will be come O(n * n) or O(n2). We call this algorithm has quadratic time complexity. That means every time the number of elements increases, the operation time will increase quadratically. Bubble sort is a great example of quadratic time complexity since for each value it needs to compare to all other values in the list, let’s see an example:let bubbleSort => (inputArr) { let len = inputArr.length; for (let i = 0; i < len; i++) { for (let j = 0; j < len; j++) { if (inputArr[j] > inputArr[j + 1]) { let tmp = inputArr[j]; inputArr[j] = inputArr[j + 1]; inputArr[j + 1] = tmp; } } } return inputArr; };6. Exponential Time - O(2n) An algorithm is said to have an exponential time complexity when the growth doubles with each addition to the input data set. This kind of time complexity is usually seen in brute-force algorithms.In cryptography, a brute-force attack may systematically check all possible elements of a password by iterating through subsets. Using an exponential algorithm to do this, it becomes incredibly resource-expensive to brute-force crack a long password versus a shorter one. This is one reason that a long password is considered more secure than a shorter one. Another example of an exponential time algorithm is the recursive calculation of Fibonacci numbers:function fibonacci(num) { if (num <= 1) return 1; return fibonacci(num - 1) + fibonacci(num - 2); }A recursive function may be described as a function that calls itself in specific conditions. As you may have noticed, the time complexity of recursive functions is a little harder to define since it depends on how many times the function is called and the time complexity of a single function call.It makes more sense when we look at the recursion tree. The following recursion tree was generated by the Fibonacci algorithm using n = 4:Note that it will call itself until it reaches the leaves. When reaching the leaves it returns the value itself.Now, look how the recursion tree grows just increasing the n to 6:7. Factorial - O(n!)An algorithm is said to have a factorial time complexity when it grows in a factorial way based on the size of the input data, for example:2! = 2 x 1 = 23! = 3 x 2 x 1 = 64! = 4 x 3 x 2 x 1 = 245! = 5 x 4 x 3 x 2 x 1 = 1206! = 6 x 5 x 4 x 3 x 2 x 1 = 7207! = 7 x 6 x 5 x 4 x 3 x 2 x 1 = 5.0408! = 8 x 7 x 6 x 5 x 4 x 3 x 2 x 1 = 40.320As you may see it grows very fast, even for a small size input.A great example of an algorithm which has a factorial time complexity is the Heap’s algorithm, which is used for generating all possible permutations of n objects. Heap found a systematic method for choosing at each step a pair of elements to switch, in order to produce every possible permutation of these elements exactly once. Let’s take a look at the example:let swap = function(array, index1, index2) { var temp = array[index1]; array[index1] = array[index2]; array[index2] = temp; return array; }; let permutationHeap = function(array, result, n) { n = n || array.length; // set n default to array.length if (n === 1) { result(array); } else { for (var i = 1; i <= n; i++) { permutationHeap(array, result, n - 1); if (n % 2) { swap(array, 0, n - 1); // when length is odd so n % 2 is 1, select the first number, then the second number, then the third number. . . to be swapped with the last number } else { swap(array, i - 1, n - 1); // when length is even so n % 2 is 0, always select the first number with the last number } } } }; // var output = function(input) { console.log(input); }; permutationHeap(["a", "b", "c"], output);The result will be:[1, 2, 3][2, 1, 3][3, 1, 2][1, 3, 2][2, 3, 1][3, 2, 1]Note that it will grow in a factorial way, based on the size of the input data, so we can say the algorithm has factorial time complexity O(n!).

Time Complexities (Part 1)

1. Computational Complexity2. Time Complexity3. Big O Notations and Time ComplexitiesWhen we talk about Big O and scalability of code, we simply mean when we grow bigger and bigger with our input, how much does the algorithm or function slow down? In this post, we will understand more about Big O, time complexity, and why we need to be concerned about it when developing algorithms.1. Computational ComplexityIn computer science, computational complexity is a field which analyzes algorithms by calculated the amount resources required to execute it. The amount of resources varies based on the size of the input. So, we can understand the complexity is a function of n, where n is the size of the input.The complexity analysis includes time complexity and space complexity. We will discuss space complexity in the next post while this post will mainly focus on time complexity and how to calculate it from different algorithms.2. Time ComplexityIn computer science, the time complexity is the computational complexity that describes the amount of time it takes to run an algorithm. Time complexity is commonly estimated by counting the number of elementary operations performed by the algorithm, supposing that each elementary operation takes a fixed amount of time to perform.When analyzing time complexity of an algorithms, there are 3 scenarios that you must consider:Best-case: this is the complexity of solving the problem for the best input.Average-case: this is the average complexity of solving the problem. This complexity is defined with respect to the distribution of the values in the input data.Worst-case: this is the complexity of solving the problem for the worst input of size n.For example: let's consider the catching Pikachu function that we discussed in the previous post:const pokemon = ['Pikachu', 'Meowth', 'Bulbasaur']; function catchPikachu (array) { for (let i = 0; i < array.length; i++) { if (array[i] === 'Pikachu') { console.log('Catch Pikachu!'); return; } } } catchPikachu(pokemon);Consider the input, we have Pikachu is the first element of the pokemon array. The function just needs to iterate to the first element and finishes so it would be a best-case. However, if we are looking for Meowth instead of Pikachu, it would be an average-case. And finally, if we need to catch a Bulbasaur, it would be the worst-case.3. Big O Notations and Time ComplexitiesIf you have read my previous post, you may understand to some extend regarding Big O Notation. Below is the definition:Big O notation, sometimes called “asymptotic notation”, is a mathematical notation that describes the limiting behavior of a function when the argument tends towards a particular value or infinity.In computer science, Big-O notation is used to classify algorithms according to how their running time or space requirements grow as the input size (n) grows. This notation characterizes functions according to their growth rates: different functions with the same growth rate may be represented using the same O notation.Let’s see some common time complexities described in the Big-O notation.NameTime ComplexityConstant TimeO(1)Logarithmic TimeO(logn)Linear TimeO(n)Quasilinear TimeO(nlogn)Quadratic TimeO(n^2)Exponential TimeO(2^n)Factorial TimeO(n!)We have discussed more about Big O notation and Time Complexity when analyzing algorithms. We will focus more in details of each Time Complexity mentioned above in the next post. Thank you for reading my post, see you next time!

Introduction to Big O

1. What is Big O2. What is Good Code?3. Big O and ScalabilityBig O is one of the most important topics for any software developer or engineer. No matter what company you are working, where you live, or how long it will be from now, this concept will be around and it is what makes you a better developer. Big companies, like  F.A.A.N.G., all know this which is why you won't get any of their interviews without encountering this topic. 1. What is Big OBig O, the official term is Big O asymptomatic analysis, can help us to see how well a problem is solved. Any coder, who is given enough time, can solve a problem. However, what matters is how well the problem is solved. We will talk about what Big O is, how we define it and then how to use Big O and it's different notation to distinguish bad code from good code, or good code from better code.2. What is Good Code?Good code is defined by two criteria: Readability and Scalability. Readability determines if your code is just generally clean, and if others can understand your code. Scalability is the ability to adapt of your code. Big O is what allows us to measure this idea of scalable code. 3. Big O and ScalabilityConsider a cake baking procedure as an example of scalability. Let's say, we have a task to bake a cake with provided recipe and a kitchen. There are many way to bake a cake which will result in either a good or a bad product. It all depends on how well we utilize the kitchen to work well with the provided recipe. We can say, our baking procedure must adapt to the provided resources that we have, in other words, it must scalable with the available resources.Well, computers are machine which need to work in order to produce something for human. Computers work the same way as the kitchen in the example above. We have instructions that we give it through code and these code are running by computers to produce some sort of an output. A coder is  someone who gives the code as instruction to a computer, just like how to use recipe to bake a cake, there are many ways to code and solve problem which will result in either good or bad outcome. So, your code performance must scalable to the provided machine which executes it. Big O notation is used to measure how well your code performs in any computer.So, how is Big O used to measure your code performance? Consider below piece of JavaScript code. We are writing a simple function to catch Pikachu!const pokemon = ['Pikachu', 'Meowth', 'Bulbasaur']; function catchPikachu (array) { for (let i = 0; i < array.length; i++) { if (array[i] === 'Pikachu') { console.log('Catch Pikachu!'); } } } catchPikachu(pokemon);The above code would run really fast in modern computers because, well, computers are fast now! The other factor that make it really fast is the list of input array is small. We only have 3 items in our pokemon array. Let's try it again with another array of more Pokemon and have a little trick to calculate how long your code take to execute in my machine.const pokemon = ['Pikachu', 'Meowth', 'Bulbasaur']; const morePokemon = ['Pikachu', 'Meowth', 'Bulbasaur', 'Charmander', 'Squirtle', 'Caterpie', 'Weedle', 'Pidgey', 'Rattata', 'Spearow']; const manyPokemon = new Array(1000000).fill('Pikachu'); function catchPikachu (array) { let t0 = performance.now(); for (let i = 0; i < array.length; i++) { if (array[i] === 'Pikachu') { console.log('Catch Pikachu!'); } } let t1 = performance.now(); console.log('Call to catch Pikachu took ' + (t1 - t0) + ' milliseconds'); } catchPikachu(pokemon); //took roughly 0 millisecond catchPikachu(morePokemon); //took roughly 0.1 millisecond catchPikachu(manyPokemon); //took roughly 7 millisecondWell, we can see different outcomes depend on the size of the input, and, of course, the machine we use to execute the code. One of the reason you may have different results of how long your code took than mine is because you ran your code in your computer and I ran mine in my computer. Big O notation is the language which is used for talking about how long an algorithm takes to run. We can compare two different algorithms using Big 0 to say which one is better than the other in term of scale regardless of our computer and input differences. So, we have been introduced to Big O and have an idea of the use of it. We will discuss how Big O is calculated and which Big O number represent horrible and excellent code performance in the next posts.

Categories
About The Author
Hoang (Lucas) Nguyen profile photo

Senior Full-Stack Software Engineer with strong expertise in designing and delivering scalable enterprise applications. Proven capability in leading end-to-end development, optimizing system performance, and implementing robust CI/CD processes to support reliable software delivery.

Follow Me