İçeriğe geç

Getting Started with Clean Architecture using ASP.NET Core – 02

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 “MovieDTO‘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

Kategori:.NET CoreArchitecturalASP.NET Core

17 Yorum

  1. eren arslan eren arslan

    merhaba bir kaç sorum olacaktı. Maddeler halinde sormak istiyorum.
    1) Kendi projelerimde onion mimari kullanıyorum. Ve Monolitic yapıları oluyor. Clear architecture daha profesyonel duruyor bence ama her bir service in en az 6-7 methodu clean de ki karşılığı handler ı olucağı için startup dosyasında 6-7 service karşılığında 50-60 tane handler DI etmemiz gerekicek. Bu normal mi ?
    2) events dosyasının açıklamasını yapmamışsınız sanırım burada kafam karıştı. Event Driven ve Message Driven diye 2 konudan söz ediliyor. RabbitMQ ya event kısmında mı mesaj yollayacağız ? Bunun için mi oluşturdunuz burayı.
    3)Microservices bi kaç aydır baya dikkatimi çekiyor ama her servisin kendi DB si olması kafamı karıştırdı. Joinler nasıl yapılacak ? Veya buna çözümü nedir ? Clear architexture sadece micro services için uygun gibi yoksa 6-7 service deki okadar çok handlerı DI yapmak doğru olmaz sanırım.
    4)Mesela Makale post edicez ve içinde image upload kısmıda olucak. Kullanıcının makalesini veri tabanına ekledik ama image cdn e atmak ve beklememek için rabbitmq kullanıcam UI da Facebook da vs resimlerin yüklenmesini bekliyor bizim seneryoda beklemiyor . UI tarafında nasıl bir seneryo uygulamalı?

    • Merhaba, bu harika sorular için teşekkür ederim.

      1) Açıkcası sizin tamamen iş ihtiyaçlarınıza göre değişir nasıl bir project structure’ı takip edeceğiniz. Eğer probleminiz çok fazla DI işlemleri ise, doğasında var. (Zaten çok fazla bir sayıya ulaşıyorsanız da, bir yerlerde yanlış bir şeyler var demektir. Tekrardan domain sınırlarınızı gözden geçirmeniz gerekebilir, tabi monolith ilerlemiyorsanız.)
      2) Evet, kafa karıştırmamak için event kısmını es geçmiştim. Her iki event işleri için de kullanabilirsiniz gerek RabbitMQ, gerekse de inter-process communication işlemleri.
      3) Bu çok derin bir konu, burada iki satırda değinmek, buna haksızlık olur sanırım. 🙂 Kısaca yine iş yapınıza göre değişir gerek shared-db yaklaşımı, gerekse de per microservice yaklaşımı izleyebilirsiniz. 2. senaryoda ise, her db’nin gerekli joinlemek istediğiniz tablodaki verileri tutabileceği bir projection datasına da ihtiyaç duyacaktır. Clean architecture’ı gerekli gördüğünüz yapılarda kullanabilirsiniz, bir ayrım yapamam maalesef.
      4) Eğer non-blocking bir işlem gerçekteştirmek istiyorsanız yani senaryonuz async operasyonlara el verişli ise, UI sadece isteğiniz alınmıştır diyip kullanıcıya bir mesaj gösterebilirsiniz. Arkadan async olarak bir rabbitmq consumer’ı aracılığı ile istediğiniz image işlemlerini gerçekleştirebilirsiniz.

  2. Murat Murat

    Interface’leri Core projeye koyup Core’u komple diğer projelere referans olarak eklemek yerine, Interface’leri bir proje içine koyup hem Core’a hem de diğer projelere referans olarak eklemek daha iyi olmaz mı?

    • Murat Murat

      Bir de burada neden Mediator pattern’e ihtiyaç duyuyoruz. “Bu paket sayesinde API ile Application Domain arasındaki iletişimi, loosely coupled olarak tek bir noktadan gerçekleştirebileceğiz.” demişsiniz. Ancak zaten iletişimimiz interface’ler üzerinden kurarsak, Loosely coupled zaten sağlamış olmuyor muyuz?

      • Evet, interface’ler ilede de loosely coupled olarak gerçekleştirmiş oluyoruz ama “tek bir nokta” üzerinden değil. API tarafında her bir controller da ilgili service interface’i ve method’larını kullanmak yerine, bu işi bir mediator’a bırakmak daha temiz oluyor. En azından gerekli service’lere (use-case’lere) bizim için otomatik dispatch ediyor. En azından benim görüşürüm. Siz dilerseniz kendiniz de gerçekleştirebilirsiniz.

    • Merhaba, eğer interface’leri adapter’ler olarak koruyabilecekseniz neden olmasın. Maksat core kısmınızı olabildiğince diğer layer’lardan soyut ve encapsule edilmiş bir şekilde tutabilmek. Ayırdığınız da ise ne değişeceğini bir düşünün. Mesela core’u kullanmadan sadece “interface” leri kullanabileceğiniz yer varmı, gibi.

  3. Hüseyin Hüseyin

    Merhaba,
    Bir konuda mimari olarak fikrinizi almak isterim. Bu konuyla ilgi genel bir konsept var mı ondan da emin değilim 🙂 Mesala bir uygulama geliştirdiniz, yukarıda bahsettiğiniz gibi temiz bir şekilde dört dörtlük, generic ve birden fazla müşteride ortak özellikleriyle beraber kullanılabilecek bir self-service/portal uygulaması hazırladınız. Tabi ki müşteriler bir süre sonra sizden özel istekler isteyecek(benim temamda buralar şu renk olsun, margin-padding böyle olsun, inputlar böyle olsun, ek olarak şuraya da log atsın, SSO olsun vs) ve bunu ana projeye müşteri özelinde eklemek çok kötü bir development süreci ortaya getirecektir (if ekleyerek, areas ekleyerek vs burada bir çok kötü çözüm var 🙂 ). Bunun yerine ana uygulamamızı master branch üzerinde tutarak, müşteri özelinde özel istekleri olan projeler için master->customer yeni bir branch açıp, bu yeni branchi, o müşteri özelinde master gibi kullanarak projeye devam etmek sizce mantıklı mı? Bu kalıp ile ilerlendiğinde, bir bug keşfedildiğinde master branch üzerinde fix edilip customer brachler üzerinde pull request ile tek tek update işlemleri yapılabilir ve özelleştirmeler müşteri bazında ayrılabilir durumda oluyor.
    Burada da şöyle bir sorun var, 3, 5 müşteri için bu süreç yavaşta olsa yürütülebilir ikin 50 müşteri olduğunda 50 tane branch açıp manuel ilerlemek süreci çok zorlaştıracaktır. Bu durum ile ilgili örnek bir yaklaşım bir mimari var mıdır?
    Temelde varmak istediğim nokta müşteri branchlerini otomatize bir şekilde master üzerinden güncel tutabilmek.

    • Merhaba, açıkcası benim de pek sevmediğim bir durum. Her zaman arada bir synchronization problemi oluyor müşteri spesific işler olmaya başladıkça. Sanırım bunun için “Multi-tenant architecture” konusunu araştırmanız daha doğru olacaktır diye düşünüyorum.

  4. Mehmet Mehmet

    Merhaba, öncelikle elinize sağlık, çok net ve anlaşılır bir mimari olmuş. Bir sorum olacak.
    Infrastructure katmanında Repository ile EF i Core’dan soyutlamışsınız. Include gibi özel ihtiyaçlarımız olduğunda Repository’e Include() gibi yeni metotlar mı eklemeliyiz?

    Yada bunun yerine Infrastructure katmanında Repository ‘i kaldırıp entity bazlı olacak şekilde bir klasörde sorguları ayrı ayrımı oluşturup Core’a dönmeliyiz, siz ne yapardınız? Selamlar

    • Merhaba, çok güzel bir soru.

      Evet buradaki amacımız, olabildiğince “Core” katmanını teknoloji bağımlılığından uzak tutabilmek. Burada benimde izlediğim yol ise, IRepository interface’ini “genişletmek”. Örneğin business’ınız gerekği mongodb veya elasticsearch gibi teknolojileri kullanıyor olabilirsiniz ve normal bir read işleminin yerine, aggregation’lara ihtiyacınız olabilir (benim de içinde bulunduğum senaryo). Bunun için ilgili repository interface’lerinizi genişletmenizi tavsiye ederim.

  5. Berkay Doğukan Urhan Berkay Doğukan Urhan

    Merhaba

    Bu mimariyi yeni öğrenmeye çalışıyorum da anlamadığım bir nokta var. Mesela yeni bir sistem eklenecek diyelim yeni bir class libraryi kurup onun içinde mi geliştirme yapmam lazım yoksa infrastructure içindemi geliştirmem yapmam lazım yol gösterebilir misiniz?

    • Merhaba,

      Yeni bir sistemden kastınız nedir? Yeni bir datasource mu yoksa bir business mı? Eğer datasource ise evet, yeni bir class library açmanız gerekiyor. Örneğin Minimal.MongoDB.Infrastructure gibi. Her şey yeni bir adapter.

      • Berkay Doğukan Urhan Berkay Doğukan Urhan

        Şöyle anlatayım diyelim ki e ticaret sistemi yapıyorum ürün işlemleri-kullanıcı işlemleri-kargo işlemleri vb. durumlarım var. Business a dahil oluyor diye düşünüyorum. Bunları eklerken nasıl bir dosya/klasör/class library yapısı izlemeliyim tam kavrayamadığım için olayı sıkıntı yaşıyorum.

        • Merhaba, kusura bakmayın geç cevap için bir süre bakamadım yorumlara. Eğer monolith bir application geliştiriyorsanız ve bu makalede ele aldığım gibi use-case yaklaşımını kullanacaksanız, her bir base use-case için bir klasör oluşturabilirsiniz şuradaki resimde olduğu gibi. https://i1.wp.com/gokhan-gokalp.com/wp-content/uploads/2019/09/clean_usecases_2.jpg?ssl=1 “ProductUseCases” “UserUserCases” gibi. Altında da istediğiniz gibi klasör kırılımlarına gidebilirsiniz. Bu use-case’leriniz ise “Core” projesinde yer almalıdır.

  6. Yavuz Yavuz

    Merhaba

    Öncelikle emeğinize sağlık çok açık ve anlaşılır makaleler kaleme almışsınız. Genelde bu konuyla ilgili örnekler yapılırken basit bir veri çekme ya da kaydetme işlemleri anlatılıyor. Fakat karmaşık bir probleminiz olduğunda ne yapmamız gerekir. Şöyle ki; mediatr ile aldığımız nesneyi handlera aldık bununla beraber birden fazla hatta üç beş farklı veritabanı işlemi yapıp gelen veriden birden fazla tablodan birbirine bağlı verileri çekip aynı anda farklı insertleri aynı logic içerisinde yapmamız gereken durumlar olabiliyor. Böyle bir durumda tek bir handler içerisine tüm farklı veritabanı işlemlerini ve hesaplama işlemlerini mi yapmamız gerekiyor bu sefer handler çok fazla karmaşık hale gelecektir ya da nasıl bir yaklaşım izlemeliyiz?

    Teşekkürler

    • Merhaba yorumunuz için teşekkür ederim.

      Evet dediğiniz tarz ihtiyaçlar içerisinde bulunduğunuz business domain’e göre karşınıza gelebilir. Fakat bu ihtiyaç, biraz da mimarinizi nasıl tasarladığınız ile alakalı. Eğer bir microservice sistemi içerisinde iseniz zaten, ilgili service’lerin API’ları aracılığı ile ihtiyacınız olan verileri çekip, işleminizi ilgili handler içerisinde gerçekleştirebilirsiniz. Burada önemli olan çizgiyi, sınırı sizin nasıl çekeceğiniz. Eğer dağıtık bir transactional işlemler yapmak istiyorsanız da, bunu saga gibi farklı mekanizmalar ile çözmeniz gerekmektedir. Bir e-ticaret için örnek vermem gerekirse, eğer çekmek istediğim veri “sipariş” ve onun “ürünleri” ise, zaten bunlar benim için aynı domain sınırları içindedir ve ilgili sipariş’in ürünlerini de, “sipariş” handler’ı üzerinden çekerim onu bir aggregate root olarak görüp.

      Bu sorunun cevabı tamamen sizin sınırlarınızı belirlemenize ve sistemi nasıl modellediğinize dayanmaktadır…

      Teşekkürler.

  7. ali ali

    use case nerde? unit testler? ports, adaptors? 2 yazinda klasik mediatr yaklasimi gostermissin. alakasiz olmus biraz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.