.NET Steps – Project Structure

Project structure

Creating folder structure and solution structure
  • Create a folder where you want to create your project, call it Company.Example
  • Inside that folder we are going to create a couple of folders
    • src
      • Application
      • Domain
      • Infrastructure
      • Presentation
    • tests
  • Move back to Company.Example folder and open a command prompt
  • We are going to create our solution file using dotnet tools. Run:
dotnet new sln --name Company.Example

Now, you should be able to see something like this

  • Double click on Company.Example.sln. it should open visual studio for you.
  • In your visual studio, you will notice that nothing is there. If you select “Switch between solutions and avaliable views” you will be able to see solution view and folder view. Selecting folder view will show folders you created, but solution view won’t. That is because solution files use something called Solution Folder. Now we need a way to connect our Real Folders with Solution Folders. It’s pretty simple to do.
  • Make sure you have solution view opened
  • Right click on the Solution ‘Company.Example’ and select New Solution Folder. Now you just have to mimic what you did with “real” folders. Result should look something like this
Adding projects to solution
  • Next thing we need to do is add projects. Right click on Domain Folder -> Add -> New Project.
  • In first menu we select Class Library and press next.
  • In the next menu
    • Project name – Company.Example.Domain
    • Location – we need to select our Domain folder
  • Now you need to follow those steps for Application Layer, Presentation Layer and Infrastructure Layer. Yup, you are on your own.
  • You should end up with this:
  • Done? Nice!
  • Last thing we need to do is add our startup project – yup, it starts a project. If you are new to programming – we are not that imaginative.
  • Nope, not even a bit, but in return we compensate by keeping off track
  • Alright, right click on the src folder -> Add -> New Project -> Select ASP.NET Core Empty -> Next
  • On the next menu:
    • Project Name – Company.Example
    • Location: we need to select our real src folder
  • On the next screen click Create
  • This is what you will end up with:
  • Now you need to right click on the Company.Example and select -> Set as Startup project.
  • Did you try to run it? I know, it doesn’t work.
Setting up project dependencies

What is a “project dependency”? Simply, if ProjectA depends on ProjectB, ProjectA can see whatever is marked as public in Project B. If you have a public class Car.cs in ProjectB, you will be able to create a new instance of it in ProjectA.

Let’s add those dependencies.

  • Expand Company.Example.Application
    • Right click on Dependencies
    • Select Add Project Reference
    • Select Company.Example.Application.Contracts
  • Expand Company.Example.Application.Contracts
    • Right click on Dependencies
    • Select Add Project Reference
    • Select Company.Example.Domain
  • Expand Company.Example.Infrastructure.Database.Mssql
    • Right click on Dependencies
    • Select Add Project Reference
    • Select Company.Example.Application.Contracts
  • Expand Company.Example.Presentation.Api
    • Right click on Dependencies
    • Select Add Project Reference
    • Select Company.Example.Application
  • Expand Comapny.Example
    • Right click on Dependencies
    • Select Add Project Reference
    • Select Company.Example.Presentation.Api, Company.Example.Application, Company.Example.Infrastructure.Database.Mssql

I know, this was boring for me as well.

Layers

Consider layers as boxes that help developers manage complexity of a code. Manage complexity of code is just a fancy expression for saying organize code. Yup, that’s it. If you put underware in one box, socks in the other, you’ll be fine. For those that don’t, what are you doing with your life?

Domain Layer
Company.Example.Domain

Folder structure:

  • Car.cs

LOL, I know – “folder”, “structure”.

namespace Company.Example.Domain
{
    using System;

    public class Car
    {
        public Car(
            Guid id,
            string name,
            string make,
            int weight,
            int maxSpeed)
        {
            this.Id = id;
            this.Name = name;
            this.Make = make;
            this.Weight = weight;
            this.MaxSpeed = maxSpeed;
        }

        public Guid Id { get; protected set; }

        public string Name { get; protected set; }

        public string Make { get; protected set; }

        public int Weight { get; protected set; }

        public int MaxSpeed { get; protected set; }

        public void Update(string name, string make, int weight, int maxSpeed)
        {
            this.Name = name;
            this.Make = make;
            this.Weight = weight;
            this.MaxSpeed = maxSpeed;
        }
    }
}
Application Layer
Company.Example.Application.Contracts

Defines the contracts our Company.Example.Application Layer needs in order to communicate with Company.Example.Infrastructure Layers. With these contracts we represent our intention.

Folder structure:

  • Database
    • Models
      • CarFilter.cs
    • ICarRepository.cs
    • IUnitOfWork.cs
