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
orProgram.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
- Use the View Component Tag Helper (
<vc:>
) in Razor Views
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.