This is a little experiment I want to try, over the years what I noticed is that the general approach in dotnet is to use multiple projects each resembling a logical part of the application, or a tier in the application architecture even.
Usually there would be a data project containing the database model, repositories and other related types. A domain or business project referencing the data one and sometimes using the database model directly (this is another story) to implement the business requirements. Finally, an ASP.NET project providing either the user interface in case of multi-page applications, or the web API where a frontend would use it and provide the user interface.
I understand the need for separation, this is not the issue, the issue is dotnet project explosion inside a solution and most of the times it is on the premise on how it will be useful in the future.
For instance, we want to have a separate domain project so that if we want to reuse it in the future we will be able to easily do so. This is oversimplified, making such a project reusable means handling its own dependencies as deliverables, a mass of NuGet packages? Is this even needed? Is the entire domain to be reused? Once you think more about the implications of this you soon get to the conclusion that there isn’t enough information to address this properly. You get left with either preparing the code for something that may not actually happen, over-complicating it in the process, or not really addressing this as it is not worthwhile.
In both cases you are left with a separate project for the sole purpose of separating concerns.
Another example would be to have a separate project because the code there is reusable. A fine keyword and a great thing to have, but this enforces the project explosion as where would your code be reusable in the first place if not in your own application, between multiple projects? I have seen applications where different areas can be extracted into libraries as they address general issues, or non-specific domain problems. But you can do this without really having a project initially and move all of this code inside a folder and later on extracting it as a separate project or even as a NuGet package.
Here as well, the main idea is the separation of concerns, we don’t want to muddy our code base with interdependencies or place everything in one namespace and have a really hard time finding anything.
Single Project Approach
What I want to challenge is the organizational part of a dotnet solution. Instead of starting with the premise that we use multiple projects because of things that might happen in the future, to start with what we need right now, which in most cases is a single project, especially when starting an application, and then add more if they are really needed.
The separation of concerns, mapping different namespaces/folders to layers of the application is absolutely necessary, that does not go away. The way we do it can be different.
This will most likely apply to small or maybe even medium applications, I don’t see large ones being able to have just one project as they most likely consist of actually multiple smaller applications working together to provide the full solution. There are a lot more moving parts and each may even have its own deployment strategy which naturally leads to multiple projects.
This is what I mean by adding projects when they are actually needed rather than bloat the codebase with them and make it more difficult to maintain.
This is an older application I keep revisiting, and it is quite popular with getting people started, it is the overly used example of tracking expenses. I like this example here as well because everyone knows about it. We will call it the “WayCash app”.
On the backend side I want to use GraphQL as it is one of the most popular approaches to Web API these days, it is easy to configure and it requires only one endpoint for executing the requests.
Most applications eventually end up requiring background processing for things like generating reports, gathering data from other APIs, database/log cleanup or sending emails.
Having this in mind, I will be using a single Azure Functions app with an HTTP trigger to handle the GraphQL requests and later on more functions can be added to handle the background processing. This is very convenient for having a single project approach as well.
For setting up the application, check Quickstart: Create a C# function in Azure using Visual Studio Code, I will be using VS Code as it is free and much more lightweight than the standard Visual Studio.
The Graph
For this example, we will be retrieving a list of items where each has a question and an answer. This is completely unrelated to the business domain, but it is easy to test and get started with.
I will be using a record to define the item.
public record Item(string Question, string Answer);
Next up is the GraphQL definition, for setting it up for our project just add the GraphQL and GraphQL.SystemTextJson NuGet packages. For more information about the installation check out GraphQL.NET / Installation.
dotnet add package GraphQL dotnet add package GraphQL.SystemTextJson
class ItemListGraphType : ObjectGraphType { public ItemListGraphType() { Field<ListGraphType<ItemGraphType>, IEnumerable<Item>>("items") .Resolve(context => context.As<IEnumerable<Item>>().Source); } }
class ItemGraphType : ObjectGraphType<Item> { public ItemGraphType() { Field(x => x.Answer); Field(x => x.Question); } }
To make this available we need a schema as well.
class WayCashAppSchema : Schema { public WayCashAppSchema() { Query = new ItemListGraphType(); } }
The graph definition uses a list of items as its context, later on this can be changed to an Entity Framework DB set, or the database context itself. Do take into consideration dependency container scopes when executing queries, usually each should run in its own scope and have individual queries resolve dependencies on query execution rather than query definition.
Dependencies & Execution
For executing GraphQL queries, I will be using an HTTP triggered function, I will use both get and post methods, the former for setting up the playground tool making it easier to explore and test the graph.
The sample project already had a function definition with the trigger I needed, however for more information on how to set one up from scratch check Azure Functions HTTP trigger.
public class GraphQL(ISchema schema, IServiceProvider serviceProvider) { [Function(nameof(GraphQL))] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "graph")] HttpRequest request ) { switch (request.Method.ToLowerInvariant()) { case "get": throw new Error("Will do this later, I promise!"); case "post": var graphQlRequest = await request .ReadFromJsonAsync<GraphQLRequest>(); var graphQlResponse = await ExecuteQueryAsync(graphQlRequest); await request .HttpContext .Response .WriteAsync(graphQlResponse, Encoding.UTF8); break; default: request.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; break; } } internal Task<string> ExecuteQueryAsync(GraphQLRequest? request) { using var serviceProviderScope = serviceProvider.CreateScope(); return schema.ExecuteAsync( new GraphQLSerializer(indent: false), context => { context.RequestServices = serviceProviderScope.ServiceProvider; context.Query = request?.Query; context.Root = new Item[] { new( "What's my name?", "Francis.." ), new( "Where was Gondor when the Westfold fell?", "..." ), new( "What did one wall say to the other?", "Meet you at the corner :)" ) }; } ); } }
There is one missing part here, which is the dependency configuration for the ISchema, for this the app configuration needs to be updated, any other dependency can be added here. I opted for singleton configuration as the schema instance itself represents the graph definition and this does not change while the application is running.
To resolve a dependency at execution time, the resolve method on a field should be used where the context can be accessed and from there the scoped service provider can be used as well. When executing a query, a scope will be automatically created each time ensuring that any transient or scoped dependencies are not kept once the result has been obtained.
var builder = FunctionsApplication.CreateBuilder(args); builder .ConfigureFunctionsWebApplication(); // Configure dependencies builder .Services .AddSingleton<ISchema, WayCashAppSchema>(); builder .Build() .Run();
To test this, run the functions app and using PowerShell call the WebAPI.
Invoke-RestMethod ` -Method 'POST' ` -Uri 'http://localhost:7071/api/graph' ` -ContentType 'application/json' ` -Body '{ "query": "{ items { question answer } }" }' ` | Select-Object -ExpandProperty data ` | Select-Object -ExpandProperty items
Unit Tests
This is a common practice in dotnet, having a separate project containing the unit tests and usually these are linked and resemble somewhat the same structure. For instance, having a WayCashApp.Domain project would result in a WayCashApp.Domain.Tests project, similar to WayCashApp.Data having a WayCashApp.Data.Tests project.
In front-end development, the unit test code can live next to the file or module it is testing, these files are not added to the bundle that gets published offering a nice way of having the entire code related to a module in one place, the actual implementation and the tests. This makes it easy to maintain as there is no need to search in a different project with a similar structure for the tests. I want this as well.
Following the same approach, unit test, or just tests in general, will use the .Tests.cs extension to separate from application code. This naming convention can be changed to include folders named Tests allowing for more flexibility. The plan is to have a convention in which tests would be included with the debug configuration but not with the release one. I still don’t want test code to end up in production where it would only sit there doing nothing.
First, I’ll write a test using the file name convention, GraphQL.Tests.cs.
public class GraphQLTests { [Fact] public async Task TestQuery() { var graphQl = new GraphQL( new WayCashAppSchema(), new DefaultServiceProviderFactory() .CreateBuilder(new ServiceCollection()) .BuildServiceProvider() ); var graphQlResponse = await graphQl .ExecuteQueryAsync(new GraphQLRequest { Query = "{ items { question answer } }" } ); var jsonResult = JsonSerializer.Deserialize<JsonObject>(graphQlResponse); Assert.Equal( ( from item in jsonResult!["data"]!.AsObject()["items"]!.AsArray() select new Item( item.AsObject()["question"]!.GetValue<string>(), item.AsObject()["answer"]!.GetValue<string>() ) ), [ new( "What's my name?", "Francis.." ), new( "Where was Gondor when the Westfold fell?", "..." ), new( "What did one wall say to the other?", "Meet you at the corner :)" ) ] ); } }
We can run tests both from the command line and using the C# DevKit Extension, if you haven’t already, check it out. They have made some really great improvements and facilitated developing C# applications in Visual Studio Code, I hardly open up the standard Visual Studio anymore.
This time I used xUnit, but any testing platform works, the following packages have been added to be able to run the tests.
dotnet add package Microsoft.NET.Test.Sdk dotnet add package xunit dotnet add package xunit.runner.visualstudio
CSProj Changes
First step is excluding the tests from the compilation, to check whether a type is part of an assembly I will be using PowerShell as I have access to the entire dotnet infrastructure. I will be loading an assembly from a given location and then trying to retrieve a specific type.
First, I will build the project for debug and release. Keep in mind that after loading the assembly in a PowerShell instance it will remain loaded until that instance (terminal) is closed, this means that for successive builds the terminal needs to be closed otherwise dotnet will let you know that the file is in use and will not compile.
dotnet build dotnet build --configuration Release
Next, I will load and test the presence of the test class as mentioned above.
[System.Reflection.Assembly]::LoadFile( (Resolve-Path ".\bin\Release\net8.0\SingleProject.dll") ).GetType("WayCashApp.Functions.GraphQLTests")
This command should return the type definition if it is found, otherwise nothing will be displayed. As the project is configured right now, a result is visible.
Test files should be removed conditionally, this can be done in the .csproj file for files matching a glob pattern or even for specific files, see MSBuild conditions for more information.
<ItemGroup> <Compile Condition="'$(Configuration)'=='Release'" Remove="**/*.Tests.cs" /> </ItemGroup>
With the above in place, close the terminal and launch a new instance. After building the application for both debug and release, running the check will yield no result for the release build. The file containing the unit test was not included in the compilation meaning that whenever we build for release, we will not be bundling the test code alongside application code.
In a similar fashion, some NuGet dependencies can be removed, specifically the ones used for unit testing the application. There is no need to copy these dependencies to the build output folder as they will be included in the deployment package, but not really used.
<ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="GraphQL" Version="8.2.1" /> <PackageReference Include="GraphQL.Server.All" Version="8.2.0" /> <PackageReference Include="GraphQL.SystemTextJson" Version="8.2.1" /> ... other dependencies </ItemGroup> <!-- Add this condition here Any other non-release configuration can be added here as well. --> <ItemGroup Condition="'$(Configuration)'!='Release'"> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="xunit" Version="2.9.2" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.0"> <IncludeAssets> runtime; build; native; contentfiles; analyzers; buildtransitive </IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup>
With this, we have a single project for the entire application which contains everything, including unit tests. Only relevant dependencies are copied over to the output directory.
The bonus from this approach is that we can have unit tests for internal members, there is no need to make them visible to the testing project anymore.
Adding a GraphQL UI
This is the last part I want to cover in this article, for the get method where GraphQL queries are posted I want to retrieve a user interface where the graph can be explored and queries can be run.
For this, I will be using GraphiQL, for more information check GraphQL.NET / GraphiQL. Since the usual setup does not work for me, I will be adapting the middleware in the function itself and provide all the necessary configuration to generate the UI.
switch (request.Method.ToLowerInvariant()) { case "get": // throw new Error("Will do this later, I promise!"); var graphiQLMiddleware = new GraphiQLMiddleware( context => Task.CompletedTask, new GraphiQLOptions { GraphQLEndPoint = "/api/graph", SubscriptionsEndPoint = "/api/graph", GraphQLWsSubscriptions = false } ); await graphiQLMiddleware.Invoke(request.HttpContext); break;
With this in place, when accessing /api/graph through the browser, we will be getting a nice interface where we can compose and execute queries. The cool part about it is that we use the exact same route to post queries.
Conclusions
Although I didn’t talk about managing expenses, I got to talk about configuring a single project for running a dotnet application which includes a GraphQL API, the ability to add background jobs as well as include unit test next to the code that they test.
This is part of a pet project I want to start, but have been postponing for a while. I find the idea interesting as on most projects I worked on they were bloated with many C# projects and I personally don’t think we need that many, we can use more folders to structure our code.
I don’t think an entire application past the medium size would fit a single project approach, however I do think it is better to start with just one and then add more as they are really needed. On top of this, the example used is very specific as it runs completely on an Azure Function service, if there was a WebApp and a Functions App then there would be at least 2 projects, if not 3.
Hopefully you found this interesting and may decide to give it a go for one of your pet projects, I’d say it can work great for this type of thing as well as small applications.