Skip to content
Xperience by Kentico StyleguideXperience by Kentico StyleguideXperience by Kentico Styleguide
GitHubYouTube

ASP.NET Core

Configuration

Store Environment-Specific Configuration in appsettings.json

Suggested

  • Use appsettings.json to store configuration that is environment specific
  • Structure configuration hierarchically using JSON
  • Provide intelligent defaults when reading in configuration in the ASP.NET Core application

Why?

Test and Production aren’t the only environments that developers need to consider. “Local” is likely the environment where developers need the most control over configuration and user secrets allows for easy customization that is scoped to each developer’s individual workspace.

Why?

Values deployed to the application file system in production are more secure than Administration accessible settings which can be read and updated by an administrator. If configuration needs to be more secure and does not need to be edited while the application is running, appsettings.json is a better source of values than Admin settings.

Why?

All CI/CD systems have a way to populate configuration files with environment-specific or security-sensitive values after an application build has completed but before the build artifact is deployed to an environment. These values (ex: database connection strings, API keys) are stored with their own security settings to ensure they cannot be read or updated without specific permissions.

Fluent Builder APIs for DI and Middleware Registration

Recommended

  • Create extension methods to abstract application setup out of Startup.cs or Program.cs
  • Use a fluent builder API pattern to keep setup readable
  • Add helper extension methods to make non-fluent setup APIs chainable

Why?

Application setup code is often very declarative without much conditional logic and it describes what the application uses and what it does, rather than how it does it. To make setup code more readable, a fluent builder API can remove unnecessary syntax and read more like a table of contents than C# code.

public class Startup
{
    public void ConfigureServices(IServiceCollection services) =>
        services
            .AddAppXperience()
            .AddAppMembership()
            .AddAppMVC()
            .AddAppCore(Environment, Config);
}

It’s very clear, from a high level, what the major areas of functionality are of this application, and these extension methods can be made more or less high level depending on the application’s complexity.

Why?

The same pattern can be followed for defining the ASP.NET Core middleware pipeline, which is already a very declarative section of the application:

public class Startup
{
    public void Configure(IApplicationBuilder app) =>
        app
            .InitKentico()
            .IfDevelopment(Environment, a => a
                .UseDeveloperExceptionPage())
            .IfNotDevelopment(Environment, a => a
                .UseExceptionHandler(new ExceptionHandlerOptions
                {
                    AllowStatusCode404Response = true,
                    ExceptionHandlingPath = "/error"
                })
            )
            .UseHttpsRedirection()
            .UseStaticFiles()
            .UseStatusCodePagesWithReExecute("/not-found", "?code={0}")
            .IfDevelopment(Environment, a => a
                .UseSwagger()
                .UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1")))
            .UseKentico()
            .UseAuthentication()
            .UseEndpoints(endpoints =>
            {
                endpoints.Kentico().MapRoutes();

                _ = endpoints.MapControllers();
            });
}

Why?

Each extension method can then call additional extension methods. Since application setup is not stateful and lacks logic, there’s no harm in using static extension methods and avoiding intermediate variables.

Why?

An organized, high-level Startup.cs will be easier to adapt to the new ASP.NET Core minimal startup

::: tip Maintaining Startup.cs

To learn more about how to organize and maintain a Startup.cs file, read Kentico Xperience Design Patterns: Good Startup.cs Hygiene

:::

Group Extensions Based on Sub-System

Suggested

  • Create separate static configuration classes for each area of functionality in an application
  • Create separate static IServiceCollection extension methods in those classes for each group of dependencies within a feature

Why?

These extensions tell a story about what features and dependencies your app has. With these extension classes and methods, looking at Startup.cs will quickly show what kinds of things an application does.

Examples:

public static class ECommerceConfiguration
{
    public static IServiceCollection AddEcommerce(this IServiceCollection services) =>
        services
            .AddStripe();

    private static IServiceCollection AddStripe(this IServiceCollection services)
    {
        // ...
    }
}

public static class ApplicationConfiguration
{
    public static IServiceCollection AddApplicationCore(this IServiceCollection services) =>
        services
            .AddXMLSitemaps();

    private static IServiceCollection AddXMLSitemaps(this IServiceCollection services)
    {
        // ...
    }
}

public static class IntegrationConfiguration
{
    public static IServiceCollection AddLegacyIntegration(this IServiceCollection services) =>
        services
            .AddActiveDirectory();

    public static IServiceCollection AddActiveDirectory(this IServiceCollection services)
    {
        // ...
    }
}

Use Reflection vs Manual DI Registration

Suggested

