Henüz makalenin ilk bölümünü okumadıysanız, konuyu daha iyi anlayabilmek adına buradan ulaşabilirsiniz.

Makalenin bu ikinci bölümünde ise, clean architecture konsept’inin .NET Core ile minimal düzeyde implementasyon işleminden bahsedeceğim.

Örnek olarak, içerisinde film ekleyebileceğimiz ve listeleyebileceğimiz basit bir API geliştireceğiz.

Application Domain’in Implementasyonu

İlk önce architecture’ın kalbi olacak olan application domain kısmını oluşturacağız.

Öncesinde ise kısaca “Application Domain” i hatırlayalım. Bu layer database, UI vb. framework’lerden isolated bir şekilde architecture’ın ortasında konumlanmaktadır. Temel olarak içerisinde ise domain modellerini, use-case’leri ve external interface’leri barındırmaktadır.

İlk olarak “Minimal.Core” isminde bir class library’si oluşturalım.

dotnet new classlib -n Minimal.Core

Ardından bu library içerisinde “Models” isminde bir klasör oluşturalım ve domain modellerini aşağıdaki gibi içerisinde tanımlayalım.

using System;

namespace Minimal.Core.Models
{
    public abstract class EntityBase
    {
        public Guid Id { get; set; }
        public DateTime CreatedAt { get; set; }
        public DateTime ModifiedAt { get; set; }
    }
}

namespace Minimal.Core.Models
{
    public class Movie : EntityBase
    {
        public string Name { get; set; }
        public double Rating { get; set; }
        public int AgeGroup { get; set; }
        public bool IsDeleted { get; set; }
    }
}

Modelleri tanımladıktan sonra ise port’ları tanımlayacağımız “Interfaces” klasörünü oluşturalım. Bu klasör içerisinde ise “IRepository” isminde bir interface/port tanımlayalım.

Bu port sayesinde database işlemlerini, “Application Domain” layer içerisinde herhangi bir teknolojiye bağımlı olmadan isolated bir şekilde gerçekleştirebileceğiz.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;

namespace Minimal.Core.Interfaces
{
    public interface IRepository<T> : IDisposable where T : class
    {
        Task CreateAsync(T entity);
        Task<IEnumerable<T>> GetWhereAsync(Expression<Func<T, bool>> predicate);
    }
}

Port’u tanımladıktan sonra ise, adapter’ler arasında kullanacağımız data transfer objelerini de tanımlamamız gerekmektedir.

Bunun için, “Dtos” isminde bir klasör daha oluşturalım. Klasörü oluşturduktan sonra içerisinde “BaseResponse” ve “Movie” DTO‘larını aşağıdaki gibi tanımlayalım.

using System.Collections.Generic;
using System.Linq;

namespace Minimal.Core.Dtos
{
    public class BaseResponseDto<TData>
    {
        public BaseResponseDto()
        {
            Errors = new List<string>();
        }

        public bool HasError => Errors.Any();
        public List<string> Errors { get; set; }
        public int Total { get; set; }
        public TData Data { get; set; }
    }
}
namespace Minimal.Core.Dtos
{
    public class MovieDto
    {
        public string Name { get; set; }
        public double Rating { get; set; }
        public int AgeGroup { get; set; }
    }
}

Request modellerini ise, “Requests” isimli bir klasör altında toplayacağız. Bunun için “Dtos” klasörü altında, “Requests” isimli bir klasör daha oluşturalım.

Minimal.Core
-Models
-Interfaces
-Dtos
--Requests

Request modellerini oluşturmadan önce ise, MediatR paketini NuGet üzerinden projeye dahil etmemiz gerekmektedir.

dotnet add package MediatR

Bu paket sayesinde API ile Application Domain arasındaki iletişimi, loosely coupled olarak tek bir noktadan gerçekleştirebileceğiz. Ayrıca use-case yaklaşımını da implemente ederken MediatR paketinden faydalanacağız.

NOT: Dilerseniz herhangi bir paket kullanmadan, kendi use-case interface’lerinizi tanımlayabilir ve implementasyonlarını oluşturabilirsiniz.

Şimdi “Requests” klasörü içerisinde, movie işlemleri için gerekli olan request modellerini tanımlayabiliriz. Bunun için “CreateMovieRequest” ve “GetBestMoviesForKidsRequest” modellerini aşağıdaki gibi tanımlayalım.

using MediatR;

namespace Minimal.Core.Dtos.Requests
{
    public class CreateMovieRequest : IRequest<BaseResponseDto<bool>>
    {
        public string Name { get; set; }
        public double Rating { get; set; }
    }
}
using System.Collections.Generic;
using MediatR;

