How I Split Projects in Razor Pages Systems

A reflection on how I split Razor Pages systems into Core, DataModel, and multiple feature projects, and why actor-based boundaries often work better for solo development.

Previous article: Unifying Data Design and Code with Entity Framework

In the previous note, I wrote about data boundaries. This time, I want to focus on project boundaries inside a Razor Pages system and how I usually split them. This is not a comparison of patterns or a claim about the single best structure. It is simply the way I currently look at project structure so the system remains understandable and maintainable while I continue operating it alone.

Solution and project boundaries are still one of the best parts of C#

In C#, there is a clear distinction between a solution and a project, so individual functions or technical responsibilities can become separate projects while the solution gathers them into one development space. Other ecosystems can build something similar, and npm workspaces are an obvious example, but when I open a .NET solution in an IDE, the classification remains especially easy to understand at a glance. Since moving back to Razor Pages development, I have relied on this separation continuously, not only because it is technically possible, but because it helps me keep the system organized in a form my own brain can continue to handle. That visual clarity matters more than people sometimes admit.

The smallest Razor Pages starting point

In a small system, the starting point is simple. When I create a Razor Pages solution in .NET, I usually begin with a solution folder and one main Razor Pages application project. At that point, the whole application is still a single project.

flowchart TD
    S["BusinessSystem.sln"]
    A["BusinessSystem.Web<br/>Razor Pages app"]
    P["Pages"]
    W["wwwroot"]
    PR["Program.cs"]
    AP["appsettings.json"]

    S --> A
    A --> P
    A --> W
    A --> PR
    A --> AP

If I am only making a very small website, I could keep adding files there and continue without much trouble, but if the site is simple enough that it only needs static pages, I probably would not be using C# in the first place. Once the system needs to handle server-side data, rules, authentication, persistence, or shared operational behavior, I have to decide where the application foundation should live. That is why I usually add a class library project called Core very early.

Adding Core before the system becomes messy

Core is where I place the application foundation that should not be buried directly inside the main web project. The exact contents differ by system, but the role is stable. It is the place where cross-cutting application structure begins to take shape.

At a simple stage, it may look like this.

flowchart TD
    S["BusinessSystem.sln"]
    W["BusinessSystem.Web<br/>Razor Pages app"]
    C["Core<br/>Class Library"]
    CD["Data"]
    CA["Auth"]

    S --> W
    S --> C
    C --> CD
    C --> CA

I suspect many Razor Pages systems end up with a similar structure even if the naming differs. In projects I joined in the past, some teams used names such as 00.Core so the common foundation would appear near the top in the IDE. It is not beautiful naming, but from the viewpoint of visual organization, it is effective.

I also usually create a separate class library called DataModel because the data model defines the structure of the system’s target domain itself, rather than one business function among many. For that reason, I normally do not split DataModel by business area. It remains one project that expresses the overall domain structure of the system.

So even in a relatively small application, I often move fairly quickly to a three-part baseline: the main Razor Pages application, Core, and DataModel. I separate them early because the foundation and the domain structure become easier to reason about when they are visible as distinct units.

Why this mattered to me so much

When I was developing in PHP, structure became a serious problem once the system reached a certain size, and Razor Pages does not magically erase that problem. If I look only at one project, it can still become crowded and hard to understand. What helps is the relationship between the solution and its multiple projects, because it becomes visually obvious in the IDE that the system is made of several large units rather than one flat mass of files. Each project also contains its own internal folder structure, so it behaves like a semi-independent function boundary. That is extremely important to me because a project is not only a compilation unit, but also a way to keep responsibility visible.

I often use Razor Class Libraries as real feature boundaries

To take advantage of this separation, I do not stop at class libraries. I also use Razor Class Libraries when I want to package shared or independent screen functions. In a small system, putting everything into the main project is acceptable, and I have done it myself, because there is no need to split things just for the sake of appearing sophisticated. But in a larger system, one project becoming too large is a serious long-term problem. It makes continued feature addition and modification harder than it needs to be, so for me the question is not whether to split, but how to split.

The first split pattern: by business function

One way I often divide projects is by business function. I saw this kind of classification many times in larger Japanese enterprise development, including SES-style projects, so I suspect it is a fairly common pattern at least in Japan. It has obvious strengths: each domain area can accumulate its own knowledge, progress can be understood per area, responsibility sharing is easier, and cooperation between multiple people also becomes easier because the system is already grouped by business meaning.

Here is a simplified example based on a real system I built for field maintenance operations centered on cleaning work. The actual system was larger, but I am simplifying it here both for explanation and to avoid exposing the business too directly.

flowchart TD
    S["Field Maintenance System.sln"]
    Core["Core"]
    DataModel["DataModel"]
    Hygiene["Hygiene and Cleaning Management"]
    Schedule["Work Scheduling"]
    Daily["Daily Cleaning Operations"]
    Monthly["Monthly Cleaning Operations"]
    Closing["Closing Confirmation"]
    Escalation["Issue Tracking and Escalation"]
    Damage["Facility Damage Reports"]
    Patrol["Patrol Inspection Reports"]

    S --> Core
    S --> DataModel
    S --> Hygiene
    S --> Escalation
    Hygiene --> Schedule
    Hygiene --> Daily
    Hygiene --> Monthly
    Hygiene --> Closing
    Escalation --> Damage
    Escalation --> Patrol

The point is simple. At the top level of the solution, I place the foundation projects and then the major business projects organized by area. If the business later expands into adjacent operations, I can add new projects while preserving the relative independence of the existing ones. For business systems, that is a very practical advantage.