namespace Company.Example.Application.Contracts.Database
{
    using Company.Example.Application.Contracts.Database.Models;
    using Company.Example.Domain;
    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;

    public interface ICarRepository
    {
        void Create(Car car);

        void Update(Car car);

        void Delete(Car car);

        Task<Car> GetByIdAsync(Guid id, CancellationToken cancellationToken);

        Task<List<Car>> GetAsync(CarFilter filter, CancellationToken cancellationToken);
    }
}
namespace Company.Example.Application.Contracts.Database
{
    using System.Threading.Tasks;

    public interface IUnitOfWork
    {
        Task SaveChangesAsync(CancellationToken cancellationToken);

        ICarRepository Cars { get; }
    }
}
namespace Company.Example.Application.Contracts.Database.Models
{
    public class CarFilter
    {
        public int Skip { get; set; }

        public int Take { get; set; }

        public string? Name { get; set; }

        public string? Make { get; set; }
    }
}
Company.Example.Application

Defines the use-cases the software is supposed to do. It coordinates interaction between different parts of our infrastructure layer.

In this layer, we are going to use MediatR for separating commands and queries, Fluent Validation for adding different validation rules and AutoMapper for mapping domain models to data transfer objects.

First, we are going to install a couple of things:

  • MediatR
  • MediatR.Extensions.Microsoft.DependencyInjection
  • FluentValidation
  • FluentValidation.DependencyInjectionExtensions
  • AutoMapper
  • AutoMapper.Extensions.Microsoft.DependencyInjection

After installing, your Company.Example.Application.csproj file will look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AutoMapper" Version="11.0.1" />
    <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
    <PackageReference Include="FluentValidation" Version="11.1.1" />
    <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.1.1" />
    <PackageReference Include="MediatR" Version="10.0.1" />
    <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Company.Example.Application.Contracts\Company.Example.Application.Contracts.csproj" />
  </ItemGroup>

</Project>

First, let’s set up our MediatR behavior. It will look something like this:

When we send a command/query via MediatR from controller, it will first go inside validator and throw an exception if conditions were not met. Then, it will go inside handler.

  • In Company.Example.Application create a folder Internal.
    • In Internal folder, create Behaviors folder
      • create ValidationBehavior.cs
    • In Internal folder, create Exceptions folder
      • create ServiceValidationException.cs
namespace Company.Example.Application.Internal.Behaviors
{
    using Company.Example.Application.Internal.Exceptions;
    using FluentValidation;
    using MediatR;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;

    public class ValidationBehavior<TRequest, TResponse>
        : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<TResponse>
    {
        private readonly IEnumerable<IValidator<TRequest>> _validators;

        public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
        {
            _validators = validators;
        }

        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            var failures = _validators
                .Select(validator => validator.Validate(request))
                .SelectMany(result => result.Errors)
                .Where(failure => failure is not null)
                .GroupBy(failure => failure.PropertyName)
                .ToDictionary(
                    group => group.Key,
                    group => group.Select(failure => failure.ErrorMessage).ToArray());

            if (failures.Any())
            {
                throw new ServiceValidationException(failures);
            }

            return await next();
        }
    }
}
namespace Company.Example.Application.Internal.Exceptions
{
    using System;
    using System.Collections.Generic;

    public class ServiceValidationException : ApplicationException
    {
        public ServiceValidationException(Dictionary<string, string[]> errors)
        {
            Title = "One or more validation errors occurred.";
            Detail = "Check the errors for more details";
            Errors = errors;
        }

        public string Title { get; }

        public string Detail { get; }

        public Dictionary<string, string[]> Errors { get; }
    }
}
  • To register it, in Company.Example.Application create DependencyInjection.cs
namespace Company.Example.Application
{
    using Company.Example.Application.Internal.Behaviors;
    using FluentValidation;
    using MediatR;
    using Microsoft.Extensions.DependencyInjection;
    using System.Reflection;

    public static class DependencyInjection
    {
        public static IServiceCollection AddApplicationConfiguration(
            this IServiceCollection services,
            params Assembly[] assemblies)
        {
            // Registers MediatR.
            services.AddMediatR(assemblies);

            // Registers Fluent Validation.
            services.AddValidatorsFromAssemblies(assemblies, includeInternalTypes: true);

            // Configuring our behavior.
            services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));

            // Configure AutoMapper.
            services.AddAutoMapper(Assembly.GetExecutingAssembly());

            return services;
        }
    }
}
  • Last thing to set up is mapping. Let’s add AutoMapper Profile that will map from our Car.cs to CarResponse.cs. Inside Internal folder create Mappings folder and add ApplicationMappingProfile.cs there.