namespace Minimal.Core.Dtos.Requests
{
    public class GetBestMoviesForKidsRequest : IRequest<BaseResponseDto<List<MovieDto>>>
    {
        
    }
}

Modelleri tanımlarken MediatR‘ın “IRequest” marker interface’i ile de, modelleri dönüş tipleri ile birlikte işaretleyerek oluşturduk.

Bu noktaya kadar “Domain” modellerini, “DTO‘ları” ve external interface’leri yani “Port” ları tanımlamış olduk.

Artık business use-case’lerini implemente etmeye başlayabiliriz. Öncelikle “Minimal.Core” projesi altında, “Services/MovieUseCases” klasörlerini oluşturalım.

Minimal.Core
-Models
-Interfaces
-Dtos
--Requests
-Services
--MovieUseCases

Oluşturmuş olduğumuz “MovieUseCases” klasörü altında, movie ile ilgili tüm use-case’leri implemente edeceğiz.

Şimdi “MovieUseCases” klasörü altında, “CreateMovieHandler” isminde bir class oluşturalım ve movie oluşturma işlemini aşağıdaki gibi implemente edelim.

using System;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
using Minimal.Core.Dtos;
using Minimal.Core.Dtos.Requests;
using Minimal.Core.Events;
using Minimal.Core.Interfaces;
using Minimal.Core.Models;

namespace Minimal.Core.Services.MovieUseCases
{
    public class CreateMovieHandler : IRequestHandler<CreateMovieRequest, BaseResponseDto<bool>>
    {
        private readonly IRepository<Movie> _repository;
        private readonly ILogger<CreateMovieHandler> _logger;
        private readonly IMediator _mediator;

        public CreateMovieHandler(IRepository<Movie> repository, ILogger<CreateMovieHandler> logger, IMediator mediator)
        {
            _repository = repository;
            _logger = logger;
            _mediator = mediator;
        }

        public async Task<BaseResponseDto<bool>> Handle(CreateMovieRequest request, CancellationToken cancellationToken)
        {
            BaseResponseDto<bool> response = new BaseResponseDto<bool>();

            try
            {
                var movie = new Movie
                {
                    Name = request.Name,
                    Rating = request.Rating,
                    IsDeleted = false,
                    CreatedAt = DateTime.Now
                };

                await _repository.CreateAsync(movie);

                response.Data = true;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, ex.Message);
                response.Errors.Add("An error occurred while creating the movie.");
            }

            return response;
        }
    }
}

Kısaca neler yaptığımıza bir bakalım.

Öncelikle “IRequestHandler” interface’ini implemente ederek, “CreateMovieHandler” class’ının bir use-case request handler’ı olduğunu belirttik. Bu handler kısaca “CreateMovieRequest” model’ini handle ederek, geriye “BaseResponseDto” tipinde bir response modeli dönmektedir.

Business use-case’ini ise dummy olarak “Handle” method’u içerisinde implemente ettik. Burada dikkat etmemiz gereken nokta ise, “IRepository” interface’inin kullanımıdır.

Dikkat edersek “IRepository” interface’i herhangi başka bir layer/adapter içerisinde değil, “Minimal.Core.Interfaces” içerisinde konumlanmaktadır. Yani “Minimal.Core“, içerisinde tanımlamış olduğumuz external port’u kullanıyoruz.

Bu sayede “CreateMovieHandler” use-case’i, herhangi bir teknoloji değişiminden etkilenmeyecektir.

Şimdi ise çocuklar için en iyi filmleri getirecek olan use-case’i implemente edelim. Bunun için “GetBestMoviesForKidsHandler” isminde bir class daha oluşturalım ve aşağıdaki gibi implemente edelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.Extensions.Logging;
using Minimal.Core.Dtos;
using Minimal.Core.Dtos.Requests;
using Minimal.Core.Interfaces;
using Minimal.Core.Models;

namespace Minimal.Core.Services.MovieUseCases
{
    public class GetBestMoviesForKidsHandler : IRequestHandler<GetBestMoviesForKidsRequest, BaseResponseDto<List<MovieDto>>>
    {
        private readonly IRepository<Movie> _repository;
        private readonly ILogger<GetBestMoviesForKidsHandler> _logger;

        public GetBestMoviesForKidsHandler(IRepository<Movie> repository, ILogger<GetBestMoviesForKidsHandler> logger)
        {
            _repository = repository;
            _logger = logger;
        }

