A good project structure makes getting around the code easier, especially for developers that just joined the project. This also makes adding new features or replacing/fixing existing ones.
The implementation should be separated from the contract. For example, we can have multiple data sources but the services using them should only be aware of the contract and not the implementation. In case we have multiple data sources, each source should be implemented in a separate project.
This is an example of a project structure that is mostly used for standard .NET Core WebAPIs:
- Example.API
- Example.Common
- Example.Contracts
- Example.Services
- Example.Data.Db
- Example.Data.SomeOtherSource (if there is a need for one)
Example.API represents our "presentation" layer. It should contain the endpoints but not the implementation of business logic. Of course, it also contains all the necessary configurations like authorization setup, dependency injection registrations, middleware, and filter registrations.
Example.Common library is used for shared classes and implementations like exception handling, shared models, extensions, etc.
Example.Contracts library represents all the entities, DTOs, repository interfaces, service interfaces, domain constants, and enumerations.
Example.Services library contains all the service implementations for the Example application.
Example.Data.Db library contains the database and ORM implementation for the Example application. This project would contain all the database configurations, mappings, migrations, and repository implementation.
Example.Data.SomeOtherSource library should be used if we are using some kind of third-party service to fetch the data. An example would be using Google Drive API to fetch files, which would then be named Example.Data.Google
.
All the projects that are not used as a "presentation" layer are implemented as .NET Standard projects. The rest depends on the needs, but in our example, we would use .NET Core for our API.
Startup.cs
This is where most of the configuration for the application is located. We use it to register services, repositories, database contexts, and other dependencies. Depending on the application type we can also tell our app to use middlewares, filters, or any other third-party service.
It is a good practice to group specific configuration sections like services and repositories to keep things organized. One of the ways to do this is through the implementation of IServiceBuilder extensions.
Since we use the Options pattern for app configuration, the bindings are done in the startup class. There are more details for this pattern in the Configuration section of this handbook.
AppSettings.json
appsettings.json
file is usually used for app configuration like connection strings, configurable parameters, etc. Since it is not a good practice to keep app secrets here and upload them to the repository, we mostly use environment variables for configuring the app in a non-development environment. In a development environment, we use user secrets.
NuGet
It is a good practice to keep app packages updated, and for installing and updating we use NuGet package manager.
Test projects
Regardless of the test framework, we try to keep a simple test project structure. For each app project, we create additional test projects. We use simple naming strategies for the projects, only adding the suffix ".Test" for the test project. For example, Example.API would be Example.API.Test
You can find more information about app testing in the Testing section of the handbook.