namespace Company.Example.Application.Internal.Mappings
{
    using AutoMapper;
    using Company.Example.Application.Cars.Common;
    using Company.Example.Domain;

    internal sealed class ApplicationMappingProfile : Profile
    {
        public ApplicationMappingProfile()
        {
            this.CreateMap<Car, CarResponse>();
        }
    }
}

FEATURE: Get Car By Id

  • In Company.Example.Application add folder Cars
    • Inside Cars, add Commands folder
    • Inside Cars, add Queries folder
      • Create GetCarByIdQuery.cs
    • Inside Cars, add Common folder
      • Create CarResponse.cs

You should end up with something like this:

namespace Company.Example.Application.Cars.Queries
{
    using AutoMapper;
    using Company.Example.Application.Cars.Common;
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Domain;
    using FluentValidation;
    using MediatR;
    using System;
    using System.Threading;
    using System.Threading.Tasks;

    public record GetCarByIdQuery(Guid Id) : IRequest<CarResponse>;

    internal sealed class GetCarByIdQueryValidator : AbstractValidator<GetCarByIdQuery>
    {
        public GetCarByIdQueryValidator()
        {
            this.RuleFor(query => query.Id)
                .NotEmpty();
        }
    }

    internal sealed class GetCarByIdQueryHandler : IRequestHandler<GetCarByIdQuery, CarResponse>
    {
        private readonly IUnitOfWork unitOfWork;
        private readonly IMapper mapper;

        public GetCarByIdQueryHandler(IUnitOfWork unitOfWork, IMapper mapper)
        {
            this.unitOfWork = unitOfWork;
            this.mapper = mapper;
        }

        public async Task<CarResponse> Handle(GetCarByIdQuery request, CancellationToken cancellationToken)
        {
            Car car = await this.unitOfWork.Cars.GetByIdAsync(request.Id, cancellationToken);

            return this.mapper.Map<CarResponse>(car);
        }
    }
}

FEATURE: Get Cars

  • Create GetCarsQuery.cs
namespace Company.Example.Application.Cars.Queries
{
    using AutoMapper;
    using Company.Example.Application.Cars.Common;
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Application.Contracts.Database.Models;
    using Company.Example.Domain;
    using FluentValidation;
    using MediatR;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;

    public class GetCarsQuery : IRequest<List<CarResponse>>
    {
        public string? Name { get; set; }

        public string? Make { get; set; }

        public int Skip { get; set; }

        public int Take { get; set; }
    }

    internal sealed class GetCarsQueryValidator : AbstractValidator<GetCarsQuery>
    {
        public GetCarsQueryValidator()
        {
            this.RuleFor(c => c.Skip)
                .GreaterThanOrEqualTo(0);

            this.RuleFor(c => c.Take)
                .GreaterThan(0)
                .LessThanOrEqualTo(50);
        }
    }

    internal sealed class GetCarsQueryHandler : IRequestHandler<GetCarsQuery, List<CarResponse>>
    {
        private readonly IUnitOfWork unitOfWork;
        private readonly IMapper mapper;

        public GetCarsQueryHandler(IUnitOfWork unitOfWork, IMapper mapper)
        {
            this.unitOfWork = unitOfWork;
            this.mapper = mapper;
        }

        public async Task<List<CarResponse>> Handle(GetCarsQuery request, CancellationToken cancellationToken)
        {
            CarFilter filter = new CarFilter(request.Skip, request.Take, request.Name, request.Name);

            List<Car> cars = await this.unitOfWork.Cars.GetAsync(filter, cancellationToken);

            return this.mapper.Map<List<CarResponse>>(cars);
        }
    }
}

FEATURE: Create Car

  • Add CreateCarCommand.cs
namespace Company.Example.Application.Cars.Commands
{
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Domain;
    using FluentValidation;
    using MediatR;
    using System.Threading;
    using System.Threading.Tasks;

    public class CreateCarCommand : IRequest
    {
        public string Name { get; set; } = default!;

        public string Make { get; set; } = default!;

        public int Weight { get; set; }

        public int MaxSpeed { get; set; }
    }

    internal sealed class CreateCarCommandValidator : AbstractValidator<CreateCarCommand>
    {
        public CreateCarCommandValidator()
        {
            this.RuleFor(c => c.Name)
                .NotEmpty();

            this.RuleFor(c => c.Make)
                .NotEmpty();

            this.RuleFor(c => c.Weight)
                .GreaterThan(0);

            this.RuleFor(c => c.MaxSpeed)
                .GreaterThan(0);
        }
    }

