If you haven’t read the first part of this article, you can reach it from here for better understanding of the subject.
In this second part of this article, I will mention about the minimal implementation of the clean architecture concept with .NET Core.
As an example, we will develop a simple API which we can use to add and list movies.
First, we will create the application domain part which will be the hearth of the architecture.
Before that, let’s recall the “Application Domain“. This layer is located in the middle of the architecture and is isolated from other frameworks, databases, UI, etc. It mainly contains domain entities, use-cases and external interfaces.
Well, first we need to create a class library called “Minimal.Core”.
dotnet new classlib -n Minimal.Core
After that, let’s create a folder called “Models” then define domain models as follows.
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; } } }
After creating the models, we need to create a folder called “Interfaces” where we will define ports. Then, let’s define an interface/port called “IRepository
“.
With this port, we will be able to perform isolated database operations in the “Application Domain” layer without depending on any technologies.
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); } }
After defining the port, we also need to define the data transfer objects to be used between adapters.
To do this, let’s create another folder called “Dtos“. Then, in this folder, we can define “BaseResponse
” and “Movie
” DTOs.
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; } } }
We will create request models under another folder called “Requests“. Therefore, let’s create a folder called “Requests” under the “Dtos” folder.
Minimal.Core
-Models
-Interfaces
-Dtos
--Requests
Before creating request models, first we need to include the MediatR package in the project via NuGet.
dotnet add package MediatR
With this package, we will be able to perform communication between API and Application Domain in a loosely coupled way from single point. In addition, we will also benefit from MediatR package when implementing the use-case approach.
NOTE: If you want, you can also define and implement your own use-case interfaces without using any packages.
Now, in the “Requests” folder, we can define request models which we will use for movie operations. Let’s define the “CreateMovieRequest
” and “GetBestMoviesForKidsRequest
” models as follows.
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>>> { } }
We have created the models together with its return types by using the “IRequest” marker interface of MediatR.
Until this point, we have defined “Domain” models, “DTOs” and external interfaces, I mean “Ports“.
So, we can start to implement business use-cases. First, let’s create “Services/MovieUseCases” folders under the “Minimal.Core” project.
Minimal.Core
-Models
-Interfaces
-Dtos
--Requests
-Services
--MovieUseCases
In this folder, we will implement all movie-related use-cases.
Now, under the “MovieUseCases” folder, let’s create a class called “CreateMovieHandler
” and implement the movie creation operation as follows.
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; } } }
Let’s take a look at what we are doing.
First of all, by implementing the “IRequestHandler
” interface, we have specified that the “CreateMovieHandler
” class is a use-case request handler. This handler basically handles the “CreateMovieRequest
” model and returns a response model type of “BaseResponseDto
“.
We implemented the business use-case as dummy in the “Handle
” method. The important point here is the usage style of the “IRepository
” interface.
If we look at the above code block, we can see the “IRepository
” interface is located in the “Minimal.Core.Interfaces” namespace and not in any other layer/adapter. I mean, we are using the external port that we defined in the “Minimal.Core” project.
This means the “CreateMovieHandler
” use-case will not be affected by any technology changes.
Now let’s implement the use-case, which will return the best movies for children. To do that, let’s create another class called “GetBestMoviesForKidsHandler
” and implement it as follows.
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; } } }
In this use-case, we have coded a logic which simply returns movies that are best for children. According to our example, the application domain layer is now ready.
Now let’s pass to the infrastructure section where we will perform database operations.
First, let’s create a class library called “Minimal.Infrastructure“.
dotnet new classlib -n Minimal.Infrastructure
After that, we need to add “Minimal.Core” project as reference.
Since we will use EntityFrameworkCore as ORM, we need to add also “Microsoft.EntityFrameworkCore” package to the project via NuGet.
If we remember in the “Minimal.Core” project, we had defined a port called “IRepository
” to reverse the dependency flow. In the infrastructure, we will implement this port.
First we need a DbContext. To do that, let’s define a class called “AppDbContext
” and code as follows.
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); //... } } }
Now we can implement the repository class.
To do this, we need to create a folder called “Repositories” and then implement the “Repository
” class in this folder as follows.
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); } } }
At the above code block, we have implemented a simple generic repository by using the db context which we created. Thus, we have completed the implementation of the “Infrastructure” adapter.
Lastly, we need a communication adapter/API.
If we look from the architecture point of view, we can see the implementation operation of the API is not different from the implementation of other adapters. If we recall, we had said that we can think for hexagonal architecture as plug-in-based structure, which locates the core layer in the middle of the architecture, which we can plug an adapter, or out.
For implementation, first, let’s create an ASP.NET Core Empty Web project.
dotnet new web -n Minimal.API
Then, let’s add application domain layer and infrastructure adapter as reference.
dotnet add reference ../Minimal.Core/Minimal.Core.csproj dotnet add reference ../Minimal.Infrastructure/Minimal.Infrastructure.csproj
We had benefited from the MediatR library while implementing business use-cases in the application domain. With this approach, we had mentioned that we can perform the communication between API and application domain in a loosely coupled way from single point.
To do this, we have to include “MediatR” and “MediatR.Extensions.Microsoft.DependencyInjection” packages in the API project via NuGet.
dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Now we are ready for implementation.
Now let’s create an API controller class called “MoviesController
” under the “Controllers” folder and then code as follows.
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); } } } }
In the above code block, we have implemented the “CreateMovieAsync
” method, which we will use to create a movie, and the “GetBestMoviesForKidsAsync
” method, which we will use to get best movies for children.
In the methods, we have set only the relevant request models, which we want it to handle, as parameters to the mediator as follows.
await _mediator.Send(createMovieRequest); await _mediator.Send(new GetBestMoviesForKidsRequest());
Mediator will find the relevant request handlers for us and execute the request models. Performing these operations from single point is good approach, right?
Now let’s perform the injection operations in the “Startup
” class.
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; 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(); } } }
In the “Startup
” class, we specified that we will use an in-memory database for testing purpose. Then we performed the injection operations of the “IRepository
” interface and MeaditR library.
Now, this adapter is also ready.
Thus, we can say that we completed the implementation operation of this example project with clean architecture.
Especially if we have got used to a dependency flow from top to bottom, it may seem a bit strange to define the ports (I mean, interfaces) in the application domain, and then implement their adapters around the architecture. After getting used to this architecture, we can see how this approach gives us speed and flexibility in the face of future changes/new features, testability and maintenance.
For example, let’s assume we decided to use a NoSQL technology as the database. All we need to do is create an adapter that implements the “IRepository
” port, and then perform the necessary injection operations.
Because, by nature of the architecture we are building an inner/core layer with no dependencies on the outside world.
We equip the around of the core layer with replaceable adapters.
https://github.com/GokGokalp/CleanArchitectureBoilerplates/tree/master/src/MinimalCleanArchitecture
{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…
{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…
{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…
{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…
{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…
{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…
View Comments
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.
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ı?
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.
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.
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.
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.
Şö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.
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.
use case nerde? unit testler? ports, adaptors? 2 yazinda klasik mediatr yaklasimi gostermissin. alakasiz olmus biraz.