        public async Task<BaseResponseDto<List<MovieDto>>> Handle(GetBestMoviesForKidsRequest request, CancellationToken cancellationToken)
        {
            BaseResponseDto<List<MovieDto>> response = new BaseResponseDto<List<MovieDto>>();

            try
            {
                List<MovieDto> movies = (await _repository.GetWhereAsync(m => m.AgeGroup <= 16)).Select(m => new MovieDto
                {
                    Name = m.Name,
                    Rating = m.Rating,
                    AgeGroup = m.AgeGroup
                }).ToList();

                response.Data = movies;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, ex.Message);
                response.Errors.Add("An error occurred while getting movies.");
            }

            return response;
        }
    }
}

Bu use-case içerisinde ise, basitçe çocuklar için uygun olan filmleri getirecek bir logic kodladık. Artık örneğimize göre application domain katmanı hazır durumda.

Şimdi ise database işlemlerini gerçekleştireceğimiz, infrastructure kısmına geçelim.

Infrastructure’ın Implementasyonu

Öncelikle “Minimal.Infrastructure” isminde bir class library oluşturalım.

dotnet new classlib -n Minimal.Infrastructure

Ardından “Minimal.Core” projesini referans olarak ekleyelim.

ORM olarak EntityFrameworkCore kullanacağımız için NuGet üzerinden “Microsoft.EntityFrameworkCore” paketini de projeye dahil edelim.

“Minimal.Core” projesi içerisinde hatırlarsak, dependency flow’unu tersine çevirebilmek için “IRepository” isminde bir port tanımlamıştık. Infrastructure içerisinde ise tanımlamış olduğumuz bu port’u implemente edeceğiz.

İlk olarak bir DbContext oluşturalım. Bunun için “AppDbContext” isminde bir class tanımlayalım ve aşağıdaki gibi kodlayalım.

using Microsoft.EntityFrameworkCore;
using Minimal.Core.Models;

namespace Minimal.Infrastructure
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        { }

        public DbSet<Movie> Movies { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            //...
        }
    }
}

Şimdi repository class’ını implemente edebiliriz.

Bunun için “Repositories” isminde bir klasör oluşturalım ve ardından “Repository” class’ını aşağıdaki gibi bu klasör içerisinde implemente edelim.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Minimal.Core.Interfaces;
using Minimal.Core.Models;

namespace Minimal.Infrastructure.Repositories
{
    public class Repository<T> : IRepository<T> where T : EntityBase
    {
        private readonly AppDbContext _context;
        private DbSet<T> _dbSet;

        public Repository(AppDbContext context)
        {
            _context = context;
            _dbSet = context.Set<T>();
        }

        public async Task CreateAsync(T entity)
        {
            _dbSet.Add(entity);

            await _context.SaveChangesAsync();
        }

        public async Task<IEnumerable<T>> GetWhereAsync(Expression<Func<T, bool>> predicate)
        {
            return await _dbSet.Where(predicate).ToListAsync();
        }

        private bool _disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    _context.Dispose();
                }
            }
            _disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}

Yukarıdaki kod bloğunda, oluşturmuş olduğumuz db context’i kullanarak basit bir generic repository implementasyonu gerçekleştirdik. Böylece “Infrastructure” adapter’ünün implementasyon işlemini de tamamlamış olduk.

Son olarak, bir de communication adapter’üne/API‘ına ihtiyacımız var.

API’ın Implementasyonu

Architecture bakış açısından baktığımızda, API‘ın imlementasyon işleminin de diğer adapter’lerin implementasyon işleminden bir farklı yoktur. Hatırlarsak hexagonal architecture için, merkezinde core’u barındıran ve istediğimiz adapter’ü söküp takabileceğimiz bir plug-in yapısı gibi düşünebiliriz demiştik.

Implementasyon için öncelikle aşağıdaki komut satırını kullanarak, bir ASP.NET Core Emtpy Web projesi oluşturalım.

dotnet new web -n Minimal.API

Ardından application domain layer’ı ve infrastructure adapter’ünü referans olarak ekleyelim.

dotnet add reference ../Minimal.Core/Minimal.Core.csproj
dotnet add reference ../Minimal.Infrastructure/Minimal.Infrastructure.csproj

Application domain içerisinde business use-case’lerini implemente ederken, MediatR library’sinden yararlanmıştık. Bu yaklaşım sayesinde API ile application domain arasındaki iletişimi, loosely coupled olarak tek bir noktadan gerçekleştirebileceğimizden bahsetmiştik.