    internal sealed class CreateCarCommandHandler : AsyncRequestHandler<CreateCarCommand>
    {
        private readonly IUnitOfWork unitOfWork;

        public CreateCarCommandHandler(IUnitOfWork unitOfWork)
        {
            this.unitOfWork = unitOfWork;
        }

        protected override async Task Handle(CreateCarCommand request, CancellationToken cancellationToken)
        {
            Car car = new Car(
                Guid.NewGuid(),
                request.Name,
                request.Make,
                request.Weight,
                request.MaxSpeed);

            this.unitOfWork.Cars.Create(car);

            await this.unitOfWork.SaveChangesAsync(cancellationToken);
        }
    }
}

FEATURE: Update Car

  • Create UpdateCarCommand.cs
namespace Company.Example.Application.Cars.Commands
{
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Domain;
    using FluentValidation;
    using MediatR;
    using System.Threading;
    using System.Threading.Tasks;

    public class UpdateCarCommand : IRequest
    {
        public Guid Id { get; set; }

        public string Name { get; set; } = default!;

        public string Make { get; set; } = default!;

        public int Weight { get; set; }

        public int MaxSpeed { get; set; }
    }

    internal sealed class UpdateCarCommandValidator: AbstractValidator<UpdateCarCommand>
    {
        public UpdateCarCommandValidator()
        {
            this.RuleFor(c => c.Id)
                .NotEmpty();
            
            this.RuleFor(c => c.Name)
                .NotEmpty();
            
            this.RuleFor(c => c.Make)
                .NotEmpty();
            
            this.RuleFor(c => c.Weight)
                .GreaterThan(0);
            
            this.RuleFor(c => c.MaxSpeed)
                .GreaterThan(0);
        }
    }

    internal sealed class UpdateCarCommandHandler : AsyncRequestHandler<UpdateCarCommand>
    {
        private readonly IUnitOfWork unitOfWork;

        public UpdateCarCommandHandler(IUnitOfWork unitOfWork)
        {
            this.unitOfWork = unitOfWork;
        }

        protected override async Task Handle(UpdateCarCommand request, CancellationToken cancellationToken)
        {
            Car car = await this.unitOfWork.Cars.GetByIdAsync(request.Id, cancellationToken);

            car.Update(request.Name, request.Make, request.Weight, request.MaxSpeed);

            this.unitOfWork.Cars.Update(car);

            await this.unitOfWork.SaveChangesAsync(cancellationToken);
        }
    }
}

FEATURE: Delete Car

  • Create DeleteCarByIdCommand.cs
namespace Company.Example.Application.Cars.Commands
{
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Domain;
    using FluentValidation;
    using MediatR;
    using System;
    using System.Threading;
    using System.Threading.Tasks;

    public class DeleteCarByIdCommand : IRequest
    {
        public DeleteCarByIdCommand(Guid id)
        {
            this.Id = id;
        }

        public Guid Id { get; set; }
    }

    internal sealed class DeleteCarByIdCommandValidator : AbstractValidator<DeleteCarByIdCommand>
    {
        public DeleteCarByIdCommandValidator()
        {
            this.RuleFor(c => c.Id)
                .NotEmpty();
        }
    }

    internal sealed class DeleteCarByIdCommandHandler : AsyncRequestHandler<DeleteCarByIdCommand>
    {
        private readonly IUnitOfWork unitOfWork;

        public DeleteCarByIdCommandHandler(IUnitOfWork unitOfWork)
        {
            this.unitOfWork = unitOfWork;
        }

        protected override async Task Handle(DeleteCarByIdCommand request, CancellationToken cancellationToken)
        {
            Car car = await this.unitOfWork.Cars.GetByIdAsync(request.Id, cancellationToken);

            this.unitOfWork.Cars.Delete(car);

            await this.unitOfWork.SaveChangesAsync(cancellationToken);
        }
    }
}
  • End result should look like this:
Infrastructure Layer
Company.Example.Infrastructure.Database.Mssql

This layer is responsible for interacting with external resources. We are going to communicate with MSSQL Server using EF Core.

We need to install a couple of things. Right click on the Company.Example.Infrastructure.Database.Mssql and select Manage Nuget Packages. From there we are going to install:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.Abstractions
  • Microsoft.Extensions.Configuration.UserSecrets
  • Microsoft.Extensions.DependencyInjection.Abstractions
  • Microsoft.Extensions.Hosting

Folder structure:

  • Internal
    • Extensions
      • QueryableExtensions.cs
    • Configurations
      • CarEntityTypeConfiguration.cs
    • Repositories
      • CarRepoistory
    • MssqlDbContext.cs
    • MssqlDbContextFactory.cs
    • UnitOfWork.cs
  • Migrations (this folder will be created automatically when we create migrations)
  • DependencyInjection.cs

