DEV Community

loading...
Cover image for Kentico Xperience Design Patterns: Multiple Types Per File

Kentico Xperience Design Patterns: Multiple Types Per File

Sean G. Wright
Chief Solutions Architect @WiredViews, founding partner @craftbrewingbiz. @KenticoXP MVP. love to learn / teach web dev & software engineering, collecting vinyl records, mowing my lawn, craft 🍺
・12 min read

Conventions in the C# development community dictate that each class or interface should be in its own file.

I think this is, generally, a fine convention to follow, especially for newer developers who might not understand the trouble they can get into when projects grow in size and complexity.

However, I also think there are many good opportunities to break with this convention when following the best practices of co-location and cohesion.

Let's explore this further...

📚 What Will We Learn?

  • The Goal of 1 Class Per File
  • Letter of the Law vs Spirit of the Law
  • Things that Change Together, Stay Together
  • Where Multiple Types per File make sense in Kentico Xperience

🧱 The Goal of 1 Class Per File

Back in the old times 👵🏽 (mid-2000s), C# was a new programming language that followed the patterns of languages like C++ and Java, and it was more verbose in syntax than it is today.

Here's an example made-up C# class from that era:

public class Product
{
    private int _id;

    public Product(int id)
    {
       _id = id;
    }

    public int Id
    {
        get
        {
            return _id;
        }
        set
        {
            _id = value;
        }
    }