Why that model does not always fit solo development

That said, this approach does not always match my current working reality because I now build most systems alone. In team development, one of the biggest advantages of business-function splitting is that assignment becomes easier and different people can own different domains. But when I am the only developer, that benefit is much smaller. The structure is still valid, but the strongest advantage of that pattern is no longer available to me, so I often choose a different split.

The second split pattern: by actor

What I now do more often is split by actor. Here is another simplified example, this time from an order management system I developed in the past.

flowchart TD
    S["Order Management System.sln"]
    Core["Core"]
    DataModel["DataModel"]
    Sales["Sales Management"]
    Customer["Customer Information"]
    Quote["Quotation Information"]
    Order["Order Information"]
    Product["Product Management"]
    Item["Product Information"]
    Stock["Inventory Information"]
    Shipping["Shipping Management"]
    Shipment["Shipment Information"]

    S --> Core
    S --> DataModel
    S --> Sales
    S --> Product
    S --> Shipping
    Sales --> Customer
    Sales --> Quote
    Sales --> Order
    Product --> Item
    Product --> Stock
    Shipping --> Shipment

This example is also simplified, but the atmosphere should be clear enough. The system manages orders, products, inventory, and shipment flow. More importantly, each project can be understood as a cluster of use cases tied to a particular actor’s work. Seen from that angle, the split looks more like this.

flowchart LR
    SalesStaff((Sales Staff))
    OfficeStaff((Office Staff))
    DevelopmentStaff((Development Staff))
    QualityStaff((Quality Staff))
    ProductionStaff((Production Staff))
    WarehouseStaff((Warehouse Staff))

    UC1(["Maintain customer information"])
    UC2(["Prepare quotations"])
    UC3(["Register orders"])
    UC4(["Maintain product definitions"])
    UC5(["Check inventory"])
    UC6(["Prepare shipments"])
    UC7(["Confirm shipment targets"])

    subgraph SalesMgmt["Sales Management"]
        UC1
        UC2
        UC3
    end

    subgraph ProductMgmt["Product Management"]
        UC4
        UC5
    end

    subgraph ShippingMgmt["Shipping Management"]
        UC6
        UC7
    end

    SalesStaff --> UC2
    SalesStaff --> UC3
    OfficeStaff --> UC1
    OfficeStaff --> UC2
    OfficeStaff --> UC3
    DevelopmentStaff --> UC4
    DevelopmentStaff --> UC5
    QualityStaff --> UC4
    QualityStaff --> UC5
    ProductionStaff --> UC4
    ProductionStaff --> UC5
    WarehouseStaff --> UC6
    WarehouseStaff --> UC7

Sales Management covers the use cases around maintaining customer information, preparing quotations, registering orders, and reviewing the progress of deals. Product Management covers the use cases around maintaining product definitions, checking stock conditions, and coordinating the information needed by development, quality control, and production. Shipping Management covers the use cases around preparing shipments, confirming shipment targets, and completing the operational flow in the warehouse.

In reality, systems are more detailed and messier than this. Even so, the actor boundary is often clearer in day-to-day operation than a pure business-function taxonomy, because it maps more directly to the work people actually perform.

Why actor-based splitting is often easier for me

The practical advantage is authorization, but only because I design it that way from the beginning. When I split Razor Class Library projects, I follow a rule that each project owns the first path segment, so Sales Management lives under /sales/ and Shipping Management lives under /ship/. Then I build the authorization foundation on top of that rule so permissions can be granted and checked by segment. In other words, this is not a lucky side effect of project splitting. I intentionally make the segment boundary and the authorization boundary match.

That matters a lot in solo development because the rule is structural. I do not need to reconsider permission strategy screen by screen every time I add something. The project boundary already tells me where the authorization boundary should be. Data is still shared through DataModel, of course, and a function used by one actor may still need to read or reference data primarily maintained by another actor’s area. But that access can be limited through domain models and APIs rather than by collapsing all screens into one project.

I do not know what the global mainstream is for this kind of project split. What I often saw in Japanese IT work was permission control configured screen by screen or actor by actor, and many real services also allow very detailed permission settings. That flexibility has value. But for a system I need to keep building alone over time, simplicity matters more. If a simple structure is available, I would rather use it. That is why actor-based project splitting has worked well for me.

Closing

This note focused on how I split projects in C# systems. The answer is not universal. It depends on team size, operational structure, and what kind of clarity the system needs most.

For me, the stable starting point is a Razor Pages main project, a Core project, and a DataModel project. From there, I usually choose either business-function boundaries or actor boundaries, depending on what will keep the structure easiest to operate over time.

I also build these systems with Cotomy, even though Cotomy itself is TypeScript-first. But that part deserves its own explanation. In the next note, I want to write about how I integrate Cotomy into this kind of split Razor Pages structure without blurring the project boundaries I rely on.

C# Architecture Notes

This article is part of the Cotomy C# Architecture Notes, which reflect on backend and project-structure decisions around business systems.

Series articles: Why I Chose C# for Business Systems and Still Use It , From Global CSS Chaos to Scoped Isolation , Unifying Data Design and Code with Entity Framework , How I Split Projects in Razor Pages Systems, and Integrating TypeScript into Razor Pages with Cotomy .

Next article: Integrating TypeScript into Razor Pages with Cotomy

Learn Cotomy

Cotomy is a DOM-first UI runtime for long-lived business applications.