Create our DbContext class.

namespace Company.Example.Infrastructure.Database.Mssql.Internal
{
    using Microsoft.EntityFrameworkCore;
    using System.Reflection;

    internal sealed class MssqlDbContext : DbContext
    {
        public MssqlDbContext(DbContextOptions<MssqlDbContext> options): base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder optionsBuilder)
        {
            optionsBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
        }
    }
}

Now we need to tell EF Core how it should map Car.cs to Cars table.

namespace Company.Example.Infrastructure.Database.Mssql.Internal.Configurations
{
    using Company.Example.Domain;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Metadata.Builders;

    internal sealed class CarEntityTypeConfiguration : IEntityTypeConfiguration<Car>
    {
        public void Configure(EntityTypeBuilder<Car> builder)
        {
            builder.ToTable("Cars");

            builder.HasKey(key => key.Id);

            builder.Property(p => p.Name).IsRequired();

            builder.Property(p => p.Make).IsRequired();

            builder.Property(p => p.MaxSpeed).IsRequired();

            builder.Property(p => p.Weight).IsRequired();
        }
    }
}

Before we implement CarRepository.cs, let’s add some helper extension methods for IQueryable interface.

namespace Company.Example.Infrastructure.Database.Mssql.Internal.Extensions
{
    using System;
    using System.Linq;
    using System.Linq.Expressions;

    internal static class QueryableExtensions
    {
        public static IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> source, bool condition, Expression<Func<TSource, bool>> predicate)
        {
            if (condition)
            {
                return source.Where(predicate);
            }

            return source;
        }
    }
}

Next thing to implement is CarRepository.cs. This class implements interface from Company.Example.Application.Contracts. Up until this point we were just creating contracts, now we are implementing them.

namespace Company.Example.Infrastructure.Database.Mssql.Internal.Repositories
{
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Application.Contracts.Database.Models;
    using Company.Example.Domain;
    using Company.Example.Infrastructure.Database.Mssql.Internal.Extensions;
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;

    internal sealed class CarRepository : ICarRepository
    {
        private readonly DbSet<Car> cars;

        public CarRepository(MssqlDbContext context)
        {
            this.cars = context.Set<Car>();
        }

        public void Create(Car car)
        {
            this.cars.Add(car);
        }

        public void Delete(Car car)
        {
            this.cars.Remove(car);
        }

        public async Task<List<Car>> GetAsync(CarFilter filter, CancellationToken cancellationToken)
        {
            return await this.cars
                .WhereIf(filter.Name is not null, p => p.Name.StartsWith(filter.Name!))
                .WhereIf(filter.Make is not null, p => p.Make.StartsWith(filter.Make!))
                .Skip(filter.Skip)
                .Take(filter.Take)
                .ToListAsync(cancellationToken);
        }

        public async Task<Car> GetByIdAsync(Guid id, CancellationToken cancellationToken)
        {
            return await this.cars.FindAsync(new object[] { id }, cancellationToken) ?? throw new ApplicationException("Unable to find car.");
        }

        public void Update(Car car)
        {
            this.cars.Update(car);
        }
    }
}

Implement UnitOfWork.cs class. This is just a facade around EF Core.

namespace Company.Example.Infrastructure.Database.Mssql.Internal
{
    using Company.Example.Application.Contracts.Database;
    using System.Threading;
    using System.Threading.Tasks;

    internal sealed class UnitOfWork : IUnitOfWork
    {
        private readonly MssqlDbContext context;

        public UnitOfWork(ICarRepository cars, MssqlDbContext context)
        {
            this.context = context;
            this.Cars = cars;
        }

        public ICarRepository Cars { get; }

        public async Task SaveChangesAsync(CancellationToken cancellationToken)
        {
            await this.context.SaveChangesAsync(cancellationToken);
        }
    }
}

Implement DependencyInjection.cs. Here we are going to register our DbContext and all contracts implementation relevant to this project.

namespace Company.Example.Infrastructure.Database.Mssql
{
    using Company.Example.Application.Contracts.Database;
    using Company.Example.Infrastructure.Database.Mssql.Internal;
    using Company.Example.Infrastructure.Database.Mssql.Internal.Repositories;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.DependencyInjection;

    public static class DependencyInjection
    {
        public static IServiceCollection AddInfrastructureDatabaseMssql(this IServiceCollection services, MssqlSettings settings)
        {
            services.AddDbContext<MssqlDbContext>(options => options.UseSqlServer(settings.ConnectionString));

            services.AddScoped<ICarRepository, CarRepository>();

            services.AddScoped<IUnitOfWork, UnitOfWork>();

            return services;
        }
    }

