Recently, a question has been raised on Reddit/r/dotnet: When should you create a new .csproj?
This is a crucial consideration for every .NET team. In this article, we will first enumerate objective criteria to help decide whether to create a new project. Then we will look at how these criteria are used in some real-world OSS applications.
When to create a new .csproj? The Guidelines
- Modularization: Create a new project when breaking a large application into smaller, more manageable modules or libraries. Allowed project dependencies and the internal encapsulation modifier help define boundaries and prevent chaos. Without these boundaries, a few large projects can quickly devolve into spaghetti code. However, keep in mind that each project generates an assembly, adding to the number of physical artifacts. More projects mean more files to handle during referencing and deployment, potentially increasing complexity and causing headaches.
- Reusability: Create a new project when you need a library that will be reused across multiple applications or services. Typically, domain libraries are reused across the application. Utility libraries, such as those for string manipulation, are also commonly shared. Logging utilities can be reused across various applications and services.
- Separation of Concerns: Isolate different concerns, such as data access, business logic, domain, and UI, into separate projects. You can objectively check this by analyzing project dependencies. For example, domain types depend only on .NET Base Class Library primitives like strings, numbers, and collections. If a class depends on Entity Framework or MAUI, you know it is not a domain class, or it is wrongly designed. The Clean Architecture pattern is based on this separation of concerns principle. This point can be invalidated if you opt for a Vertical Slice Architecture.
- Code That Changes Together Should Be Grouped Together: The previous point can be invalidated if you opt for a Vertical Slice Architecture. This approach organizes code by features rather than technical layers. It promotes focus on individual functionalities, reduces cross-cutting concerns, and decreases build time since work is often confined to a single feature.
- Application vs. Test Code: Create one or more dedicated projects for tests to keep test code separate from production code. Typically, thousands of unit tests can execute in a few seconds, while integration tests may take minutes. To optimize efficiency, segregate unit tests from integration tests into different projects. This approach allows unit tests to be run quickly and frequently, without disrupting the developer workflow.
- Separation of Interfaces and Implementations: Create separate projects for interfaces and their implementations to adhere to the Dependency Inversion Principle. This principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions. By isolating interfaces, you promote loose coupling and flexibility, allowing implementations to change without affecting dependent code. This approach enhances maintainability and testability.
- Team Collaboration: Create separate projects to allow multiple teams to work on different parts of an application independently. This reduces interference and merge conflicts, enabling parallel development and focused expertise. Separate projects also make onboarding new team members easier by reducing the learning curve.
- Microservices: In a microservices architecture, each microservice typically has its own project to encapsulate its functionality.
Case Studies
Now let’s have a look at various choices made within some popular OSS applications.