This article aims to show you how I organise a .NET solution. It walks through the creation of a simple web app called Fragment, and the implementation of a single feature. This app allows you to store and tag short fragments of text - ideal for use as a simple journal or notebook. Source code for the whole solution can be found on GitHub.
Assumptions
I'll be creating this solution in Visual Studio Code, but you can follow along with whatever IDE or editor you are most comfortable with. I'm using Windows, but the same should be true for Mac or Linux (bar a couple of minor points). You should be familiar with how to create a new solution, create and add projects to it, and the use of NuGet for adding packages. I will not be explaining how to do these things in this article. Ready? Let's go.
Domain
Let us start by creating an empty solution called Fragment
for our code, and adding a class lib project called Domain
to it. With this in place, we can create our classes for the domain model. We want to be able to store fragments of tagged text, so two classes are needed - TextFragment
and Tag
. Here are the classes:
namespace Fragment.Domain;
public class TextFragment
{
private string _text;
public string Text
{
get { return _text; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Text must be supplied.");
}
_text = value;
}
}
public DateTimeOffset CreatedOn { get; protected set; }
public ICollection<Tag> Tags { get; protected set; }
protected TextFragment()
{
Tags = new List<Tag>();
CreatedOn = DateTimeOffset.UtcNow;
}
public TextFragment(string text) : this()
{
Text = text;
}
}
namespace Fragment.Domain;
public class Tag
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Name must be supplied.");
}
_name = value;
}
}
public Tag(string name)
{
Name = name;
}
}
We will revisit these as we work on the project. From here we can either work our way up to the web UI, or down to the persistence layer. I usually choose the latter, and get the persistence infrastructure sorted out first. With that in mind, let us introduce some interfaces to our domain model that will handle moving objects in and out of our backing store.
Add a folder called Repositories
to your domain project. Add an empty code file called ITagRepository.cs
and another called ITextFragmentRepository.cs
. These will define the operations against our as yet unknown persistence layer. For our application, I want to be able to view all the tags we have, add new ones, and delete existing ones. Therefore I'm going to need three methods on my interface to support these operations. These should all be async methods that return a Task
, as whatever backing store we end up using will likely be using disk I/O.
namespace Fragment.Domain.Repositories;
public interface ITagRepository
{
Task<List<Tag>> GetAllAsync(CancellationToken ct);
Task AddAsync(Tag tag, CancellationToken ct);
Task DeleteAsync(int id, CancellationToken ct);
}
For the text fragments, I'll want to be able to add and remove a fragment. Additionally I'll want to see all fragments, but that might be a rather large list. To this end, I'll page the data and return a subset of the full list of fragments on each call.
namespace Fragment.Domain.Repositories;
public interface ITextFragmentRepository
{
Task<TextFragment?> GetByIdAsync(int id, CancellationToken ct);
Task<List<TextFragment>> GetPageAsync(int skip, int take, CancellationToken ct);
Task AddAsync(TextFragment fragment, CancellationToken ct);
Task DeleteAsync(int id, CancellationToken ct);
}
That is the repository interfaces defined. We need one more interface, a unit of work. This is an instance that tracks changes in the domain objects we are using. It then coordinates saving changes to the underlying backing store. As we will see later on, we can get this behaviour for free if we use Entity Framework Core (EF Core) and an underlying database. For now, we just need to define the interface for this object as exposing one method.
namespace Fragment.Domain.Repositories;
public interface IUnitOfWork
{
Task CommitChangesAsync(CancellationToken ct);
}
There, we are now finished with the domain side of persistence. Next we shall implement these interfaces using SQLite as the underlying database.
Infrastructure.Sql
Add a new class lib project to your solution called Infrastructure.Sql
. This will hold all the details related to persisting data in and out of our backing store. We will be using the SQLite database as our backing store. This is a simple file-based database that requires very little configuration, and is therefore very easy to work with.
To interact with the database we could write our own SQL, and execute it using ADO.Net classes. Then we could map the results from the table schema to our domain classes. This is as tedious as it sounds. Various Object Relational Mapper (ORM) solutions exist to take away this drudgery. EF Core is the one we will be using. Let's reference the following NuGet packages in our Infrastructure.Sql
project:
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Relational
Microsoft.EntityFrameworkCre.Sqlite
These three NuGet packages add EF Core to our infrastructure project. The heart of EF Core is the class DbContext
. This is an abstraction over our database that we can configure in many different ways. Let's add one to our project and walk through the code in it. Add a file called FragmentDbContext.cs
to the project.
using Microsoft.EntityFrameworkCore;
using Fragment.Domain.Repositories;
using Fragment.Domain;
namespace Fragment.Infrastructure.Sql;
public class FragmentDbContext : DbContext, IUnitOfWork
{
public FragmentDbContext(DbContextOptions<FragmentDbContext> options) : base(options)
{
}
public DbSet<Tag> Tags { get; set; }
public DbSet<TextFragment> TextFragments { get; set; }
public async Task CommitChangesAsync(CancellationToken ct)
{
_ = await SaveChangesAsync(ct);
}
}
You will need to reference the Domain
project we created earlier in the Infrastructure.Sql
project to resolve the IUnitOfWork
interface and domain classes.
So what is this code doing? We define a constructor that we will use later on (when we build the web UI project and start to wire up all our dependencies). The DbSet<T>
properties define what domain models we are working with. Each set will be mapped to a table in our database (as we shall see later in this section when we create migrations).
The only method we have so far is an implementation of our IUnitOfWork
interface. This delegates the commit method call to the DbContext
save changes call. Why do this? Couldn't we just use the DBContext
directly in our calling code instead of going via an interface? We could, but then our code higher up in the application is tightly coupled with EF Core. This may or may not be a bad thing depending on your project's size, scope, and expected lifetime. I tend to err on the side of caution, and add a layer of indirection with the unit of work and repositories.
We now have almost enough to create the database via an EF Core feature known as migrations. First, we need a way to supply a connection string to our database. Where should this live? Configuration files typically reside with the top-most layer of your solution - in our case the Web UI. We haven't created this project yet, so let's do that next before we return to finish off our work in the infrastructure layer.
Web UI configuration file
Add a new ASP.Net Core Razor Pages project called WebUI
to the solution. This will most likely scaffold a number of pages and other files that we will change as we work with our solution. For now, we are interested in the appsettings.json
file. Let us add content to make it look like the following:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"Fragment": "Data Source=C:\\Databases\\Fragment.db"
}
}
The connection string we added has the key Fragment
. It points to a file on disk called C:\Databases\Fragment.db
. Feel free to point to another file on your machine. (Also note that this filename is one for a Windows machine. If you are running on Linux or MacOS you should use your normal filename format.)
Add the NuGet package Microsoft.EntityFrameworkCore
to the WebUI
project. In Program.cs
add the following using statement:
using Microsoft.EntityFrameworkCore;
Next reference the Infrastructure.Sql
project in the WebUI
project. Add the following service registration code in Program.cs
file:
builder.Services.AddDbContext<FragmentDbContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("Fragment");
options.UseSqlite(connectionString);
});
Next we need to add a tool to our command line that allows us to create migrations (amongst other things) in EF Core. Run the following command from your shell:
dotnet tool install --global dotnet-ef
You should see a confirmation of the tool having been installed. Now we are ready to head back to our infrastructure project and create the database migrations.
Database migrations
Add the following NuGet package to your Infrastructure.Sql
and WebUI
projects:
Microsoft.EntityFrameworkCore.Design
Before we add our first migration, we need to amend the domain classes to contain some id fields. In the TextFragment.cs
file add the following property:
public int Id { get; protected set; }
In the Tag.cs
file add the following property:
public int Id { get; protected set; }
Next up, we need to configure how we want these domain classes mapped to tables in our database. We do this in code using a fluent interface. Create a new folder called Configurations
in the Infrastructure.Sql
project. Add a couple of empty files called TagConfiguration.cs
and TextFragmentConfiguration.cs
. Apply the code below to these two files:
using Fragment.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Fragment.Infrastructure.Sql.Configurations;
public class TagConfiguration : IEntityTypeConfiguration<Tag>
{
public void Configure(EntityTypeBuilder<Tag> builder)
{
builder.Property(t => t.Name).IsRequired();
builder.HasIndex(t => t.Name).IsUnique();
}
}
using Fragment.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Fragment.Infrastructure.Sql.Configurations;
public class TextFragmentConfiguration : IEntityTypeConfiguration<TextFragment>
{
public void Configure(EntityTypeBuilder<TextFragment> builder)
{
builder.Property(f => f.Text).IsRequired().HasMaxLength(-1);
builder.HasMany(f => f.Tags).WithMany().UsingEntity("TextFragmentTags");
}
}
Now back in the FragmentDbContext.cs
file, we need to pickup these configuration classes.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
}
This snippet of code tells the model builder to scan for any configuration classes in the current assembly and apply them to the model.
We are now ready to add our migration to scaffold the database. In your command shell, run the following:
dotnet ef migrations add --project Infrastructure.Sql/Infrastructure.Sql.csproj --startup-project WebUI/WebUI.csproj InitialMigration
This should add a new folder to the infrastructure project called Migrations
. The classes in here are responsible for updating our database for us. To apply these to our database, use the following command:
dotnet ef database update --startup-project WebUI/WebUI.csproj
We now have a nice new database that can persist our domain model classes to it via EF Core. To browse the database you can open the file from disk using DB Browser. This lets you see the data (of which there is none yet) in the tables, as well as the sql used to create the database schema.
Almost done. We need to implement the repository interfaces and make them use our db context to perform the operations we defined earlier. I'll show the implementation of ITextFragmentRepository
here, the code for the tag repository follows a similar pattern. Create a folder called Repositories
in the Infrastructure.Sql
project for the files TextFragmentRepository.cs
and TagRepository.cs
to live in.
using Fragment.Domain;
using Fragment.Domain.Repositories;
using Microsoft.EntityFrameworkCore;
namespace Fragment.Infrastructure.Sql.Repositories;
public class TextFragmentRepository : ITextFragmentRepository
{
private readonly FragmentDbContext _dbContext;
public TextFragmentRepository(FragmentDbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}
public async Task AddAsync(TextFragment fragment, CancellationToken ct)
{
if (fragment is null)
{
throw new ArgumentNullException(nameof(fragment));
}
await _dbContext.TextFragments.AddAsync(fragment, ct);
}
public async Task DeleteAsync(int id, CancellationToken ct)
{
var fragment = await _dbContext.TextFragments.SingleOrDefaultAsync(f => f.Id == id, ct);
if (fragment is not null)
{
_dbContext.TextFragments.Remove(fragment);
}
}
public async Task<TextFragment?> GetByIdAsync(int id, CancellationToken ct)
{
return await _dbContext.TextFragments.SingleOrDefaultAsync(f => f.Id == id, ct);
}
public async Task<List<TextFragment>> GetPageAsync(int skip, int take, CancellationToken ct)
{
return await _dbContext.TextFragments.Skip(skip).Take(take).ToListAsync(ct);
}
}
We've covered a lot of ground so far just to get persistence in place. We have done so in a nicely abstracted way, so the domain is not aware of the specifics of the underlying backing store in use (in this case, SQLite; but it could be anything). Next, let's move up the stack to our Web UI and start implementing some pages.
Web UI
Back in our top-most layer, the Web UI. It's important to note that this project is performing double duty as our UI and also our composition root. The composition root is the part of an application that is responsible for wiring up all the dependencies needed in an application. We did this earlier by adding our customised DbContext
to the services collection. We now need to wire up the repositories we implemented in the previous section. Open Program.cs
and add the following service registrations:
builder.Services.AddScoped<ITagRepository, TagRepository>();
builder.Services.AddScoped<ITextFragmentRepository, TextFragmentRepository>();
These are scoped to the length of an HTTP request, which is the same scope as the underlying DbContext
instance we registered earlier. With that done, let's turn our attention to some actual UI.
Our first page will be a listing of all the tags in the application. Add a new folder called Tags
under the Pages
folder. Then add a new Razor Page to this folder called List
. Let's specify the UI markup first, then turn our attention to the code-behind file.
@page
@model MyApp.Namespace.ListModel
@{
ViewData["Title"] = "List Tags";
}
<h1>Tags</h1>
@if (Model.Tags is not null)
{
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var tag in Model.Tags)
{
<tr>
<td>@tag.Name</td>
<td>
<a asp-page="/Tags/Delete" asp-route-tagId="@tag.Id">delete</a>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p>No tags found.</p>
}
Here we are creating a table to display all tag names, with a link to delete the tag (this goes to a page we haven't created yet, so clicking the link will do nothing). Let us see what the code-behind looks like to support this markup.
using Fragment.Domain;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MyApp.Namespace;
public class ListModel : PageModel
{
public List<Tag> Tags { get; set; }
public void OnGet()
{
// TODO: Load tags into list.
}
}
OK, so how are we to load the list of tags in a nicely decoupled way? We might be tempted to inject an instance of our repository class into the code-behind and use it to load our domain model instances. That poses a problem, as the UI is then tightly coupled to the domain classes. What we need is an indirect way of getting the data. Enter the Application
project.
Application
In order to insulate our UI from our domain, we need a new project. Add a new project called Application
to the solution. Notice how in the code-behind file in our UI page that we referenced the Tag
class from the domain project. We need to break this coupling. Instead, the UI will work on a Data Transfer Object (DTO) that is defined in the Application
project. Add a new folder called Dtos
to the Application
project. Then add a file called TagDto.cs
with the following content:
namespace Fragment.Application.Dtos;
public class TagDto
{
public int Id { get; set; }
public string Name { get; set; }
}
OK, so isn't this just the same as our Tag
domain class? Yes and no. Yes it currently has the same shape (public properties) as our domain class, but there is no reason why it has to. It can evolve in a different way to the domain class. The application code we will write shortly is responsible for mapping from the domain instance to a dto instance.
We need something to do the work of calling the tag repository and mapping to our dto. This will be a class known as a handler. The Web UI will pass a request - encapsulated as an object - to an instance of this handler. The handler will talk to the repository, map the result to a dto, and then return a response. We could (quite easily) write the code for all this ourselves. However there is an elegant library called MediatR that does this job already. Lets use it.
Install the following NuGet package in the Application
project:
MediatR
We are going to organise our application project in to use cases. A use case is a specific action a user can perform in your application. Create a folder called ListTags
and add a new file called ListTagsRequest.cs
. This will be where we encapsulate the request information for the handler. The code for it is as follows:
using Fragment.Application.Dtos;
using MediatR;
namespace Fragment.Application.ListTags;
public class ListTagsRequest : IRequest<List<TagDto>>
{
}
There isn't any logic or properties here. We implement the marker interface IRequest<T>
and give it our return type List<TagDto>
. Next add a file for the handler to the same folder called ViewTagsHandler.cs
. Add the code as follows:
using Fragment.Application.Dtos;
using Fragment.Domain.Repositories;
using MediatR;
namespace Fragment.Application.ListTags;
public class ListTagsHandler : IRequestHandler<ListTagsRequest, List<TagDto>>
{
private readonly ITagRepository _tagRepository;
public ListTagsHandler(ITagRepository tagRepository)
{
_tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository));
}
public async Task<List<TagDto>> Handle(ListTagsRequest request, CancellationToken cancellationToken)
{
var tags = await _tagRepository.GetAllAsync(cancellationToken);
return tags.Select(t => new TagDto { Id = t.Id, Name = t.Name}).ToList();
}
}
You'll need to reference the Domain
project in order to pick up the repository interface from earlier. We now have our application logic nicely encapsulated in our handler class, and not in the UI. Next we need to wire up the handler in the Program.cs
file in the Web.UI
project. Add the following service registration:
builder.Services.AddMediatR(c => c.RegisterServicesFromAssemblyContaining<ListTagsHandler>());
You'll need to reference the Application
project in the WebUI
project for the AddMediatR
method to be picked up. This uses assembly scanning to add all the handlers we have (or will in future) define in the Application
project. We can now turn our attention back to the page in the UI for viewing the tags.
The code-behind file for our page can now use MediatR
to pass off it's request to the handler and get a DTO in response.
using Fragment.Application.Dtos;
using Fragment.Application.ListTags;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MyApp.Namespace;
public class ListModel : PageModel
{
private readonly IMediator _mediator;
public ListModel(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
public List<TagDto> Tags { get; set; }
public async Task<IActionResult> OnGetAsync()
{
var request = new ListTagsRequest();
var response = await _mediator.Send(request);
Tags = response;
return Page();
}
}
Run the WebUI
project and browse to the page /Tags/List
. We should see an empty table. If you debug the OnGetAsync()
call you can trace the execution of the request through the application layer, into the infrastructure and back again. Our UI has no knowledge of the application logic or the SQL infrastructure.
Architecture
There are four projects in our solution now. It is worth observing the dependencies between them in a little more details. Take a look at the source code in the finished repository. First up is the Domain
. This houses our entities with their associated domain logic, and the interfaces for our repositories. This project depends on none of the others. It could be reused as-is in another project.
Next is Infrastructure.Sql
. This encapsulates all of the EF Core dependencies and database specific code. It's only dependency is on the Domain
project for the repository interfaces. We could very easily add a new infrastructure project that uses a different type of backing store, and the effects would not be felt by the rest of the application.
In Application
we encapsulate (via the use of MediatR
) the use-case specific logic in our solution. The handlers are just entry points into the application layer. You can introduce other objects in the layer to perform other tasks, and the handlers will use them to coordinate processing the request and providing a response.
Finally the WebUI
project is our user interface. The use of MediatR
keeps it loosely coupled with the Application
project. You could introduce a new UI project - say a command line or desktop app - and have it re-use the rest of the solution without having to change anything else. Note that this project does contain references to the other projects, but this is because it is also acting as the composition root of our application. This is the wiring up of dependencies in the Program.cs
file. This should be the only place in our WebUI
project that we use those references.
I hope this walkthrough and the finished code has helped you understand how I build a loosely coupled solution in .NET.
Top comments (0)