Lesson Weekend

Including both a C# class (like the Car class) and logic for interacting with users in one file isn't best practice. Let's refactor our code and address the proper file structure for a C# application.

Namespaces


C# uses namespaces to group interrelated and interdependent classes together. A namespace is a keyword used to identify sets of classes. Packaging sets of classes in namespaces helps us write clean, DRY code. If two classes are in the same namespace, they will 'know' about each other and be in the same scope.

To give a class a namespace, we simply wrap the class in another set of curly braces and declare the namespace like this:

ExampleClass.cs
namespace ExampleNameSpace
{

  public class ExampleClass
  {
    // all ExampleClass methods, fields, and properties here
  }

}

We can wrap other classes in the same namespace to make them available to each other:

SecondClass.cs
namespace ExampleNameSpace
{

  public class SecondClass
  {
    // all SecondClass methods, fields, and properties here
  }

}

Let's follow this pattern in our car dealership application.

Project Structure


Instead of including everything in a single file, we need to separate our code into two files:

  • One will contain the user interface or command line interface logic. This file will be responsible for prompting the user, gathering user input, calling any methods, and returning information to the user. This is the "frontend" or "user interface logic." We will name this file Program.cs.

  • The other will contain our custom class. This includes the class declaration itself, our constructor, and any additional methods. We'll refer to this as the class file or business logic. This is the "backend" of our application. This is our Car.cs file.

Refactoring into Multiple Files

Let's separate our code into the appropriate files now.

  • Create a new directory called car-dealership.
  • Create a CarDealership.csproj in this directory. See the code snippet below for the exact code that should be in this file.
  • Place a new Car.cs file in this directory.
  • Create a new file in this directory called Program.cs.
  • Cut everything from the public class Program block of code out of Car.cs, and place it inside Program.cs.
  • Add the using System and using System.Collections.Generic; lines to the top of Program.cs too.
  • Remove the using System.Collections.Generic; line from Car.cs, because it no longer contains any List objects.
  • Wrap your Program class in a new namespace that represents our project, Dealership.
  • Wrap your Car class in a namespace that represents its relationship to our project, Models. Note that the word Models here does not have to do with a car's model! In our projects we refer to the classes that we utilize, such as our Car class, as Models, because they model the format of the objects we will create. Regardless of the theme of your project, your custom classes that you call in the frontend are known as Models.

The result should be three files that look like this:

CarDealership.csproj
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
    </PropertyGroup>

</Project>
car-dealership/Program.cs
using System;
using System.Collections.Generic;

namespace Dealership
{
  public class Program
  {
    public static void Main()
    {
      Car volkswagen = new Car("1974 Volkswagen Thing", 1100, 368792);
      Car yugo = new Car("1980 Yugo Koral", 700, 56000);
      Car ford = new Car("1988 Ford Country Squire", 1400, 239001);
      Car amc = new Car("1976 AMC Pacer", 400, 198000);

      List<Car> Cars = new List<Car>() { volkswagen, yugo, ford, amc };

      Console.WriteLine("Enter maximum price: ");
      string stringMaxPrice = Console.ReadLine();
      int maxPrice = int.Parse(stringMaxPrice);

      List<Car> CarsMatchingSearch = new List<Car>(0);

      foreach (Car automobile in Cars)
      {
        if (automobile.WorthBuying(maxPrice))
        {
          CarsMatchingSearch.Add(automobile);
        }
      }

      foreach(Car automobile in CarsMatchingSearch)
      {
        Console.WriteLine(automobile.MakeModel);
      }
    }
  }
}
car-dealership/Car.cs
namespace Models
{
  public class Car
  {
    public string MakeModel;
    public int Price;
    public int Miles;

    public Car(string makeModel, int price, int miles)
    {
      MakeModel = makeModel;
      Price = price;
      Miles = miles;
    }

    public bool WorthBuying(int maxPrice)
    {
      return (Price <= maxPrice);
    }
  }
}

When we separate our logic into different files, we launch our program by compiling and launching the file containing the Main() method. Remember that Main() is the entry point in our program. It's the method that runs first. We can test our new program structure by navigating into our car-dealership directory and executing the following command:

$ dotnet run

...But wait! We get an error!

Program.cs(8,5): error CS0246: The type or namespace name `Car' could not be found. Are you missing an assembly reference?
Program.cs(9,5): error CS0246: The type or namespace name `Car' could not be found. Are you missing an assembly reference?
Program.cs(10,5): error CS0246: The type or namespace name `Car' could not be found. Are you missing an assembly reference?
Program.cs(11,5): error CS0246: The type or namespace name `Car' could not be found. Are you missing an assembly reference?
Program.cs(13,10): error CS0246: The type or namespace name `Car' could not be found. Are you missing an assembly reference?
Program.cs(19,10): error CS0246: The type or namespace name `Car' could not be found. Are you missing an assembly reference?
Program.cs(21,32): error CS0841: A local variable `Cars' cannot be used before it is declared
Program.cs(29,31): error CS0841: A local variable `CarsMatchingSearch' cannot be used before it is declared
Compilation failed: 8 error(s), 0 warnings
Cannot open assembly 'Program.exe': No such file or directory.

Our compilation fails because the Program class no longer knows about the Car class. This is because they're now in separate files and separate namespaces. So how do we tell the Program class that the Car class exists? We need to use its namespace.

It's time to explore the using directive further. We'll add using Models to our Program.cs file:

car-dealership/Program.cs
using System;
using System.Collections.Generic;
using Models; //<-----This is new code.

...

The keyword using is called a directive. It tells C# that the code in this file will be "using" code that isn't contained in this file or in this file's namespace. Our using statements import other namespaces that contain the code our file needs. For example, we need to import System so we can use the Console class. We can also import custom namespaces.

If classes in different files share namespaces, it's not necessary to import namespaces with the using directive. For instance, if class A in file A.cs is in the same namespace as class B in file B.cs, file A.cs does not need a using B directive. Classes in the same namespace have access to each other as long as they both exist within the same directory tree.

It is important to note that a using [namespace] directive imports the types contained in the given namespace, but specifically does not import nested namespaces. This is to avoid naming conflicts and explains why we must import both System to use Console and System.Collections.Generic to use Lists even though the latter namespace is nested in the former.

Organizing namespaces and Handling Multiple Files

When we create namespaces, we want to make sure they tell us something useful by naming them in a descriptive manner. A convention in C# is to use our directory structure to define our namespaces.

We define Car() within the Models namespace, but the Models are part of our Dealership project. Let's rename that namespace to Dealership.Models and update the corresponding using directive:

car-dealership/Car.cs
...
namespace Dealership.Models
...
car-dealership/Program.cs
using Dealership.Models;

To organize our code further, we'll create a Models folder that holds our back-end logic. It should hold all classes we use for storing, manipulating, and retrieving data. Having a Models folder is considered best practice.

Here's our new project structure:

car-dealership
├── Models
│   └── Car.cs
└── Program.cs

We do not need to make any updates to our using directive for the program to run in the terminal.

Additional Resources


Terminology


  • Namespace: A keyword used to identify sets of classes. If two classes are in the same namespace, they will be in the same scope. We can namespace a class like this:
namespace ExampleNameSpace
{

  public class ExampleClass
  {
    // all ExampleClass methods and properties here
  }

}

We can wrap other classes in the same namespace to make them available to each other:

SecondClass.cs
namespace ExampleNameSpace
{

  public class SecondClass
  {
    // all SecondClass methods and properties here
  }

}

Lesson 4 of 10
Last updated more than 3 months ago.