NDepend Blog

Improve your .NET code quality with NDepend

When to create a new .csproj?

June 24, 2024 5 minutes read

When to create a new .csproj

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.

Fat Project and Spaghetti Code

NopCommerce is a popular open-source eCommerce platform, significantly larger than a simple Clean Architecture prototype, with a total of 28 projects. However, most of these projects are small plugins. The main application code is organized into a few large projects: Core, Services, Data, and Web.

Diagrams below have been generated through the NDepend Dependency Graph.

NopCommerce .NET Architecture

If we examine a large project like Nop.Services closely, we find that nearly all namespaces depend on each other, either directly or indirectly. This project contains 736 types that cannot be easily divided into smaller projects without significant refactoring. Therefore, applying architectural rules, such as Avoid namespace dependency cycles, is important for maintaining order and clarity within large projects.

NopCommerce Project Structure

Clean Architecture vs Vertical Slices Architecture

As discussed earlier, there’s an ongoing debate between these two approaches. We can compare both Jason Taylor’s Clean Architecture .NET solution with Nadir Badnjevic’s Vertical Slice Architecture. You must choose one structure for your code projects, as both cannot be combined. The screenshot below illustrates how the TodoItem feature is implemented in both approaches, highlighting the simplicity of the Vertical Slice Architecture.

.net-clean-architecture-vs-vertical-slices-architecture

On the other hand, examining the project dependency diagrams reveals that Clean Architecture properly segregates the assemblies sets referenced. In contrast, the Vertical Slice Architecture tends to promote a fat approach, where a feature project consumes everything, DB UI and infrastructure.

Clean Architecture vs Vertical Slice Architecture

Reusability across Microservices

Now let’s look at the project structure of run-aspnetcore-microservices. We can see that the project Common.Logging is re-used by each service.

Micro Services Architecture

Implementation Must Depend on Abstractions

Finally, let’s examine the architecture of the large ASP.NET Core application OrchadCore. It consists of 201 projects, with 41 of them with names suffixed with .Abstractions. Highlighting these projects reveals that most dependencies flow from implementation projects to abstraction projects.
OrchadCore Abstractions Projects

Let’s double-check our intuition with this C# LINQ code query that matches abstraction projects that depend on non-abstraction projects.

This query could be easily transformed into a code rule with a warnif count > 0 prefix. Hopefully, there are only 5 violations:

OrchadCore Abstractions Projects Violation

Conclusion

Segregating application code into components involves trade-offs that significantly impact maintainability. As the codebase grows, each decision to create a new project must be driven by clear, consistent reasons. Thoughtful structuring ensures scalability, reduces complexity, and enhances long-term maintainability. Careful planning at this stage can prevent future headaches and promote a cleaner, more organized codebase.