Table of Contents
In this post, we will see how to structure an application, whether it is a web application as in this example, a console application, or if you have an application that includes both a web app and several console apps or multiple lambda/Azure functions, the idea is the same.
Organization of folders and subprojects is crucial as our main project grows, since good organization will make it much easier to work on it.
NOTE: This post dates back to 2020, since then I've changed my approach to creating applications to a more "direct" process. You can read or watch my updated opinion in the following post: https://www.netmentor.es/entrada/core-driven-architecture |
1 - Folder Structure of a C# Repository
This part is more personal opinion / open source community convention rather than something that you must follow at all costs. But within open source communities, a structure like the following is usually recommended:
We start with our repository on GitHub (although any version control system is valid).
In the root folder we should include the .gitignore, license, and readme, and inside we do not place the code directly, but rather create a folder called src
(source code).
If our project has any other content, such as documentation, it will also go in this root folder.
Once inside the src
folder, we should create a folder for each "project" that the larger project contains.
In our particular case, for now, we have a Back end API
and the front end is planned to be done in Blazor. For this, we create two folders, one for each project.
Inside these folders, we do NOT write our code, instead we have two more folders, one named src
, like before, and a second folder called test
.
Alongside our project folders, if we have common code, we should place it in a common or shared section, usually called Shared
. It's not common to have tests inside the Shared
folder, but you can include them. In our example, I will just include the railway oriented programming library.
And let's not forget integration tests, which should cover the entire application and therefore will also have their own separate folder.
You should follow this structure to make your code as understandable as possible, because if you create projects wherever you want, both the code and the structure will become chaotic.
Following a good folder structure will also tell you when you can or cannot reference projects from other projects. If you step out of the project's parent folder (BackEnd
/FrontEnd
in our case), you should only reference shared
. If you reference any other project, you know you are doing it wrong.
2 - Project Structure
We must create our code with the layers of the project well separated and clearly indicated. Usually, these layers are in the form of "libraries" and should have their full names. What does that mean?
In our example, we are working on a project called WebPersonal
, and within this personal web, the API is in what we've called the "BackEnd," so the full name of the API will be WebPersonal.BackEnd.API
.
And we should do this for ALL our projects, no matter which "subproject" or section we are in, as this brings much more clarity to the application.
2.1 - Model
We should mainly create our code to be as independent as possible.
So, we need to create our model, which is the central part of the project where we define types, and we should try to make sure it does NOT contain external dependencies, especially third-party libraries.
Each of your applications within the repository should have a model, meaning you should create a separate model for each application.
2.2 - Data Access
To access our database, we will also have our own library for database access.
2.3 - Service
Services are defined as the project containing the business logic for our specific process, that is, the logic for "the function" we're going to execute. If you remember from the Railway oriented programming post and SOLID principles, every method or function should perform a single action.
This is where we will put this part of the logic. For example, if we have an API endpoint that reads the personal profile by ID, there will be a service that does only the task of retrieving a personal profile given an ID parameter.
2.4 - Service Dependencies
Next, we should create what we call ServiceDependencies
; this library project will hold all the dependencies for our service.
A dependency is any data or process our service needs to access or consult.
For example, the file system, if we need to read a file, the database, or even another API, whether from our project or another one.
In object-oriented programming, we commonly use interfaces. These interfaces represent what the class itself will be. We should place the interfaces in the services project.
This is because we will need to add a reference from ServiceDependencies
to our service. This prevents circular references.
As the serviceDependencies
project will reference both the data access project (for example) and the services project, this reference will prevent us from referencing the database directly from the services, for example.
Of course, to directly apply this pattern, you should use dependency inversion.
2.5 - Entry Point
In our specific case, it is an API, but this applies to console applications or lambdas as well. We have to define the entry point of our application.
It should follow the same naming structure and will have access to the service class library, and services will be called from the entry point. This way, we avoid circular references easily.
2.6 - Descriptive Image
2.5 - MSBuild File
It is also a very good practice to have a file that includes all the common features of our projects, such as the language version we use.
For example, we can restrict the language version to C#7.1
so as not to use features from C#8.0
while still using netcore 3.1
.
This way, we ensure that ALL the projects in our repository share the same language version, and we can update them all at once.
It's also the place to specify the author, company, product name, copyright, or version that will be common in all our projects, as well as Release and Debug configurations.
Using the MSBuild file to indicate the configuration files for the different applications is common, although not 100% mandatory.
Here you can see an example of the file
<Project>
<PropertyGroup>
<Authors>Ivan Abad</Authors>
<Company>NetMentor</Company>
<Product>WebPersonal</Product>
<Copyright>Copyright NetMentor 2020</Copyright>
<Version>1.0.0</Version>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<Description>Configuration=Debug</Description>
<Configuration>Debug</Configuration>
<Description>Configuration=Debug</Description>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Description>Configuration=Release</Description>
<Configuration>Release</Configuration>
<Description>Configuration=Release</Description>
</PropertyGroup>
<PropertyGroup>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Update="API.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="API.Production.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="API.Staging.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="API.Internal.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="API.Development.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="API.user.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
Of course, you should reference this file from each .csproj where you want to apply this configuration. A suitable name for this file would be GlobalSettings.msbuild
3 - Implementation in Code
Obviously, I can't show how to create projects in code throughout this post. For that, you can access the video, available on July 14th, but I can indicate how and where to make the change to properly name a C# project.
To change a project's name, you need to go to its .csproj
, which will open in XML format. One of the tags is <PropertyGroup>
and there can be multiple elements, but what we are looking for is <RootNamespace>
where you should specify the project's name.
Optionally, I recommend enabling <TreatWarningsAsErrors>
so you'll have to fix warnings before compiling.
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>WebPersonal.BackEnd.API</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
Conclusion
In this post, we have reviewed the folder structure your repository should follow.
As well as the structure that each of the projects should follow, explaining their main libraries.
I highly recommend using a good structure in your projects since it will help a lot when programming and understanding the project content.
Of course, if you follow the pattern I indicated in section 2, you'll avoid circular reference errors, which are often a major problem in older codebases.
If there is any problem you can add a comment bellow or contact me in the website's contact form