Having worked on many software projects written in C#, I've come to prefer certain conventions when creating the architecture of a C# project. In this highly opinionated blog post, I will share these personal preferences, which I hope may be of some value to the reader.
If possible, I prefer not to hunt down projects that ultimately make up the product. If a product is a collection of projects, then I think this should be bundled as a Visual Studio solution.
With the projects contained in a solution, you can then reference another project in your solution and then build the solution to then build your projects in order of dependencies.
If your product relies heavily on a database, I prefer that the database is built from a project contained in the product solution.
Visual Studio supports schema comparisons to import and deploy database schemas designed in the database project.
The motivation behind this mentality is now you have version control of your data objects. In particular, database procedures and functions since they are now maintained in your database projects.
I prefer to pair a project with its corresponding test project, which contains the test fixtures for the paired project.
In addition, the database project mentioned above can be paired with a test project that agnostically connects to a test database that deploys the database project's schema and run the test project's test fixtures.
For a detailed explanation, refer to my blog post about unit-testing database projects.
With the projects contained in a single solution, I prefer to maintain deployable build artifacts in the solution itself.
This consolidation can be achieved by modifying each project's build configuration to move (copy) its build artifacts in a common location relative to the solution path.
The motivation behind this mentality is to allow your solution to be easily deployable so that it can be automated via continuous integration. While your CI build can be configured to hunt down multiple projects and build your product, from my first point above, if everything is consolidated, then setting up your CI build configuration should be trivial and it would take the load off of your build engineers so they can focus on promoting builds in targeted environment rather than figuring out how to build your product.
Interfaces are always preferred.
If interfaces start to bloat your projects, then the next best thing would be to add virtual keywords in your class members that provide your logic.
This mentality keeps your projects easily testable since interfaces can easily be mocked and virtual members can be overridden by a test class.
I prefer to keep logic outside of the constructor. Even better is to include a default constructor and use public properties. The class will then need to throw exceptions if operations are called during an invalid state.
Like the interfaces mentality, by maintaining simple constructors, your projects will be easily testable since when calling concrete constructors, you don't risk your tests accessing external resources.