    public bool IsValid
    {
        get
        {
            return _id > 0;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Ya, that's a lot of lines! Let's compare that to what it might look like as a C# 9 record type written today:

public record Product(int Id)
{
    bool IsValid => Id > 0;
}
Enter fullscreen mode Exit fullscreen mode

Yes, this is a contrived example, but I'm showing it to support the idea that C# code used to take up a lot more space, which meant each class definition of non-trivial complexity could be quite large 😯.

At that time, having multiple class definitions in one file could lead to difficult to navigate and understand files, and given the two extremes of every class in its own file and every class in one file, the file-per-class convention is unquestionably better.

One class-per-file can help developers focus on a unit of encapsulated behavior when working on a class. It's easy to tell how complex an individual class has become by looking at the line count of a file, and developers might establish a habit 😉 of defining clear functional boundaries for a class when it's not surrounded by others.

The true goals of the '1 class per file' rule are improving code organization and reducing code complexity, and I think these goals are mostly achieved with this coding convention. Of course, there are exceptions to every rule!

⚖ Letter of the Law vs Spirit of the Law

There is a philosophical concept called the letter and spirit of the law:

When one obeys the letter of the law but not the spirit, one is obeying the literal interpretation of the words (the "letter") of the law, but not necessarily the intent of those who wrote the law.

Always creating a file in C# for every type is following the 'letter of the law', but not necessarily the spirit or original intent of making code easier to navigate and understand.

Let's look at a couple of examples to support this idea...

🔎 Other Languages

Other programming languages, which scale to extremely large and complex applications, do not follow this convention. JavaScript and TypeScript applications frequently combine multiple types (functions, objects, interfaces) in the same file. There's a lot less ceremony 😎 compared to C# to create these reusable pieces of language functionality, which means they take up fewer lines of code.

F#, another .NET language, also frequently combines multiple types in the same file. This is largely because F# encourages the creation of many types to effectively model the valid states of an application and its syntax for creating new types is much more terse than C# (mostly from the lack of curly braces).

These languages combine types in a single file when those types are related or are directly dependent on each other, and applications written in these languages do not become unmanageable by breaking the '1 type per file' rule. They still use helpful code organization techniques and are enjoyed by large communities of developers. They follow the spirit 👻 of the law - organizing code for better understanding and maintainability.

🎁 New C# Features

As we saw in the original comparison between modern C# and the C# that was written 15 years ago, we can see that C# 9 record types reduce boilerplate significantly for certain use cases.

I think record types are great 💪🏼 for traditional data transfer objects (DTOs) and other simple classes.

Here's some record types from a Kentico Xperience site our team has running in production:

public record FooterNavigationQuery : 
    IQuery<FooterNavigationQueryData>;

public record FooterNavigationQueryData(
    Maybe<ImageContent> Logo, 
    Maybe<string> FooterHTML, 
    IEnumerable<FooterNavigationItem> Children);

public record FooterNavigationItem(
    Maybe<LinkContent> Link, 
    IEnumerable<FooterNavigationItem> Children);
Enter fullscreen mode Exit fullscreen mode

If we were to follow the letter of the law we would need to put each one of these record types in its own file. But that doesn't feel like it's following the spirit of the law.

1 line of code in a file (excluding namespaces) feels like we are being overly prescriptive and not considering the experience for other developers to have to open many different files 😩 to make updates to a feature or track down the source of a problem.

Even if we are using classes and not record types, we can write far less verbose syntax compared to older C#:

public class Product
{
    public Product(int id) => Id = id;

    public int Id { get; set; }

    public bool IsValid => Id > 0;
}
Enter fullscreen mode Exit fullscreen mode

The expression bodied member/constructor in the class definition above remove many lines of curly braces while still expressing the same meaning as the more verbose class syntax.

If we have multiple related classes that are less than 10 lines long each, should all those go in separate files 😑? If we put them all in the same file, are we making the code harder to read and use or are we making it easier to navigate and modify 🤔?


I'd also like to mention some of the features coming in C# 10 (as demo'd in this video from MS Build 2021) are helping to remove boilerplate.

File scoped namespaces remove the curly braces and indentation that comes with every new file and global usings also let us centralize the most commonly used using statements for namespaces.

With these C# features, as the files our types are defined in become more and more empty, why don't we fill them up with multiple concise and related types 😁?

🤝🏽 Things that Change Together, Stay Together

Some software design patterns can lead us to creating large classes, and we are discouraged from combining multiple large classes in a single file as was mentioned earlier.

Implementing the repository pattern can result in very large classes 📚, since the repository will typically contain a method for every variation of data querying, and updating, which our application needs to perform on a given entity.

In Kentico Xperience, we are mostly writing code to read data out of the database and render it in HTML, so let's look at an example for an "Article" Page Type.

Our ArticleRepository might have 10 different methods for retrieving Articles and their related content from the database in various ways. Any class with 10 data access methods is going to be pretty large and if those methods use DTOs as parameters and return types, we aren't going to be inclined to include those DTO class definitions in the same file as our ArticleRepository, even though that repository class is the owner of those types:

public class ArticleRepository
{
    public Article Get(int id)
    {
        // ...
    }

    public Article Get(Guid id)
    {
        // ...
    }

    public IEnumerable<Article> Get(ArticlesFilter filter)
    {
        // ...
    }

    public IEnumerable<Article> GetMostPopular()
    {
        // ...
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

When I say the ArticleRepository is the owner of the DTOs like ArticlesFilter and Article above, I mean that the only reason those types would change is because the implementation details of ArticleRepository changes. They are concretely connected to each other.

It would be nice to keep the Article class close to the ArticleRepository class since the repository is the one place in the code base responsible for creating new Article instances 😊! Cohesion relates these types in a way that is beneficial to our application.

However, the large size of the ArticleRepository is already discouraging us from adding any more to its file 😒.


MVC Controller classes are another place where we see this 'bag of methods' approach to class design.

Controllers have action methods that, typically, return View Models, and some also have model-bound method parameter types.

These View Models and parameter types are only created in 1 place in the application - the Controller action methods - so we might be inclined to define those classes where they are being used, in the same file as the Controller.

However, Controllers often grow in size just like Repositories, so we are then not incentivized to do this 😕.

Sometimes I see the argument made that View Models and Controller classes should be separated because that 'separates the concerns'.

I'm not very convinced 🧐 by that statement because both the Controller and View Model (along with the View) are all part of the same layer of the application - the Presentation concern.

Remember that all the pieces that make up the MVC acronym are not our entire 'application' and those pieces are not separate layers or concerns - they are all part of the same Presentation layer 🤓.

These types work extremely closely together and there's unlikely to be major changes to a View Model class without needing to also open the View and Controller files that use it.

🔬 Where Multiple Types per File Makes Sense

Hopefully I've been able to convince you 😄 that there are cases when keeping multiple types in the same file makes sense (assuming we're using certain C# language features or changing our architecture patterns slightly).

In a Kentico Xperience application we have many opportunities to break the 1 type-per-file rule and improve navigation and maintainability of an application...

MVC Controllers, Action Method Parameters, and View Models

As we saw above, the namesake MVC components are a great candidate for merging into 1 file, however we likely need to take a different approach on Controller class authoring 😮.

Instead of creating extremely large Controller classes, we can create many Controllers, each with 1 action method. This shrinks each Controller class and gives us room to add the View Model and action method parameter classes to the same file:

public class CoffeeListController : Controller
{
    private readonly IPageRetriever retriever

    public CoffeeListController(
        IPageRetriever retriever) =>
        this.retriever = retriever;

    public async Task<ActionResult> Index(
        [FromQuery] CoffeeFilterParams filter,
        CancellationToken token)
    {
        var pages = await retriever.RetrieveAsync<CoffeePage>(
            q => q.OrderBy(filter.OrderBy).TopN(filter.Top),
            cancellationToken: token);

        var vm = new CoffeeListViewModel(pages);

        return View(vm);
    }
}

public record CoffeeFilterParams(int? Top, string? OrderBy);

public class CoffeeListViewModel
{
    public IEnumerable<string> Coffees { get; }

    public CoffeeListViewModel(IEnumerable<CoffeePage> pages) =>
        Pages = pages.Select(p => p.Fields.Name).ToArray();
}
Enter fullscreen mode Exit fullscreen mode

This would match the pattern that the ApiEndpoints library establishes 🧐.

View Components + View Models (Widgets / Sections)

Since View Components are a slimmed down design of the traditional MVC pattern, it also makes sense to combine the View Component and its View Model classes into a single file.

This approach is just as appropriate with Kentico Xperience Page Builder components, like Sections and Widgets, since these are made from View Components 🤓:

public class FeaturedCoffeeWidget : ViewComponent
{
    public const string IDENTIFIER = "sandbox.widget.featured-coffee";

    private readonly IPageRetriever retriever;

    public FeaturedCoffeeWidget(IPageRetreiver retriever) =>
        this.retriever = retriever;

    public async Task<IViewComponentResult> InvokeAsync(
        ComponentViewModel<FeaturedCoffeeWidgetProperties> vm)
    {
        var nodeGuid = vm.Properties
            .Coffees
            .Select(c => c.NodeGuid)
            .FirstOrDefault();

        var pages = await retriever.RetrieveAsync<CoffeePage>(
            q => q.WhereEquals("NodeGUID", nodeGuid),
            HttpContext.RequestAborted);

        return pages.Any()
            ? View(new FeaturedCoffeeViewModel(pages.Single()))
            : View("_Error", $"Coffee [{nodeGuid}]");
    }
}

public class FeaturedCoffeeWidgetProperties : IWidgetProperties
{
    [EditingComponent(
        PageSelector.IDENTIFIER, Label = "Featured Coffee")]
    public IEnumerable<PageSelectorItem> Coffees { get; set; } = 
        Array.Empty<PageSelectorItem>();
}

public class FeaturedCoffeeViewModel
{
    public string CoffeeName { get; }

    public FeaturedCoffeeViewModel(CoffeePage coffee) =>
        CoffeeName = coffee.Fields.Name;
}
Enter fullscreen mode Exit fullscreen mode

Page Template Types

Another set of types I like to combine into a single file are those related to Page Templates. Using the Page Template Utilities NuGet package 👨🏾‍🏫, we typically create a Page Template Filter, Page Template Properties, and a Page Template registration attribute all in the same file for a given template.

We even combine multiple sets of these if the templates are used for the same Page Type:

[assembly: RegisterPageTemplate(
    "Sandbox.CoffeePage_Default", "Coffee Page (Default)", 
    typeof(CoffeePageTemplateProperties))]

[assembly: RegisterPageTemplate(
    "Sandbox.CoffeePage_Featured", "Coffee Page (Featured)", 
    typeof(CoffeePageTemplateProperties))]

[assembly: RegisterPageTemplate(
    "Sandbox.CoffeePage_Sale", "Coffee Page (Sale)", 
    typeof(CoffeePageTemplateProperties))]

namespace Sandbox.Delivery.Web.Features.Coffee
{
    public class CoffeePageTemplateFilter : 
        PageTypePageTemplateFilter
    {
        public override string PageTypeClassName => 
            CoffeePage.CLASS_NAME;
    }

    public class CoffeePageTemplateProperties : 
        IPageTemplateProperties
    {
        [EditingComponent(
            TextBoxComponent.IDENTIFIER,
            Label = "CTA Color")]
        public string Color { get; set; } = "";
    }
}
Enter fullscreen mode Exit fullscreen mode

Mediatr, CQRS Query/Data Types

Instead of trying to make a large Repository class we could adopt an architectural pattern that is better at isolating each data access operation into its own class.

This could be accomplished using a library like Mediatr or something with more semantic meaning for database querying, like the Xperience CQRS library I've been working on.

Both of these libraries follow the idea of implementing a common interface with 1 method and using C# generics to determine which implementation handles a given request or query. The request/query and response/data types are so closely bound to each other that it always seemed intuitive to me to define the in the same file 🤗:

public record HomePageQuery : IQuery<HomePageQueryData>;

public record HomePageQueryData(
    string Title, 
    Maybe<string> BodyHTML, 
    Maybe<HomePageImageData> Image);

public record HomePageImageData(
    Guid ImageGuid, 
    string ImagePath);
Enter fullscreen mode Exit fullscreen mode

Interfaces with Single Implementations

Often, we might start out with interfaces that have a single default implementation, and the interface exists primarily to make pieces of our code more unit testable.

I see a lot of developers go and separate these types into different files (and even into different namespaces 😝!)

Any time you find yourself making a project folder named Interfaces, pause and ask yourself why organizing code by whether its not an interface has meaning to the business goals of your app 😉.

If these types need to be testable but also only have a single implementation, it makes sense to me to place them in the same file, and only separate them in the future if we end up with multiple implementations:

public interface IClock
{
    DateTime Now();
}

public class SystemClock : IClock
{
    public DateTime Now() => DateTime.Now;
}
Enter fullscreen mode Exit fullscreen mode

There's really no need to create separate files for these 4 line types because it doesn't benefit us organizationally or help reduce complexity or improve maintainability.

Conclusion

As we've seen, the coding convention in C#, of creating a separate file for each type, has good intentions and generally good results. We've also seen that abiding by the letter of the law 🏛 isn't always as beneficial as following the spirit of the law.

Kentico Xperience is flexible enough in its ASP.NET Core MVC implementation to support a number of different architectural patterns and many of these patterns could benefit from combining multiple types in a single file.

Even if the 1-type-per-file C# coding convention is what you are most comfortable with, I recommend taking a step back from your code and considering 'could things be different?' or 'what could make my development experience better?' 🤔.

Conventions are good and so is questioning them 😁!

As always, thanks for reading 🙏!

References


Photo by Jordan Madrid on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

Or my Kentico Xperience blog series, like:

Discussion (4)

Collapse
themarkschmidt profile image
themarkschmidt

Good article, interesting ideas. It would be good to see how you name these files and where they live in the folder structure.

Collapse
seangwright profile image
Sean G. Wright Author

Thanks for reading Mark!

I typically name the file after the 'primary' type.

If I do include multiple types in a single file, it's because they are all directly related.

Example:

public class ProductDetailsViewModel
{
    public ProductViewModel Product { get; set; }
    public IEnumerable<RelatedProductViewModel> RelatedProducts { get; set; }
    public IEnumerable<ProductPromotionViewModel> Promotions { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

I could put all 4 of the types referenced above in separate files, but they are all directly related (referenced in the ProductDetailsViewModel public type definition), so instead I would create a file ProductDetailsViewModel.cs and define all those related classes there.

If it then turns out that the ProductPromotionViewModel is used in another View, I might extract that out to a higher namespace/folder since it would no longer be completely 'owned' by ProductDetailsViewModel.

On the other hand, if all these types were only used by the ProductDetailsController, I would be inclined to move all their definitions into the ProductDetailsController.cs file.

As an example for file organization in folders/namespaces, if I have an ImageViewModel that I use across all features / pages on a site, I'll store that in:

src\delivery\APPCODE.Delivery.Web\Features\Images\ImageViewModel.cs

and that might be the only class in that file.

So, I'm typically combining multiple types in a single file when there is a strong relationship between them and one of the types 'owns' the others.

Collapse
themarkschmidt profile image
themarkschmidt

Thanks for the detailed reply @seangwright . It "feels" like it breaks so many rules that are in my head (one file, one thing). But it does keep things simpler and still follows (one file, one "concept"). They are all heavily related objects, and typically if one thing changes, then a few of them may change. Which... still follows the rule. It does feel cleaner (not 10 files all over the place, when you make "one change" and have to touch all of them). And, at the end of the day, as soon as something else is using it, you can easily refactor/extract. I'll have to give it a try. Thanks agian.

Thread Thread
seangwright profile image
Sean G. Wright Author

I think you made a perfect TLDR of my post!