    public class MssqlSettings
    {
        public const string Key = nameof(MssqlSettings);

        public string ConnectionString { get; set; } = default!;
    }
}

Next, we need to create our database. Since we are using Code First Approach we need to create Migrations for our database.

If you don’t have any SQL Server installed on your machine, you can do it easily with Docker.

  • Install Docker
  • Pull MSSQL image:
docker pull mcr.microsoft.com/mssql/server
  • Run container
docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=yourStrong(!)Password" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest

Now, we need to add MssqlDbContextFactory.cs so that we are able to run migrations from command line.

namespace Company.Example.Infrastructure.Database.Mssql.Internal
{
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Design;
    using Microsoft.Extensions.Configuration;

    internal sealed class MssqlDbContextFactory : IDesignTimeDbContextFactory<MssqlDbContext>
    {
        public MssqlDbContext CreateDbContext(string[] args)
        {
            IConfigurationRoot configuration = BuildConfiguration(args);

            var optionsBuilder = new DbContextOptionsBuilder<MssqlDbContext>();
            var connectionString = configuration.GetSection(args[0]).Value;

            optionsBuilder
                .UseSqlServer(connectionString);

            var instance = new MssqlDbContext(optionsBuilder.Options);

            if (instance is null)
            {
                throw new InvalidOperationException($"Unable to initialize {nameof(MssqlDbContext)} instance.");
            }

            return instance;
        }

        private static IConfigurationRoot BuildConfiguration(string[] args)
        {
            return new ConfigurationBuilder()
                .SetBasePath(args[2])
                .AddJsonFile(
                    path: "appsettings.json",
                    optional: false,
                    reloadOnChange: true)
                .AddEnvironmentVariables()
                .AddUserSecrets(args[1])
                .AddCommandLine(args)
                .Build();
        }
    }
}
dotnet ef migrations add InitialCreate --project [ProjectName] -- [Arg0] [Arg1] [Arg2]
ProjectNamePath to infrastruture project
Arg0Name of key in appsettings.json
Arg1Secrets.json unique id found in Company.Cars.csproj
Arg2Path to startup project

Right click on startup project, Company.Example, and select Manage User Secrets. This will generate your secrets.json file where you can add your connection string.

{
  "MssqlSettings:ConnectionString": "Server=localhost;Database=ExampleService;User Id=SA;Password=yourStrong(!)Password;"
}

Also, this will create a unique Guid in you Company.Example.csproj file that we are going to need. Double click on Company.Example and you will see:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UserSecretsId>71662ab5-0465-421c-b3ff-a027bc12246d</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Application\Company.Example.Application\Company.Example.Application.csproj" />
    <ProjectReference Include="..\Infrastructure\Company.Example.Infrastructure.Database.Mssql\Company.Example.Infrastructure.Database.Mssql.csproj" />
    <ProjectReference Include="..\Presentation\Company.Example.Presentation.Api\Company.Example.Presentation.Api.csproj" />
  </ItemGroup>

</Project>

Run migration command – you need to use your parameters

dotnet ef migrations add InitialCreate --project src\Infrastructure\Company.Example.Infrastructure.Database.Mssql -- MssqlSettings:ConnectionString 71662ab5-0465-421c-b3ff-a027bc12246d C:\Users\Mario\source\tutorials\Company.Example\src\Company.Example

Update database command – you need to use your parameters

dotnet ef database update --project src\Infrastructure\Company.Example.Infrastructure.Database.Mssql -- MssqlSettings:ConnectionString 71662ab5-0465-421c-b3ff-a027bc12246d C:\Users\Mario\source\tutorials\Company.Example\src\Company.Example
Presentation Layer

This layer is public exposed and can be consumed by clients. This is an entry point for clients. This can be an REST API, SOAP, Kafka Consumer etc.

Company.Example.Presentation.Api

In this layer we are going to use MediatR in order to send commands/queries to our application layer and Hellang Middleware in order to map our exceptions.

You know the drill, we need to install something first:

  • Hellang.Middleware.ProblemDetails
  • Microsoft.AspNetCore.Mvc.NewtonsofJson
  • Newtonsoft.Json
  • Swashbuckle.AspNetCore

Folder structure

  • Controllers
    • V1
      • Models
        • CreateCarDto.cs
        • GetCarsQueryDto.cs
        • UpdateCarDto.cs
      • CarsController.cs
      • ApiControllerBase.cs
  • Internal
    • Mappings
      • PresentationMappingProfile.cs
  • DependencyInjection.cs

