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

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/open in new window


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/open in new window

  • 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

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 descriptionsopen in new window.

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 Utilityopen in new window 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

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 refreshesopen in new window 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#10open in new window), 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 classesopen in new window and generated classes for Xperience objectsopen in new window 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 CLIopen in new window using the dotnet new command or the Visual Studio File -> 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 featuresopen in new window 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 supportopen in new window the modern NuGet <PackageReference> syntax.

Enable Nullable Reference Types

  • Turn on compiler analysis for nullable reference typesopen in new window 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 unionopen in new window of all reference typesopen in new window 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 behavioropen in new window 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 typesopen in new window 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#

Why?

.NET has been moving in the direction of providing tools and configuration for enforcing conventions within projects. The dotnet formatopen in new window 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 Saveopen in new window extension (or built-in Visual Studio featuresopen in new window) 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

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.

Why?

Source Linkopen in new window 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 experienceopen in new window 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 Feedsopen in new window.

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 Sliceopen in new window, 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 ViewLocationExpanderopen in new window 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 typesopen in new window (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 usingsopen in new window and global/implicit usingsopen in new window, 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-1a99open in new window