I've discussed several of my favorite features of the C# programming language in the previous few posts, but a question that I have heard on my Twitch stream and on Twitter is: "how should we best structure our projects to take advantage of these awesome features?"
Oh the architecture questions. There are many ways to structure your projects and solutions, and that volume of choices is part of what makes C# and .NET in general so amazing: we get to be artists for a bit and design our source code structure in a way to makes us happy.
In this post, I'll share some of the ways that I like to structure my projects and solutions as they evolve. This is NOT a 'best practices' or required set of techniques for anyone to build an application system. This is the way that I build systems that makes me happy, and maybe you'll find some of these techniques valuable in your systems as well.
The C# language documentation has some great recommendations for standard conventions around naming, layout, and commenting. However, they don't answer the question: "how should I structure the content of my C# code file?"
There is a rule that StyleCop uses that will encourage you to order the contents of your C# files (*.cs) as follows:
- Extern Alias Directives
- Using Directives
Within a class, struct, or interface the elements should be position in this order:
- Finalizers (Destructors)
- Nested Classes or Records
You can add the StyleCop Analyzers to your projects by simply adding a reference to the NuGet package "StyleCop.Analyzers". On every build after installing these analyzers, you will get a report of the recommended changes to your application.
As I start building a new system, the projects that comprise that system are typically all managed within the same solution file. In rare cases, multiple solution files can be used to manage various logical segments of the system.
As a web developer first, I like to typically start a new system by creating my initial application and name it the same as my Solution:
- FritzsCoolApplication.sln - FritzsCoolApplication
I've learned over the many applications that I've created that this application is going to grow, shift, and change. I've started postfixing my initial web application names with
.Web as they represent the web application that will be delivered.
Additional projects that are added to the solution typically have the same base-name and have a suffix to indicate their purpose. It's common to see projects in my solutions with names like:
FritzsCoolApplication.Web FritzsCoolApplication.Api FritzsCoolApplication.BackOffice FritzsCoolApplication.Common FritzsCoolApplication.Core FritzsCoolApplication.Client FritzsCoolApplication.Communication FritzsCoolApplication.Proxy FritzsCoolApplication.Migrate FritzsCoolApplication.Test
Folders in some projects are defined with conventions for use in making applications like ASP.NET MVC work without adding configuration. This convention over configuration means the same folders are defined in these projects that we're going to find regardless of the direction of the project. Consider the conventional folders for an MVC project:
Views folders are where the heart of the MVC application operates. In all ASP.NET Core applications, the
wwwroot folder is where static files are kept that are served without server interpretation. The default
img folder and a
fonts folder as needed for images and fonts and you have a great configuration for your web project.
In a Razor Pages project, we see the replacement of the
Views folders with a single
Data interactions in the project typically start with a
Data folder and a potential
Migrations folder that is maintained by Entity Framework.
If I'm creating a hosted service, that is something that will be running as a background process inside my application, I will create a
Services folder in the project to contain these application specific processes.
ViewModels are another concern, as we begin to constrain the contents presented and interacting with our web application. These contents could be stored in a
ViewModels folder with appropriate conventions defined for working with user data. This is also a great convention to use for interacting with our Web APIs that we may define in a project, especially if we want to introduce a front-end API framework like gRPC or OpenAPI.
When significant business logic enters my application, I start to introduce unit tests. By business logic, I am referring to more than the simple create, read, update, and delete operations with a database. This logic is the programming model that you are introducing to calculate and provide those features that define your application.
I prefer starting a second project next to my initial web application with a
Test suffix like
FritzsCoolApplication.Test. I'll even go a step further and configure solution folders to place the test project in a location at the bottom of the Solution Explorer in Visual Studio, making it easier for me to find the test project.
As the complexity of our application grows, we want to introduce interfaces and abstractions to provide for re-use. This is when I begin to introduce new projects that can be shared so that each project is a smaller and more manageable unit.
The first project I typically introduce is a Class Library built with .NET Standard 2.0 named
.Common and contains a collection of shared Enums, Constants, and Interfaces that I might be using across projects. This project typically does not contain any executable code, but rather contains the definitions of cross-cutting features in use throughout the application system.
Why make this project .NET Standard 2.0? These features that I am defining are simple and I want maximum compatibility across all future projects. .NET Standard 2.0 provides all of the flexibility that I need if I want to target Windows, Azure, Xamarin, .NET Core and more.
As the number of resources in each folder of my initial web project grow and additional executables or services need to be hosted, I begin to separate each of those folders into their own projects that new executable or service projects can share.
As new services for business domains are defined and added, I introduce a third segment to my project naming scheme that defines that domain. The below image shows how I would introduce a pair of services that manage the Accounting and Inventory domains for my solution.
The flexibility with which you can grow your application from a simple website or desktop application into an entire enterprise scale service is only limited by your imagination. A few rules of thumb and guideposts along the way as you grow your application will help prevent project-sprawl with hundreds of little projects that don't add significant re-use or value to your solution, but instead create maintenance headaches.
Don't be afraid to refactor mercilessly. Content that starts to be re-used should move into shared projects and libraries that domains can re-use and end-user applications or websites can consume easily. Your unit-tests are your friend and can help guarantee that your projects continue functioning at a high-level.
How do you grow your applications? What suggestions do you have for a maturing project that needs to grow out of a single folder or a single project?
Did you know, I host a weekly live stream on the Visual Studio Twitch channel teaching the basics of C#? Tune in on Mondays at 9a ET / 1300 UTC for two hours of learning in a beginner-friendly Q+A format with demos and sample code you can download.
Looking to get started learning C#? Checkout our free on-demand courses on Microsoft Learn!