We need to create our models that our clients will use.

namespace Company.Example.Presentation.Api.Controllers.V1.Models.Cars
{
    public record CreateCarDto(
        string Name,
        string Make,
        int Weight,
        int MaxSpeed);
}
namespace Company.Example.Presentation.Api.Controllers.V1.Models.Cars
{
    public record GetCarsQueryDto(
        string? Name,
        int Skip = 0,
        int Take = 20);
}
namespace Company.Example.Presentation.Api.Controllers.V1.Models.Cars
{
    using System;

    public record UpdateCarDto(
        Guid Id,
        string Name,
        string Make,
        int Weight,
        int MaxSpeed);
}

Then, let’s create an abstract controller.

namespace Company.Example.Presentation.Api.Controllers.V1
{
    using AutoMapper;
    using MediatR;
    using Microsoft.AspNetCore.Mvc;
    using System.Net.Mime;

    [ApiController]
    [Produces(MediaTypeNames.Application.Json)]
    [Route("api/[controller]")]
    public abstract class ApiControllerBase : ControllerBase
    {
        protected ApiControllerBase(
            IMediator mediator,
            IMapper mapper)
        {
            this.Mediator = mediator;
            this.Mapper = mapper;
        }
        protected IMediator Mediator { get; }

        protected IMapper Mapper { get; }
    }
}

Implement our CarsController.cs class

namespace Company.Example.Presentation.Api.Controllers.V1
{
    using AutoMapper;
    using Company.Example.Application.Cars.Commands;
    using Company.Example.Application.Cars.Common;
    using Company.Example.Application.Cars.Queries;
    using Company.Example.Presentation.Api.Controllers.V1.Models.Cars;
    using MediatR;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;

    public class CarsController : ApiControllerBase
    {
        public CarsController(IMediator mediator, IMapper mapper) : base(mediator, mapper)
        {
        }

