Project Setup and Configuration
These recommendations focus on the .NET Solution structure and individual project configuration.
Documentation
Add a Repository README.md
- Add a
README.md
to the root of your repository - Write a brief description of the project
- Detail steps to get the project running for local development
Why?
New developers on the project will reference a README.md
to orient themselves in the project. Setup instructions will help them troubleshoot working on the application.
Why?
Having project information in a README.md
that exists in the project's repository (co-located) increases the likelihood of the information being kept up to date as the project changes.
Enhance the README.md
- Create a table that lists the details of Web and Database Server details for each environment
- List project features / integrations
- Link to other project documentation
Why?
Environment information (URLs, Server Names) is often implicit knowledge on a team. Detailing this in the project README.md
makes it discoverable and maintainable, reducing information silos.
Why?
A list of features and integrations is a good high level substitute for package and infrastructure dependencies in an application. This list often shows what the volatile dependencies and complex areas of an application are.
Maintain Architectural Decision Records (ADR)
What is an Architectural Decision Record?
An Architectural Decision (AD) is a software design choice that addresses a functional or non-functional requirement that is architecturally significant. https://adr.github.io/
An Architecture Decision Record is a tool for documenting a decision that has been made (or is under discussion) related to the architecture of a particular system or application. https://ardalis.com/getting-started-with-architecture-decision-records/
- Create an
ADR.md
file in the root of your project's repository - Add a dated entry as a record of each significant architecture addition or change
- Include the entry as part of a code review process
Why?
Business rule and project requirement changes are often recorded by project management or stakeholders, but the impact that these changes have on an application can easily be forgotten. An ADR
file encourages recording a history of changes that developers can consult in the future to better understand decisions made in the past.
Why?
An ADR entry should include details about an architectural decision. If there were multiple solutions available, the entry should describe each one, including their pros/cons, and why the chosen solution was selected. Writing out this decision making process can help reflect on the why of a change and expressing explicitly any implications of the solution.
Add XML Doc Comments
- Use the powerful XML doc comment syntax to annotate types, methods, and properties
Why?
Swagger API documentation for HTTP/REST APIs has become a standard in most enterprise web applications. XML doc comments helps to make these auto-generated pages more useful with human readable descriptions.
Why?
A type name can give a developer a hint as to what a type is (ex: IPageRetriever
), but the name alone might not indicate when or why a type or method should be used. XML doc comments are a great place to add these explanations. Comments that clearly describe what something is and why it exists, while avoiding describing how it works, can help developers using those types and methods from needing to explore the source code.
Why?
Method overloading is a useful C# feature, but variations on parameter lists can be difficult to understand without XML doc comments detailing what parameters are used for.
Why?
Proper use of XML doc comments can be refactor-proof when referring to other code as symbols instead of strings (ex <see cref="SomeType" />
).
/// <summary>
/// Validates URls based on the configuration supplied by <see cref="URLConfiguration" />
/// </summary>
public class URLValidator
{
private readonly URLConfiguration config;
public URLValidator(IOptions<URLConfiguration> config) =>
this.config = config.Value;
// more methods
}
TIP
In the above code, renaming URLConfiguration
using Visual Studio's refactoring feature (f2
) will ensure the XML doc comment on URLValidator
has the referenced type renamed as well.
Commit SQL Scripts
- Create a documentation folder in the repository and add dated SQL scripts to it
- These scripts should be documented with comments and detail one time data migrations or imports
- They can be linked to from an ADR
- Include Kentico Xperience Hotfix scripts
Why?
The impact of changes to the database are not visible through source control, so it can be difficult to determine when or why changes to data/schema were made from SQL. Committed scripts help match these changes with related code changes at a given point in time.
Why?
Some Kentico Xperience 13 hotfixes include SQL scripts that the KIM or Hotfix Utility will apply for you. However, in production environments these should be applied manually (or through CI/CD automation) and having them documented in the repository makes them easily accessible.
Why?
Data imports might need to be re-executed in the future, or in multiple environments. Having these scripts recorded means they don't need to be re-written from scratch.
Why?
Scripts in source control can be validated during a code-review before they are executed in a live environment.
.NET and C#
Use the Latest Target Frameworks for Applications
- Target .NET 6.0 for Kentico Xperience 13 (latest version as of 2022/01)
Why?
Kentico Xperience 13 supports ASP.NET MVC 5 and ASP.NET Core 3.1, 5.0, and 6.0. MVC 5 development should be avoided for any greenfield project. ASP.NET Core 6.0 offers the most features and best performance without any limitations for Kentico Xperience.
Why?
Kentico Xperience 13's product model includes regular refreshes to the platform. To get the latest features, developers will need to regularly apply refreshes and update their custom code to turn on these features. These regular updates are a great opportunity to ensure the latest supported version of .NET is being used.
Why?
Microsoft is releasing new version of C# along with new versions of .NET on a yearly cadence. To benefit from the newest language features (ex: C#10), developers need to be on the latest version of .NET.
Use .NET Standard 2.0 for Shared Libraries
- Code shared between the Content Management (CMS .NET 4.8 ASP.NET Web Forms) application and the Content Delivery (ASP.NET Core) application needs to target the highest common denominator for both versions of .NET
netstandard2.0
provides the most features and best compatibility
Why?
Despite the Content Management and Content Delivery apps being built on different eras of technology and serving different purposes, in typical Kentico Xperience 13 projects they will share some code. Custom module classes and generated classes for Xperience objects are normally shared between both applications.
Why?
.NET 4.x projects cannot be referenced by modern .NET 5/6 projects. The same is true in the other direction. Therefore, .netstandard2.0
is the most common target framework for code shared between these two versions of .NET.
Use SDK-Style Projects
- Create new class libraries using the .NET CLI using the
dotnet new
command or the Visual StudioFile -> New Project
UI. - Ensure the new class library
.csproj
file beings with<Project Sdk="Microsoft.NET.Sdk">
Why?
All modern .NET Core/.NET 5+ projects use the SDK-Style Project format which includes a large number of improvements and new features when compared with the project format of older .NET 4.x projects. Some of these features, like hand editable .csproj
files that can be edited without 'unloading' projects in Visual Studio, are welcomed conveniences. Others, like properly tracked transitive NuGet package references, make the difference between applications that do and do not function correctly when run. Even .netstandard2.0
class libraries can use the new SDK-Style projects.
Why?
.NET Developers have had a history of being intimidated by .csproj
files which were meant to be managed by tooling in Visual Studio. SDK-Style projects are meant to be manageable by and approachable to both new and experienced .NET developers.
WARNING
The CMS .NET 4.8 ASP.NET Web Forms application does not support the SDK-Style project format, however it can support the modern NuGet <PackageReference>
syntax.
Enable Nullable Reference Types
- Turn on compiler analysis for nullable reference types for any project supporting C# 8 or above
- Add
<nullable>enable</nullable>
to a<PropertyGroup>
in your.csproj
file to enable nullable reference types - Add
<WarningsAsErrors>nullable</WarningsAsErrors>
to a<PropertyGroup>
in your.csproj
file to treat nullable reference type warnings as compilation errors
Why?
One of the most common exceptions developers encounter in their code is the NullReferenceException
. This typically happens because of C#'s implicit type union of all reference types and the value null
. The compiler can't tell the developer that a value might be null before it is used, so developers are forced to add checks themselves. It's easy to forget these checks which leads to the exceptions are runtime.
Enabling nullable reference types makes the type union of reference types and null
explicit, which means the compiler can alert us when we haven't guarded against null
values.
Why?
Nullable reference types were a C# language feature enhancement to help developers catch errors at compile time. By default, the compiler will treat missing null checks or nullable type mismatches as warnings, but those defeat the purpose of using the compiler to help us write more robust code. By having the compiler emit errors instead of warnings, we ensure we are getting the most protection for our application with this feature.
Directory.Build.props / Directory.Build.targets for Shared Configuration
- In the root of your repository, create a
Directory.Build.props
file for shared project configuration - In the root of your repository, create a
Directory.Build.targets
file for shared project build actions
Why?
Directory.Build.props
and Directory.Build.targets
are a convenient way to define common MSBuild properties and behavior in a single location. While these files are most useful in libraries that are shared via NuGet packages, applications can also benefit from them.
Adding the following to a Directory.Build.props
would enable nullable reference types for the entire solution and ensure any nullability warnings are treated as compilation errors:
Directory.Build.props
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>
Why?
If you embed ownership metadata in your compiled assemblies, having to repeat this metadata in every .csproj
file can be tedious and error prone. Instead it can be specified in Directory.Build.props
and applied to all projects:
Directory.Build.props
<PropertyGroup>
<Company>Your Company</Company>
<Authors>$(Company)</Authors>
<Copyright>Copyright © $(Company) $([System.DateTime]::Now.Year)</Copyright>
<Trademark>$(Company)™</Trademark>
<Product>$(Company) - Your Project</Product>
<VersionPrefix>1.0.0</VersionPrefix>
</PropertyGroup>
Use EditorConfig for Consistent C#
- Generate an .editorconfig in Visual Studio to enforce your current code conventions
- Adopt an editorconfig from a popular open source project to align with its conventions
- Explore the Code style and quality analysis options and author a new file from scratch
Why?
.NET has been moving in the direction of providing tools and configuration for enforcing conventions within projects. The dotnet format command provided by the .NET CLI works with .editorconfig
files to automatically clean up a project's source code to meet the code style and quality to match the configuration.
Using the Code Cleanup on Save extension (or built-in Visual Studio features) ensures the IDE handles formatting your files according to .editorconfig
rules any time you make a change.
Why?
Defining formatting conventions at the beginning of a project and providing tooling to automatically apply this formatting reduces noise in source control diffs/merges, reduces the cognitive overhead for developers new to a project, and helps prevent team debates over formatting in the future.
Project Structure
Create Solution Items Folder
- Add a new folder in your .NET solution to contain items at the root of your project that should be visible in Visual Studio
- It's common to name this folder
Solution Items
Why?
.NET solutions have traditionally focused on source code and projects since they are a target for compilation. However, it's also common to have many files in a .NET repository that are not code or projects, like README.md and .gitignore
files. These should be viewable and editable in the same way that source code is editable. Having these files visible (in Visual Studio or Rider) makes them more likely to be edited and kept up to date.
Why?
Having a solution folder with a convention-based name makes it easier for developers to explore a project and find common non-code files.
Create Build Folder
Create Source Folder
Co-Locate Tests and Libraries
- In a .NET solution, keep test projects adjacent to the applications and libraries they are associated with
- Create a solution folder for each project and place both the application (or library) and its test project in the solution folder
Why?
By co-locating tests and the source the tests were written for, the tests and code are more likely to stay in-sync.
Why?
Developers will be more likely to keep testing in mind when adding new features if they are reminded there is a place to add tests in the solution and they don't have to go hunt down the correct test project.
Why?
Solution folders are a virtual organizational tool for .NET projects. This means source code and test code can still be located in different paths on the file system (ex: src/
and test/
) even if they are co-located in the solution.
Use Source Link for NuGet Packaged Libraries
- Include the correct Source Link package in your library
- Verify the quality of the NuGet package generated by the library using NuGet Package Explorer
- Publish the symbols with the library NuGet package
Why?
Source Link is a technology for .NET that enables developers to debug into the source of Release
compiled assemblies packaged in NuGet packages.
Visual Studio has been investing in the developer experience of Source Link by exposing the source code of NuGet packages built with Source Link directly in the Visual Studio Solution Explorer.
Being able to debug the source code of a NuGet when running an application helps developers better understand how to use a package and diagnose issues more quickly.
Why?
Source Link can be used for internal packages exposed through a private NuGet feed (like GitLab or Azure DevOps) or through a public NuGet server like NuGet.org, so all teams and projects can benefit.
TIP
For an example of a public project built using Source Link, see Xperience Community - RSS Feeds.
Feature Folders (Vertical Slice Architecture)
- Create folders in a .NET project based on a feature
- Organize and architect classes to be feature-oriented
Why?
Developers work on features so organizing code by feature helps developers find many of the pieces of code they need to modify when working on a feature. If they need to create new code for a feature, it's obvious, based on the feature-oriented organization, where that code should go in a project.
Why?
Projects organized by features are more comprehensible because the feature folders end up being a list of the application's main entry points an functionality. The feature folders also aid in revealing the complexity level of a given feature compared to another.
Why?
Feature folders encourage developers to think about functionality as a Vertical Slice, which is an architecture that leads to features that are easier to work on without incurring regressions elsewhere in an application. Vertical slices are also easier to separate into their own applications if they end up having unique scalability or deployment requirements.
Avoid organizing by type
Developers are never tasked to work on "interfaces" or "view models". Instead they are tasked with resolving issues or creating new functionality for features. Organizing code by what 'type' of a thing it is (ex: "interface", "implementation", "factory", "view model") focuses too much on a somewhat arbitrary technical detail, while the real focus in a codebase should be on business use-case.
Co-Locate Controllers, View Models, and Views
- Place Controllers, View Models, and Views all in the same feature folder
- Move the
_ViewImports.cshtml
and_ViewStart.cshtml
files to the root of the ASP.NET Core project so they are accessible to Views outside the~/Views
folder - Optional: Define Controllers (or View Component classes) and their View Model classes in the same file
- Optional: Customize a ViewLocationExpander to help MVC understand the conventions for your project structure
Why?
The majority of code in an ASP.NET Core MVC application is related to presentation of information. Models, Views, and Controllers are all presentation concerns and are all edited together - a change in a View Model typically results in a change in both a Controller and View. By co-locating these files we aren't breaking some rule of 'separation of concerns' since all of these files are for the same (presentation) concern. Instead we are making our lives easier when working in the project since we won't need to jump around the project folder hierarchy every time we make a change to presentation functionality.
Why?
Most developers working on a Kentico Xperience application won't only work in C# files or Razor Views - we tend to have responsibilities for all areas of content presentation. Separating Views into their own folder creates an artificial barrier that doesn't align with developer workloads.
Why?
View Models only exist to pass data from Controllers (and View Component classes) to Views. They are never used outside of this role and they are typically unique to a Controller/View pair. This means these files are all tightly coupled. While too many lines of code in a single file can be a code smell, this is typically only when a single unit of encapsulation (class, method) has too many lines.
There's nothing wrong with defining multiple classes in the same file, especially when they are tightly coupled, and co-locating them will help increase developer productivity when working on these types.
Multiple Coupled Types per-file
- Keep coupled classes/types in the same file
- Example: Request parameters, Controllers/View Components, and View Models
- Example: DTOs and the methods that produce them
Why?
The C# convention of 1 class per file was more beneficial when defining types in C# was much more verbose. Today, C# record types (introduced in C# 9 / .NET 5) offer an extremely terse type definition with primary constructors. Often types can be defined on a single line.
File scoped usings and global/implicit usings, both introduced in C# 10 / .NET 6, also decrease the amount of boilerplate per file. The result is that a record defined as public record User(string Name, string Email);
might be the only line in a file if we follow the 1 class per file rule.
Why?
Co-locating code that changes together is always preferable over spreading this code across a project. Similar to how feature folder improve productivity, including related types in the same file can make them easier to find, understand, and edit.
Editors also include shortcuts to quickly navigate to a symbol, which can be much faster than visually navigating through a folder structure to find a file.
- VS Code:
ctrl + T
(Go to Symbol in Workspace) - Visual Studio:
ctrl + T
(Go to) - Rider:
ctrl + alt + shift + T
(Navigate | Go to Symbol)
Once you open the file with the type you are looking for, you can find the related types in the same file.
Why?
Finding a file is too long when including related types together might be a good sign of too much complexity in those types. It can also be a good indicator that you are including things that aren't actually related. For example, the View Model returned by a Controller action is often more related to that action method than a completely separate action that's been bundled in the same Controller for convenience.
Kentico Xperience Design Patterns: Multiple Types Per File
A more detailed explanation can be read in the following blog post.
https://dev.to/seangwright/kentico-xperience-design-patterns-multiple-types-per-file-1a99