Structuring projects and managing packages
How should you structure your projects? In this book, we will build multiple projects using different technologies that work together to provide a single solution.
With large, complex solutions, it can be difficult to navigate through all the code. So, the primary reason to structure your projects is to make it easier to find components. It is good to have an overall name for your solution that reflects the application or solution.
We will build multiple projects for a fictional company named Northwind. We will name the solution MatureWeb
and use the name Northwind
as a prefix for all the project names.
There are many ways to structure and name projects and solutions, for example, using a folder hierarchy as well as a naming convention. If you work in a team, make sure you know how your team does it.
Structuring projects in a solution
It is good to have a naming convention for your projects in a solution so that any developer can tell what each one does instantly. A common choice is to use the type of project, for example, class library, console app, website, and so on.
Since you might want to run multiple web projects at the same time, and they will be hosted on a local web server, we need to differentiate each project by assigning different port numbers for their endpoints for both HTTP and HTTPS.
Commonly assigned local port numbers are 5000
for HTTP and 5001
for HTTPS. We will use a numbering convention of 5<chapter>0
for HTTP and 5<chapter>1
for HTTPS. For example, for an ASP.NET Core MVC website project that we will create in Chapter 2, we will assign 5020
for HTTP and 5021
for HTTPS.
We will therefore use the following project names and port numbers, as shown in Table 1.2:
Name |
Ports |
Description |
|
N/A |
A class library project for common types like interfaces, enums, classes, records, and structs, is used across multiple projects. |
|
N/A |
A class library project for common EF Core entity models. Entity models are often used on both the server and client side, so it is best to separate dependencies on specific database providers. |
|
N/A |
A class library project for the EF Core database context with dependencies on specific database providers. |
|
N/A |
An xUnit test project for the solution. |
|
|
An ASP.NET Core project for complex websites that uses a mixture of static HTML files and MVC Razor Views. |
|
|
An ASP.NET Core project for a Web API aka HTTP service. A good choice for integrating with websites because it can use any .NET app, JavaScript library, or Blazor to interact with the service. |
Table 1.2: Example project names for various project types
Structuring folders in a project
In ASP.NET Core projects, organizing the project structure is vital for maintainability and scalability. Two popular approaches are organizing by technological concerns and using feature folders.
Folder structure based on technological concerns
In this approach, folders are structured based on the type of components, such as Controllers
, Models
, Views
, Services
, and so on, as shown in the following output:
/Controllers
ShoppingCartController.cs
CatalogController.cs
/Models
Product.cs
ShoppingCart.cs
/Views
/ShoppingCart
Index.cshtml
Summary.cshtml
/Catalog
Index.cshtml
Details.cshtml
/Services
ProductService.cs
ShoppingCartService.cs
There are pros and cons to the technical concerns approach, as shown in the following list:
- Pro – Familiarity: This structure is common and well-documented, and many sample projects use it, making it easier for developers to understand.
- Pro – IDE support: SDKs and IDEs assume this structure and may provide better support and navigation for it.
- Con – Scalability: As the project grows, finding related files can become difficult since they are spread across multiple folders.
- Con – Cross-cutting concerns: Managing cross-cutting concerns like logging and validation can become cumbersome.
The .NET SDK project templates use this technological concerns approach to folder structure. This means that many organizations use it by default despite it not being the best approach for their needs.
Folder structure based on features
In this approach, folders are organized by features or vertical slices, grouping all related files for a specific feature together, as shown in the following output:
/Features
/ShoppingCart
ShoppingCartController.cs
ShoppingCartService.cs
ShoppingCart.cs
Index.cshtml
Summary.cshtml
/Catalog
CatalogController.cs
ProductService.cs
Product.cs
Index.cshtml
Details.cshtml
There are pros and cons to the feature folders approach, as shown in the following list:
- Pro – Modularity: Each feature is self-contained, making it easier to manage and understand. Adding new features is straightforward and doesn’t affect the existing structure. Easier to maintain since related files are located together.
- Pro – Isolation: Helps in isolating different parts of the application, promoting better testability and refactoring.
- Con – Learning curve: Less familiar to some developers, requiring a learning curve.
- Con – Code duplication: Potential for code duplication if not managed properly.
Feature folders are a common choice for modular monolith architecture. It makes it easier to later split the feature out into a separate project for deployment.
Feature folders align well with the principles of Vertical Slice Architecture (VSA). VSA focuses on organizing code by features or vertical slices, each slice handling a specific business capability end-to-end. This approach often includes everything from the UI layer down to the data access layer for a given feature in one place, as described in the following key points:
- Each slice represents an end-to-end implementation of a feature.
- VSA promotes loose coupling between features, making the application more modular and easier to maintain.
- Each slice is responsible for a single feature or use case, which fits well with SOLID’s Single Responsibility Principle (SRP).
- VSA allows for features to be developed, tested, and deployed independently, which is beneficial for microservices or distributed systems.
Folder structure summary
Both organizational techniques have their merits, and the choice depends on the specific needs of your project. Technological concerns organization is straightforward and familiar but can become unwieldy as the project grows. Feature folders, while potentially introducing a learning curve, offer better modularity and scalability, aligning well with the principles of VSA.
Feature folders are particularly advantageous in larger projects or those with distributed teams, as they promote better organization and isolation of features, leading to improved maintainability and flexibility in the long run.
Central Package Management
By default, with the .NET SDK CLI and most code editor-created projects, if you need to reference a NuGet package, you add the reference to the package name and version directly in the project file.
Central Package Management (CPM) is a feature that simplifies the management of NuGet package versions across multiple projects within a solution. This is particularly useful for large solutions with many projects, where managing package versions individually can become cumbersome and error-prone.
The key features and benefits of CPM include:
- Centralized Control: CPM allows you to define package versions in a single file, typically
Directory.Packages.props
, which is placed in the root directory of your solution. This file centralizes the version information for all NuGet packages used across the projects in your solution. - Consistency: Ensures consistent package versions across multiple projects. By having a single source of truth for package versions, it eliminates discrepancies that can occur when different projects specify different versions of the same package.
- Simplified Updates: Updating a package version in a large solution becomes straightforward. You update the version in the central file, and all projects referencing that package automatically use the updated version. This significantly reduces the maintenance overhead.
- Reduced Redundancy: Removes the need to specify package versions in individual project files (
.csproj
). This makes project files cleaner and easier to manage, as they no longer contain repetitive version information.
Good Practice: It is important to regularly update NuGet packages and their dependencies to address security vulnerabilities.
Let’s set up Central Package Management for a solution that we will use throughout the rest of the chapters in this book:
- Create a new folder named
web-dev-net9
that we will use for all the code in this book. For example, on Windows, create a folder:C:\web-dev-net9
. - In the
web-dev-net9
folder, create a new folder namedMatureWeb
. - In the
MatureWeb
folder, create a new file namedDirectory.Packages.props
. - In
Directory.Packages.props
, modify its contents, as shown in the following markup:<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</Man agePackageVersionsCentrally> </PropertyGroup> <ItemGroup Label="For EF Core."> <PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" /> </ItemGroup> <ItemGroup Label="For testing."> <PackageVersion Include="coverlet.collector" Version="6.0.2" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" /> <PackageVersion Include="xunit" Version="2.9.2" /> <!--The following package was still a preview on .NET 9 release day.--> <PackageVersion Include="xunit.runner.visualstudio" Version="3.0.0-pre.49" /> <PackageVersion Include="Microsoft.Playwright" Version="1.49.0" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" /> </ItemGroup> <ItemGroup Label="For ASP.NET Core websites."> <PackageVersion Include= "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.0" /> <PackageVersion Include= "Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.0" /> </ItemGroup> <ItemGroup Label="For deployment."> <PackageVersion Include= "Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> </ItemGroup> <ItemGroup Label="For caching."> <!--The following package was still a preview on .NET 9 release day.--> <PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.9.24556.5" /> </ItemGroup> <ItemGroup Label="For ASP.NET Core web services."> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" /> <PackageVersion Include="NSwag.MSBuild" Version="14.1.0" /> <PackageVersion Include= "Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.OData" Version="9.0.0" /> </ItemGroup> <ItemGroup Label="For FastEndpoints web services."> <PackageVersion Include="FastEndpoints" Version="5.31.0" /> </ItemGroup> <ItemGroup Label="For Umbraco CMS."> <PackageVersion Include="Umbraco.Cms" Version="14.3.1" /> <PackageVersion Include="Microsoft.ICU.ICU4C.Runtime" Version="72.1.0.3" /> </ItemGroup> </Project>
Warning! The <ManagePackageVersionsCentrally>
element and its true
value must go all on one line. Also, you cannot use floating wildcard version numbers like 9.0-*
as you can in an individual project. Wildcards are useful to automatically get the latest patch version, for example, monthly package updates on Patch Tuesday. But with CPM you must manually update the versions.
For any projects that we add underneath the folder containing this file, we can reference the packages without explicitly specifying the version, as shown in the following markup:
<ItemGroup>
<PackageReference
Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference
Include="Microsoft.EntityFrameworkCore.Design" />
</ItemGroup>
You should regularly review and update the package versions in the Directory.Packages.props
file to ensure that you are using the latest stable releases with important bug fixes and performance improvements. For example, the Microsoft.Extensions.Caching.Hybrid
package was still in preview on the day of .NET 9’s release when I finished final drafts. By the time you read this, it is likely to be out of preview, so update its version number.
Good Practice: I recommend that you set a monthly event in your calendar for the second Wednesday of each month. This will occur after the second Tuesday of each month, which is Patch Tuesday when Microsoft releases bug fixes and patches for .NET and related packages.
For example, in December 2024, there are likely to be new versions, so you can go to the NuGet page for each of your packages. You can then update the versions if necessary, for example, as shown in the following markup:
<ItemGroup Label="For EF Core.">
<PackageVersion
Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="9.0.1" />
...
</ItemGroup>
Before updating package versions, check for any breaking changes in the release notes of the packages. Test your solution thoroughly after updating to ensure compatibility.
Educate your team and document the purpose and usage of the Directory.Packages.props
file to ensure everyone understands how to manage package versions centrally.
You can override an individual package version by using the VersionOverride
attribute on a <PackageReference />
element, as shown in the following markup:
<ItemGroup>
<PackageReference
Include="Microsoft.EntityFrameworkCore.SqlServer"
VersionOverride="9.0.0" />
...
</ItemGroup>
This can be useful if a newer version introduces a regression bug.
More Information: You can learn more about CPM at the following link:
https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management