        /// <summary>
        /// Gets cars.
        /// </summary>
        /// <param name="query">Model for quering cars.</param>
        [HttpGet]
        [ProducesResponseType(typeof(List<CarResponse>), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
        public async Task<IActionResult> Get([FromQuery] GetCarsQueryDto request)
        {
            var query = this.Mapper.Map<GetCarsQuery>(request);

            var response = await this.Mediator.Send(query);

            return this.Ok(response);
        }

        /// <summary>
        /// Creates car.
        /// </summary>
        [HttpPost]
        [ProducesResponseType(typeof(CarResponse), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
        public async Task<IActionResult> Post([FromBody] CreateCarDto request)
        {
            var command = this.Mapper.Map<CreateCarCommand>(request);

            await this.Mediator.Send(command);

            return this.NoContent();
        }

        /// <summary>
        /// Updates car.
        /// </summary>
        [HttpPut("{id}/fuzBiz")]
        [ProducesResponseType(typeof(CarResponse), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
        public async Task<IActionResult> Put(Guid id, [FromBody] UpdateCarDto request)
        {
            var command = this.Mapper.Map<UpdateCarCommand>(request, opt => opt.AfterMap((_, command) =>
            {
                command.Id = id;
            }));

            await this.Mediator.Send(command);

            return this.NoContent();
        }

        /// <summary>
        /// Gets car by id.
        /// </summary>
        [HttpGet("{id}")]
        [ProducesResponseType(typeof(CarResponse), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
        public async Task<IActionResult> GetById(Guid id)
        {
            var response = await this.Mediator.Send(new GetCarByIdQuery(id));

            return this.Ok(response);
        }

        /// <summary>
        /// Deletes car by id.
        /// </summary>
        [HttpDelete("{id}")]
        [ProducesResponseType(typeof(CarResponse), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status500InternalServerError)]
        public async Task<IActionResult> DeleteById(Guid id)
        {
            await this.Mediator.Send(new DeleteCarByIdCommand(id));

            return this.NoContent();
        }
    }
}

Since we are using AutoMapper to map from DTO to command/query, we need a mapping profile.

namespace Company.Example.Presentation.Api.Internal.Mappings
{
    using AutoMapper;
    using Company.Example.Application.Cars.Commands;
    using Company.Example.Application.Cars.Queries;
    using Company.Example.Presentation.Api.Controllers.V1.Models.Cars;

    internal sealed class PresentationMappingProfile : Profile
    {
        public PresentationMappingProfile()
        {
            this.CreateMap<CreateCarDto, CreateCarCommand>();
            this.CreateMap<UpdateCarDto, UpdateCarCommand>();
            this.CreateMap<GetCarsQueryDto, GetCarsQuery>();
        }
    }
}

And finally, we need to register everything in DependencyInjection.cs

namespace Company.Example.Presentation.Api
{
    using Company.Example.Application.Internal.Exceptions;
    using Hellang.Middleware.ProblemDetails;
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Routing;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    using System.Reflection;

    public static class DependencyInjection
    {
        public static IServiceCollection AddPresentationConfiguration(
            this IServiceCollection services,
            IHostEnvironment environment)
        {
            services.AddAutoMapper(Assembly.GetExecutingAssembly());

            Action<RouteOptions> routeOptions = options => options.LowercaseUrls = true;

            Action<ProblemDetailsOptions> problemDetailsOptions = options => SetProblemDetailsOptions(options, environment);

            Action<MvcNewtonsoftJsonOptions> newtonsoftOptions = options =>
            {
                options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
                options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
                options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            };

            services
                .AddRouting(routeOptions)
                .AddProblemDetails(problemDetailsOptions)
                .AddControllers()
                .AddNewtonsoftJson(newtonsoftOptions);

            services.AddSwaggerGen();

            return services;
        }

        private static void SetProblemDetailsOptions(ProblemDetailsOptions options, IHostEnvironment enviroment)
        {
            Type[] knownExceptionTypes = new Type[] { typeof(ServiceValidationException) };

            options.IncludeExceptionDetails = (_, exception) =>
                enviroment.IsDevelopment() &&
                !knownExceptionTypes.Contains(exception.GetType());

            options.Map<ServiceValidationException>(exception =>
                new ValidationProblemDetails(exception.Errors)
                {
                    Title = exception.Title,
                    Detail = exception.Detail,
                    Status = StatusCodes.Status400BadRequest
                });
        }
    }
}
Company.Example

Finally, lets connect everything in our startup project.

Create ApplicationLauncher.cs which will be used in Program.cs to run application.

namespace Company.Example
{
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.Hosting;

    public static class ApplicationLauncher
    {

        public static async Task RunAsync<TStartup>(string[] args)
            where TStartup : class
        {
            var builder = new ConfigurationBuilder();

            BuildConfiguration<TStartup>(builder, args);
           
            await CreateHostBuilder<TStartup>(args).Build().RunAsync();

        }

        private static IHostBuilder CreateHostBuilder<TStartup>(string[] args)
            where TStartup : class
        {
            return Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<TStartup>());
        }

        private static void BuildConfiguration<TStartup>(IConfigurationBuilder builder, string[] args)
            where TStartup : class
        {
            var environment = Environment.GetEnvironmentVariable(HostEnvironment.Variable);

            var isDevelopment = string.IsNullOrWhiteSpace(environment)
                || string.Equals(environment, HostEnvironment.Development, StringComparison.OrdinalIgnoreCase);

            builder
                .SetBasePath(AppContext.BaseDirectory)
                .AddJsonFile(
                    path: "appsettings.json",
                    optional: false,
                    reloadOnChange: true)
                .AddJsonFile(
                    path: $"appsettings.{environment}.json",
                    optional: true,
                    reloadOnChange: true)
                .AddEnvironmentVariables();

            if (isDevelopment)
            {
                builder.AddUserSecrets<TStartup>();
            }

            builder.AddCommandLine(args);
        }
    }
}

In our Startup.cs we need to connect everything from those DependencyInjection.cs classes we added in each layer. Also, additional configuration is needed.

namespace Company.Example
{
    using Company.Example.Application;
    using Company.Example.Infrastructure.Database.Mssql;
    using Company.Example.Presentation.Api;
    using Hellang.Middleware.ProblemDetails;

    internal sealed class Startup
    {
        public Startup(
            IConfiguration configuration,
            IWebHostEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }

        public IConfiguration Configuration { get; }

        public IWebHostEnvironment Environment { get; }

        public MssqlSettings MssqlSettings =>
            Configuration
                .GetSection(MssqlSettings.Key)
                .Get<MssqlSettings>();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddCors();
            services.AddInfrastructureDatabaseMssqlConfiguration(MssqlSettings);
            services.AddApplicationConfiguration();
            services.AddPresentationConfiguration(Environment);
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseProblemDetails();

            if (!Environment.IsDevelopment())
            {
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseCors(options => options
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader());

            app.UseSwagger();
            app.UseSwaggerUI();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }
}

And finally, our Program.cs.

namespace Company.Example
{
    public static class Program
    {
        public static async Task Main(string[] args) => await ApplicationLauncher.RunAsync<Startup>(args);
    }
}

You can find project on Github