-
Book Overview & Buying
-
Table Of Contents
-
Feedback & Rating
Real-World Web Development with .NET 10 - Second Edition
By :
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.
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, a 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 its 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. |
|
|
HTTP HTTPS |
An ASP.NET Core project for complex websites that uses a mixture of static HTML files and MVC Razor Views. |
|
|
HTTP HTTPS |
An ASP.NET Core project for a Web API, a.k.a. 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
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.
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:
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.
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:
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:
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.
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, as shown in the following markup:
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="10.0.0" />
...
</ItemGroup>
Central Package Management (CPM) is a feature that simplifies the management of NuGet package versions across multiple projects and solutions within a directory hierarchy. 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:
Directory.Packages.props, which is placed in the root directory of a directory hierarchy that contains all your solutions and projects. This file centralizes the version information for all NuGet packages used across the projects in your solutions..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.
Microsoft packages usually have the same number each month, like 10.0.2 in February, 10.0.3 in March, and so on. You can define properties at the top of your Directory.Packages.props file, and then reference these properties throughout the file. This approach keeps package versions consistent and makes updates easy.
For example, in your Directory.Packages.props file, at the top of the file, within a <ProjectGroup> tag, define your custom property and then reference it for the package version, as shown in the following markup:
<Project>
<PropertyGroup>
<MicrosoftPackageVersion>10.0.2</MicrosoftPackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore"
Version="$(MicrosoftPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging"
Version="$(MicrosoftPackageVersion)" />
<!-- Add more Microsoft packages as needed. -->
</ItemGroup>
<!-- Other packages with specific versions. -->
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
</ItemGroup>
</Project>
Note the following about the preceding configuration:
<PropertyGroup> element, the line <MicrosoftPackageVersion>10.0.2</MicrosoftPackageVersion> defines the property. This value can be changed once at the top of the file, and all references will update automatically.$(PropertyName) to reference the defined property. All occurrences of $(MicrosoftPackageVersion) will resolve to the version number that you set.When the monthly update rolls around, for example, from 10.0.2 to 10.0.3, you only have to update this number once.
You might want separate properties for related packages if they differ in version number, such as:
<AspNetCorePackageVersion>10.0.3</AspNetCorePackageVersion>
<EFCorePackageVersion>10.1.2</EFCorePackageVersion>
This allows independent updates if packages diverge in their release cycles or versions later.
After making changes, at the terminal or command prompt, run the following command:
dotnet restore
This will verify the correctness of your references and quickly alert you if you’ve introduced errors. By adopting this pattern combined with CPM, you simplify version management, reduce redundancy, and make your projects easier to maintain over time.
Good practice: Choose clear and consistent property names, like MicrosoftPackageVersion or AspNetCorePackageVersion, to easily distinguish between different package ecosystems. Check your Directory.Packages.props file into source control. Regularly update and test after changing versions to ensure compatibility.
Let’s set up CPM for a solution that we will use throughout the rest of the chapters in this book. We will define item groups for the following packages:
Let’s go:
web-dev-net10 that we will use for all the code in this book. For example, on Windows, create a folder: C:\web-dev-net10.web-dev-net10 folder, create a new folder named MatureWeb.MatureWeb folder, create a new file named Directory.Packages.props. At the command prompt or terminal, you can optionally use the following command: dotnet new packagesprops
To save you time manually typing this large file, you can download it at the following link: https://github.com/markjprice/web-dev-net10/blob/main/code/MatureWeb/Directory.Packages.props.
Directory.Packages.props, modify its contents, as shown in the following markup:
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<Net10>10.0.0</Net10>
</PropertyGroup>
<ItemGroup Label="For EF Core.">
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="$(Net10)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite"
Version="$(Net10)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design"
Version="$(Net10)" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools"
Version="$(Net10)" />
</ItemGroup>
<ItemGroup Label="For testing.">
<PackageVersion Include="coverlet.collector"
Version="6.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk"
Version="18.0.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio"
Version="3.1.6" />
<PackageVersion Include="Microsoft.Playwright"
Version="1.56.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
</ItemGroup>
<ItemGroup Label="For ASP.NET Core websites.">
<PackageVersion Include=
"Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore"
Version="$(Net10)" />
<PackageVersion Include=
"Microsoft.AspNetCore.Identity.EntityFrameworkCore"
Version="$(Net10)" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI"
Version="$(Net10)" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing"
Version="$(Net10)" />
</ItemGroup>
<ItemGroup Label="For caching.">
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid"
Version="$(Net10)" />
</ItemGroup>
<ItemGroup Label="For ASP.NET Core web services.">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi"
Version="$(Net10)" />
<PackageVersion Include="Scalar.AspNetCore"
Version="2.10.3" />
<PackageVersion Include="Refit" Version="8.0.0" />
<PackageVersion Include="Refit.HttpClientFactory"
Version="8.0.0/>
<PackageVersion Include=
"Microsoft.AspNetCore.Authentication.JwtBearer"
Version="$(Net10)" />
<PackageVersion Include="Asp.Versioning.Mvc"
Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer"
Version="8.1.0" />
</ItemGroup>
<ItemGroup Label="For OData web services.">
<PackageVersion Include="Microsoft.AspNetCore.OData"
Version="9.4.1" />
</ItemGroup>
<ItemGroup Label="For FastEndpoints web services.">
<PackageVersion Include="FastEndpoints" Version="7.1.0" />
<PackageVersion Include="FluentValidation" Version="12.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.JsonPatch"
Version="$(Net10)" />
<PackageVersion Include=
"Microsoft.AspNetCore.JsonPatch.SystemTextJson"
Version="$(Net10)" />
</ItemGroup>
<ItemGroup Label="For Umbraco CMS.">
<PackageVersion Include="Umbraco.Cms" Version="17.0.0" />
<PackageVersion Include="Microsoft.ICU.ICU4C.Runtime"
Version="72.1.0.3" />
</ItemGroup>
</Project>
Warning! The <ManagePackageVersionsCentrally> element and its true value must all go on one line. Also, you cannot use floating wildcard version numbers like 10.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.
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 2025, there are likely to be patch versions, so you can go to the NuGet page for each of your packages. You can then update individual package versions if necessary, for example, as shown in the following markup:
<ItemGroup Label="For EF Core.">
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer"
Version="10.0.1" />
Or, if you have defined custom properties referenced by multiple packages, update those version numbers, as shown in the following markup:
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<Net10>10.0.1</Net10>
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.
In each individual project file, you can override an individual package version by using the VersionOverride attribute on a <PackageReference /> element, as shown in the following markup:
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer"
VersionOverride="10.0.0" />
This can be useful if a newer version introduces a regression bug, so you can force the use of an older version without the bug until the bug is fixed in a later patch version.
You can learn more about CPM at the following link: https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management.
If you use CPM and you have more than one package source configured for your code editor, as shown in Figure 1.7, then you will see NuGet warning NU1507. For example, if you have both the default package source (https://api.nuget.org/v3/index.json) and a custom package source configured:
There are 2 package sources defined in your configuration. When using central package management, please map your package sources with package source mapping (https://aka.ms/nuget-package-source-mapping) or specify a single package source. The following sources are defined: https://api.nuget.org/v3/index.json, https://northwind.com/packages/.

Figure 1.7: Visual Studio with two NuGet package sources configured
The NU1507 warning reference page can be found at the following link: https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1507.
Package Source Mapping (PSM) can help safeguard your software supply chain if you use a mix of public and private package sources, as in the preceding example.
By default, NuGet will search all configured package sources when it needs to download a package. When a package exists on multiple sources, it may not be deterministic which source the package will be downloaded from. With PSM, you can filter, per package, which source(s) NuGet will search.
PSM is supported by Visual Studio 2022, .NET 6 and later, and NuGet 6 and later. Older tooling will ignore the PSM configuration.
To enable PSM, you must have a nuget.config file.
Good practice: Create a nuget.config file at the root of your source code directory hierarchy.
In a PSM file, there are two parts: defining package sources and mapping package sources to packages. All requested packages must map to one or more sources by matching a defined package pattern. In other words, once you have defined a packageSourceMapping element, you must explicitly define which sources every package (including transitive packages) will be restored from.
For example, if you want most packages to be sourced from the default nuget.org site, but there are some private packages that must be sourced from your organization’s website, you would define the two package sources and set the mapping (assuming all your private packages are named Northwind.Something), as shown in the following markup:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- <clear /> ensures no additional sources are inherited from another config file. -->
<packageSources>
<clear />
<!-- key can be any identifier for your source. -->
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="Northwind" value="https://northwind.com/packages" />
</packageSources>
<!-- All packages sourced from nuget.org except Northwind packages. -->
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="Northwind">
<package pattern="Northwind.*" />
</packageSource>
</packageSourceMapping>
</configuration>
Let’s create a nuget.config file for all the solutions and projects in this book that will use nuget.org as the source for all packages:
MatureWeb folder, create a new file named nuget.config. At the command prompt or terminal, you can use the following command: dotnet new nugetconfignuget.config, modify its contents, as shown in the following markup:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!-- <clear /> ensures no additional sources are inherited from another config file. -->
<packageSources>
<clear />
<!-- key can be any identifier for your source. -->
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<!-- All packages sourced from nuget.org. -->
<packageSourceMapping>
<!-- key value for <packageSource> should match key values from <packageSources> element -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
You can learn more about nuget.config at the following link: https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file.
You can learn more about PSM at the following link: https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping.
By default, compiler warnings may appear if there are potential problems with your code when you first build a project, but they do not prevent compilation, and they are hidden if you rebuild. Warnings are given for a reason, so ignoring warnings encourages poor development practices.
Some developers would prefer to be forced to fix warnings, so .NET provides a project setting to do this, as shown highlighted in the following markup:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
I have enabled the option to treat warnings as errors in (almost) all the solutions in the GitHub repository.
If you find that you get too many errors after enabling this, you can disable specific warnings by using the <WarningsNotAsErrors> element with a comma-separated list of warning codes, as shown highlighted in the following markup:
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>0219,CS8981</WarningsNotAsErrors>
You can learn more about controlling warnings as errors at the following link: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/errors-warnings#warningsaserrors-and-warningsnotaserrors.
Now that we have reviewed how to structure projects and manage packages in your projects, we will create some projects to define some sample data that we will then use throughout the rest of the book.
Change the font size
Change margin width
Change background colour