Bunun için, NuGet üzerinden “MediatR” ve “MediatR.Extensions.Microsoft.DependencyInjection” paket’lerini API projesine dahil etmemiz gerekmektedir.

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Artık implementasyon işlemine hazırız.

Şimdi “Controllers” klasörü altında, “MoviesController” isminde bir API controller class’ı oluşturalım ve aşağıdaki gibi kodlayalım.

using System.Collections.Generic;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Minimal.Core.Dtos;
using Minimal.Core.Dtos.Requests;

namespace Minimal.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class MoviesController : ControllerBase
    {
        private readonly IMediator _mediator;

        public MoviesController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpPost]
        public async Task<ActionResult<string>> CreateMovieAsync([FromBody]CreateMovieRequest createMovieRequest)
        {
            BaseResponseDto<bool> createResponse = await _mediator.Send(createMovieRequest);

            if (createResponse.Data)
            {
                return Created("...", null);
            }
            else
            {
                return BadRequest(createResponse.Errors);
            }
        }

        [HttpGet("kids")]
        public async Task<ActionResult<List<MovieDto>>> GetBestMoviesForKidsAsync()
        {
            BaseResponseDto<List<MovieDto>> getBestMoviesForKidsReponse = await _mediator.Send(new GetBestMoviesForKidsRequest());

            if (!getBestMoviesForKidsReponse.HasError)
            {
                return Ok(getBestMoviesForKidsReponse.Data);
            }
            else
            {
                return BadRequest(getBestMoviesForKidsReponse.Errors);
            }
        }
    }
}

Yukarıdaki kod bloğunda, movie oluşturabilmek için gerekli olan “CreateMovieAsync” ve çocuklar için en iyi filmleri getirecek olan “GetBestMoviesForKidsAsync” method’larını implemente ettik.

Method’lar içerisinde ise mediator’a, sadece handle etmesini istediğimiz ilgili request model’lerini parametre olarak aşağıdaki gibi set ettik.

await _mediator.Send(createMovieRequest);
await _mediator.Send(new GetBestMoviesForKidsRequest());

Mediator ise burada, bizim için ilgili request handler’larını bulup, request modellerini execute edecektir. Bu işlemleri tek bir noktadan gerçekleştirebilmek, ne kadar da pratik bir yaklaşım değil mi?

Şimdi “Startup” class’ında ise injection işlemlerini aşağıdaki gibi gerçekleştirelim.

using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Minimal.Core.Interfaces;
using Minimal.Core.Services.MovieUseCases;
using Minimal.Infrastructure;
using Minimal.Infrastructure.Repositories;
using Swashbuckle.AspNetCore.Swagger;

namespace Minimal.API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            //Infrastructure
            services.AddDbContext<AppDbContext>(opt => opt.UseInMemoryDatabase("TestDB"));
            services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

            //Services
            services.AddMediatR(typeof(CreateMovieHandler));
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseMvc();
        }
    }
}

“Startup” class’ı içerisinde, test amacıyla bir in-memory database kullanacağımızı belirttik. Ardından “IRepository” interface’inin ve MediatR library’sinin injection işlemlerini gerçekleştirdik.

Artık bu adapter de hazır durumda.

Böylece bu örnek projenin, clean architecture ile implementasyonunu tamamladık diyebiliriz.

Sonuç

Özellikle yukarıdan aşağıya doğru bir dependency flow’una alışmışsak, port’ları yani interface’leri application domain içerisinde tanımlayıp, ardından adapter’lerini architecture’ın etrafında implemente etmek biraz garip gelebilir. Architecture’a alıştıktan sonra ise, bu yaklaşımın, gelebilecek olan değişimler/yeni özellikler, test edilebilirlik ve maintenance işlemleri karşısında bizlere nasıl hız ve esneklik kazandırdığını görebiliriz.

Örneğin, database olarak bir NoSQL teknolojisi kullanmaya karar verdiğimizi varsayalım. Tek yapmamız gereken “IRepository” port’unu implemente eden bir adapter oluşturmak ve gerekli injection işlemlerini gerçekleştirmek.

Çünkü architecture’ın doğası gereği dış dünya ile hiç bir bağımlılığı bulunmayan bir inner/core layer inşaa ediyoruz.

Core layer’ın etrafını ise, ihtiyaçlar doğrultusunda değiştirilebilir adapter’ler ile donatıyoruz.

https://github.com/GokGokalp/CleanArchitectureBoilerplates/tree/master/src/MinimalCleanArchitecture