  • Use reflection for registering types with many implementations
  • Use a library like Scrutor for more powerful DI registration

Why?

One of the benefits of a DI conatiner is its ability to construct types for you through its access to all constructable types in an application.

Razor

Keep View Injected Dependencies Minimal

Recommended

  • Only perform presentational logic inside a Razor View
  • Use data retrieval and business logic to prepare View Models outside of Razor View files
  • Consider Models in Razor Views as prepared and ready to be used

Why?

Razor Views are difficult to debug and are not meant for error handling. By preparing a View Model in advance, we can trust the Model we have access to in a View is fully populated and ready to be used without encoutering and error.

If any errors are going to be encountered when rendering a Views, this would occur when the View Model is being prepared.

Why?

Although ASP.NET Core enables View service injection using the @inject Razor directive, this feature should be used for a limited set of use cases - e.g localization, JSON serialization.

Break up Layout Into Smaller View Components

Recommended

  • Limit the size of the _Layout.cshtml file.
  • Abstract sections of page layout (like <meta>, <link> or <script> elements) into View Components
  • Simplify the _Layout.cshtml to tell a story about how the page is structured and what assets are used on it.

Why?

The main solution _Layout.cshtml will tend to grow over time, with new asset links and metadata tags being added regularly.Manage this growing complexity continuosly by componentizing sections of the_Layout.cshtml` and moving them to View Components.

Why?

Somewhere in a _Layout.cshtml you’ll likely want to use an injected service to populate a dynamic value. Instead of injecting this service into the View, injected it into a View Component which then renders the dynamic _Layout.cshtml content.

Why?

Keeping a _Layout.cshtml readable will keep every page in a solution slightly more readable and maintainable because the _Layout.cshtml is used acrros a solution.

View Components vs Partial Views

Recommended

  • If a section of a View is being abstracted into its own part, consider using a View Component over a Partial View

Why?

Better type checking, intellisense

Use the View Component Tag Helper

Recommended

Why?

Tag Helpers help to add behavior to HTML rendering with Razor, but use an HTML-like syntax instead of C#, which can be easier to read, especially for developers not as familiar with C#.

It’s recommended to use the Xperience by Kentico Page Builder Tag Helpers, so following the convention for non-Xperience View Components follows this convention.

Why?

The View Component Tag Helper will provide syntax highlighting, auto-completion, and parameter hints in most editors.

Invoking a View Component with C# using @await Component.InvokeAsync() provides no type safety for the component or its parameters, so it is almost always a worse developer experience than the View Component Tag Helper.

Move Razor configuration to the application root

Recommended

  • Move the ~/Views/_ViewImports.cshtml file to the ~/ application root.
  • Move the ~/Views/_ViewImports.cshtml file to the ~/ application root.

Why?

If we follow a Feature Folder based approach to project organization we will end up with Views, Templates, and Components outside the ASP.NET Core ~/Views folder, which would prevent the _ViewImports.cshtml and _ViewStart.cshtml files from being applied for Views outside of the ~/View folder.

Why?

The default ASP.NET Core template places the _ViewImports.cshtml and _ViewStart.cshtml files are stored in the ~/Views folder, which is helpful when you have ASP.NET Core Areas in your app.

Xperience by Kentico does not support Areas, so the _ViewImports.cshtml and _ViewStart.cshtml files application root to ensure they are shared across the entire project’s Razor Views.

This is a valid location for these files in ASP.NET Core projects and is a better default for Xperience by Kentico projects heavy reliance on Razor template rendering.

Co-Locate Controllers, View Models, and Views

Recommended

  • Place Controllers, View Models, and Views all in the same feature folder
  • Define Controllers (or View Component classes) and their View Model classes in the same file

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.

Name View files by their Page or Component

Suggested

  • Name your Razor View files to match the page or component they are used with
  • Drop the suffix from the .cshtml file name

Why?

Finding files through editor quick search (ex: ctrl+p or cmd+p in VS Code) often works by file name matching. Having dozens of files all named Default.cshtml for View Components can make it difficult to select the right one.

A similar problem occurs when the file is open in an editor as a tab - the file name is too generic to be helpful.

Why

View Component, Page Template, and MVC View files should be organized by feature folder next to their component. This naming convention will alphabetically group the files in your editor.

🌳 ~/Features
 |
 ├─📁 Blog
 |  ├── 📄 BlogPostPage_Default.cshtml
 |  ├── 📄 BlogPostPageTemplates.cs
 |  |── 📄 BlogLandingPage_Default.cshtml
 |  ├── 📄 BlogLandingPageTemplates.cs
 |  └── 📁 Components
 |       ├── 📄 BlogPostDetail.cshtml
 |       └── 📄 BlogPostDetailViewComponent.cs
 └── ...

Multiple Coupled Types per-file

Recommended

  • 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.