Neden .NET & C#?
Microsoft'un açık kaynak, cross-platform çalışma zamanı olan .NET, tip güvenli C# dili ile birleşerek kurumsal düzeyde uygulamalar için sağlam bir temel sunar. AOT derleme, gRPC desteği, minimal API'ler ve native interop gibi özelliklerle modern backend ihtiyaçlarını karşılar. Clean Architecture, CQRS ve Domain-Driven Design gibi mimari örüntüleri .NET ekosistemi içinde birinci sınıf destekle hayata geçirebilirsiniz.
- Tip güvenli, derlenen dil — çalışma zamanı hatalarını minimize eder
- AOT ve JIT derleme seçenekleri ile üstün performans
- MediatR, CQRS ve Vertical Slice mimarisi desteği
- Azure, AWS ve on-premise dağıtım esnekliği
- Mikroservis ve monolith mimarilerde eşit yetkinlik
- Güçlü IDE desteği: Visual Studio, JetBrains Rider
Gerçek Kod Örnekleri
// Application/Orders/Commands/CreateOrderCommand.cs
using MediatR;
using FluentValidation;
using Cofreex.Domain.Entities;
using Cofreex.Application.DTOs;
namespace Cofreex.Application.Orders.Commands;
// ── Command ─────────────────────────────────────────────────
public sealed record CreateOrderCommand(
Guid CustomerId,
IReadOnlyList<OrderLineDto> Lines,
string ShippingAddress,
string Currency = "TRY"
) : IRequest<Result<OrderDto>>;
// ── Handler ─────────────────────────────────────────────────
public sealed class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, Result<OrderDto>>
{
private readonly IOrderRepository _orders;
private readonly ICustomerRepository _customers;
private readonly IUnitOfWork _uow;
private readonly IPublisher _publisher;
private readonly IMapper _mapper;
public CreateOrderCommandHandler(
IOrderRepository orders,
ICustomerRepository customers,
IUnitOfWork uow,
IPublisher publisher,
IMapper mapper)
{
_orders = orders;
_customers = customers;
_uow = uow;
_publisher = publisher;
_mapper = mapper;
}
public async Task<Result<OrderDto>> Handle(
CreateOrderCommand request,
CancellationToken ct)
{
var customer = await _customers.FindAsync(request.CustomerId, ct);
if (customer is null)
return Result.Failure<OrderDto>(CustomerErrors.NotFound(request.CustomerId));
var order = Order.Create(
customer: customer,
shippingAddress: request.ShippingAddress,
currency: request.Currency);
foreach (var line in request.Lines)
{
var addResult = order.AddLine(line.ProductId, line.Quantity, line.UnitPrice);
if (addResult.IsFailure)
return Result.Failure<OrderDto>(addResult.Error);
}
await _orders.AddAsync(order, ct);
await _uow.SaveChangesAsync(ct);
// Domain event fırlatılıyor
await _publisher.Publish(new OrderCreatedEvent(order.Id), ct);
return Result.Success(_mapper.Map<OrderDto>(order));
}
}
// ── FluentValidation Validator ───────────────────────────────
public sealed class CreateOrderCommandValidator
: AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty().WithMessage("Müşteri ID boş olamaz.");
RuleFor(x => x.ShippingAddress)
.NotEmpty().WithMessage("Teslimat adresi zorunludur.")
.MaximumLength(500);
RuleFor(x => x.Currency)
.Must(c => new[] { "TRY", "USD", "EUR" }.Contains(c))
.WithMessage("Geçerli bir para birimi seçiniz.");
RuleFor(x => x.Lines)
.NotEmpty().WithMessage("Sipariş en az bir ürün içermelidir.")
.Must(l => l.Count <= 100).WithMessage("Tek siparişte en fazla 100 ürün olabilir.");
RuleForEach(x => x.Lines).SetValidator(new OrderLineDtoValidator());
}
}
public sealed class OrderLineDtoValidator : AbstractValidator<OrderLineDto>
{
public OrderLineDtoValidator()
{
RuleFor(x => x.ProductId).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(9999);
RuleFor(x => x.UnitPrice).GreaterThan(0);
}
}
// ── DI Kaydı (Program.cs) ────────────────────────────────────
// builder.Services.AddMediatR(cfg =>
// {
// cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly);
// cfg.AddBehavior<IPipelineBehavior<,>>, ValidationBehavior<,>>();
// cfg.AddBehavior<IPipelineBehavior<,>>, LoggingBehavior<,>>();
// cfg.AddBehavior<IPipelineBehavior<,>>, TransactionBehavior<,>>();
// });
// builder.Services.AddValidatorsFromAssembly(typeof(CreateOrderCommandValidator).Assembly);
// Api/Controllers/OrdersController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MediatR;
using Cofreex.Application.Orders.Commands;
using Cofreex.Application.Orders.Queries;
using Cofreex.Application.DTOs;
namespace Cofreex.Api.Controllers;
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[Authorize]
[Produces("application/json")]
public sealed class OrdersController : ControllerBase
{
private readonly ISender _sender;
private readonly ILogger<OrdersController> _logger;
public OrdersController(ISender sender, ILogger<OrdersController> logger)
{
_sender = sender;
_logger = logger;
}
/// <summary>Siparişleri sayfalı olarak listeler.</summary>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<OrderSummaryDto>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetOrders(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? status = null,
[FromQuery] string? sort = "createdAt_desc",
CancellationToken ct = default)
{
var query = new GetOrdersQuery(page, pageSize, status, sort);
var result = await _sender.Send(query, ct);
return result.IsSuccess
? Ok(result.Value)
: BadRequest(result.Error);
}
/// <summary>Belirtilen ID'ye ait siparişi getirir.</summary>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetOrder(Guid id, CancellationToken ct)
{
var result = await _sender.Send(new GetOrderByIdQuery(id), ct);
return result.IsSuccess
? Ok(result.Value)
: NotFound(new ProblemDetails { Title = "Sipariş bulunamadı.", Detail = result.Error.Message });
}
/// <summary>Yeni sipariş oluşturur.</summary>
[HttpPost]
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status422UnprocessableEntity)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateOrder(
[FromBody] CreateOrderRequest request,
CancellationToken ct = default)
{
var customerId = User.GetCustomerId(); // JWT claim extension
var command = new CreateOrderCommand(
CustomerId: customerId,
Lines: request.Lines,
ShippingAddress: request.ShippingAddress,
Currency: request.Currency);
var result = await _sender.Send(command, ct);
if (result.IsFailure)
{
_logger.LogWarning("Sipariş oluşturulamadı. CustomerId={CustomerId} Hata={Error}",
customerId, result.Error);
return UnprocessableEntity(result.Error);
}
return CreatedAtAction(
nameof(GetOrder),
new { id = result.Value.Id, version = "1.0" },
result.Value);
}
/// <summary>Siparişi iptal eder.</summary>
[HttpPost("{id:guid}/cancel")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IActionResult> CancelOrder(Guid id, [FromBody] CancelOrderRequest request, CancellationToken ct)
{
var result = await _sender.Send(new CancelOrderCommand(id, request.Reason), ct);
return result.IsSuccess
? NoContent()
: Conflict(new ProblemDetails { Title = "Sipariş iptal edilemez.", Detail = result.Error.Message });
}
/// <summary>Siparişe ait ürün kalemlerini günceller.</summary>
[HttpPatch("{id:guid}/lines")]
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> UpdateOrderLines(
Guid id,
[FromBody] UpdateOrderLinesRequest request,
CancellationToken ct)
{
var command = new UpdateOrderLinesCommand(id, request.Lines);
var result = await _sender.Send(command, ct);
return result.IsSuccess ? Ok(result.Value) : UnprocessableEntity(result.Error);
}
}
// Infrastructure/Persistence/OrderDbContext.cs
using Microsoft.EntityFrameworkCore;
using Cofreex.Domain.Entities;
using Cofreex.Application.Interfaces;
namespace Cofreex.Infrastructure.Persistence;
public sealed class OrderDbContext : DbContext, IUnitOfWork
{
private readonly ICurrentUserService _currentUser;
private readonly IDateTimeProvider _dateTime;
public OrderDbContext(
DbContextOptions<OrderDbContext> options,
ICurrentUserService currentUser,
IDateTimeProvider dateTime) : base(options)
{
_currentUser = currentUser;
_dateTime = dateTime;
}
public DbSet<Order> Orders { get; init; } = null!;
public DbSet<OrderLine> OrderLines { get; init; } = null!;
public DbSet<Customer> Customers { get; init; } = null!;
public DbSet<Product> Products { get; init; } = null!;
protected override void OnModelCreating(ModelBuilder mb)
{
mb.ApplyConfigurationsFromAssembly(typeof(OrderDbContext).Assembly);
base.OnModelCreating(mb);
}
public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
{
// Audit fields otomatik doldurulur
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedBy = _currentUser.UserId;
entry.Entity.CreatedAt = _dateTime.UtcNow;
break;
case EntityState.Modified:
entry.Entity.UpdatedBy = _currentUser.UserId;
entry.Entity.UpdatedAt = _dateTime.UtcNow;
break;
}
}
return await base.SaveChangesAsync(ct);
}
}
// Infrastructure/Persistence/Configurations/OrderConfiguration.cs
public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.OrderNumber)
.HasMaxLength(20)
.IsRequired();
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(30)
.IsRequired();
builder.OwnsOne(o => o.ShippingAddress, sa =>
{
sa.Property(a => a.Street).HasMaxLength(250).IsRequired();
sa.Property(a => a.City).HasMaxLength(100).IsRequired();
sa.Property(a => a.PostalCode).HasMaxLength(10);
});
builder.HasMany(o => o.Lines)
.WithOne(l => l.Order)
.HasForeignKey(l => l.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasIndex(o => o.OrderNumber).IsUnique();
builder.HasIndex(o => new { o.CustomerId, o.Status });
}
}
// Infrastructure/Persistence/BaseRepository.cs
public abstract class BaseRepository<TEntity, TId>
where TEntity : Entity<TId>
where TId : notnull
{
protected readonly OrderDbContext _ctx;
protected readonly DbSet<TEntity> _set;
protected BaseRepository(OrderDbContext ctx)
{
_ctx = ctx;
_set = ctx.Set<TEntity>();
}
public async Task<TEntity?> FindAsync(TId id, CancellationToken ct = default)
=> await _set.FindAsync(new object[] { id }, ct);
public async Task<IReadOnlyList<TEntity>> FindBySpecAsync(
ISpecification<TEntity> spec,
CancellationToken ct = default)
{
return await ApplySpec(_set.AsQueryable(), spec).ToListAsync(ct);
}
public async Task<int> CountAsync(ISpecification<TEntity> spec, CancellationToken ct = default)
=> await ApplySpec(_set.AsQueryable(), spec).CountAsync(ct);
public async Task AddAsync(TEntity entity, CancellationToken ct = default)
=> await _set.AddAsync(entity, ct);
public void Update(TEntity entity)
=> _ctx.Entry(entity).State = EntityState.Modified;
public void Remove(TEntity entity)
=> _set.Remove(entity);
private static IQueryable<TEntity> ApplySpec(
IQueryable<TEntity> query,
ISpecification<TEntity> spec)
{
if (spec.Criteria is not null) query = query.Where(spec.Criteria);
if (spec.OrderBy is not null) query = query.OrderBy(spec.OrderBy);
if (spec.OrderByDesc is not null) query = query.OrderByDescending(spec.OrderByDesc);
query = spec.Includes.Aggregate(query, (q, inc) => q.Include(inc));
if (spec.IsPagingEnabled)
query = query.Skip(spec.Skip).Take(spec.Take);
return query;
}
}
// Konkret repository
public sealed class OrderRepository : BaseRepository<Order, Guid>, IOrderRepository
{
public OrderRepository(OrderDbContext ctx) : base(ctx) { }
public async Task<Order?> FindWithLinesAsync(Guid id, CancellationToken ct)
=> await _set
.Include(o => o.Lines).ThenInclude(l => l.Product)
.Include(o => o.Customer)
.FirstOrDefaultAsync(o => o.Id == id, ct);
}
// Api/Middleware/CorrelationIdMiddleware.cs
namespace Cofreex.Api.Middleware;
public sealed class CorrelationIdMiddleware : IMiddleware
{
private const string HeaderName = "X-Correlation-Id";
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
var correlationId = ctx.Request.Headers.TryGetValue(HeaderName, out var existing)
? existing.ToString()
: Guid.NewGuid().ToString("N");
ctx.Items["CorrelationId"] = correlationId;
ctx.Response.Headers.TryAdd(HeaderName, correlationId);
// Serilog log context'ine ekle — tüm log satırlarına otomatik eklenir
using (LogContext.PushProperty("CorrelationId", correlationId))
using (LogContext.PushProperty("UserId", ctx.User.FindFirst("sub")?.Value ?? "anonymous"))
using (LogContext.PushProperty("ClientIp", ctx.Connection.RemoteIpAddress?.ToString()))
{
await next(ctx);
}
}
}
// Api/Middleware/GlobalExceptionHandlerMiddleware.cs
public sealed class GlobalExceptionHandlerMiddleware : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
public GlobalExceptionHandlerMiddleware(ILogger<GlobalExceptionHandlerMiddleware> logger)
=> _logger = logger;
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken ct)
{
var (statusCode, title, detail) = exception switch
{
DomainException ex => (422, "İş Kuralı İhlali", ex.Message),
NotFoundException ex => (404, "Kaynak Bulunamadı", ex.Message),
UnauthorizedException => (401, "Yetkisiz Erişim", "Bu işlem için yetkiniz yok."),
ValidationException ex=> (422, "Doğrulama Hatası", FormatValidationErrors(ex)),
_ => (500, "Sunucu Hatası", "Beklenmeyen bir hata oluştu.")
};
var correlationId = httpContext.Items["CorrelationId"]?.ToString();
if (statusCode == 500)
_logger.LogError(exception,
"İşlenmeyen istisna. CorrelationId={CorrelationId} Path={Path}",
correlationId,
httpContext.Request.Path);
else
_logger.LogWarning(
"İstek hatası. Status={Status} Title={Title} CorrelationId={CorrelationId}",
statusCode, title, correlationId);
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = title,
Detail = detail,
Instance = httpContext.Request.Path
};
problemDetails.Extensions["correlationId"] = correlationId;
problemDetails.Extensions["traceId"] = Activity.Current?.Id ?? httpContext.TraceIdentifier;
httpContext.Response.StatusCode = statusCode;
httpContext.Response.ContentType = "application/problem+json";
await httpContext.Response.WriteAsJsonAsync(problemDetails, ct);
return true;
}
private static string FormatValidationErrors(ValidationException ex)
=> string.Join("; ", ex.Errors.Select(e => $"{e.PropertyName}: {e.ErrorMessage}"));
}
// Program.cs — Serilog konfigürasyonu
// Log.Logger = new LoggerConfiguration()
// .MinimumLevel.Information()
// .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
// .Enrich.FromLogContext()
// .Enrich.WithMachineName()
// .Enrich.WithEnvironmentName()
// .WriteTo.Console(new JsonFormatter())
// .WriteTo.Seq("http://seq:5341")
// .WriteTo.File(
// new CompactJsonFormatter(),
// "logs/app-.log",
// rollingInterval: RollingInterval.Day,
// retainedFileCountLimit: 30)
// .CreateLogger();
//
// builder.Host.UseSerilog();
//
// app.UseMiddleware<CorrelationIdMiddleware>();
// app.UseExceptionHandler();
// Pipeline Behavior — her request için otomatik transaction
public sealed class TransactionBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _uow;
public TransactionBehavior(IUnitOfWork uow) => _uow = uow;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
// Query'lerde transaction başlatma
if (request is IQuery)
return await next();
await using var tx = await _uow.BeginTransactionAsync(ct);
try
{
var response = await next();
await tx.CommitAsync(ct);
return response;
}
catch
{
await tx.RollbackAsync(ct);
throw;
}
}
}
Kullandığımız Kütüphaneler
Command/Query ayrımı, pipeline behavior'lar ve domain event yayımı için kullanılan in-process mediator kütüphanesi. ValidationBehavior ve LoggingBehavior ile genişletilir.
LINQ tabanlı, code-first ORM. Owned types, table splitting, raw SQL ve compiled query desteğiyle kompleks veri modellerini yönetir. Migration ile schema senkronizasyonu.
Strongly-typed doğrulama kuralları. MediatR pipeline'a entegre edilerek her command/query otomatik valide edilir. Custom validator ve cross-field kural desteği.
Yapısal (structured) loglama. Log context enricher'lar, Seq/Elastic sink'leri ve request/response logging middleware ile merkezi log yönetimi sağlar.
Arka plan görevleri, zamanlanmış işler (cron) ve yeniden deneme (retry) politikaları. Dashboard UI ile iş kuyruklarını görsel olarak izleyin.
WebSocket tabanlı çift yönlü iletişim. Hub soyutlaması, grup yönetimi ve Azure SignalR Service entegrasyonu ile ölçeklenebilir gerçek zamanlı bildirimler.
Domain entity'lerini DTO'lara dönüştürmek için profile tabanlı mapping. Nested mapping, custom converter ve value resolver desteğiyle esnek nesne dönüşümü.
Retry, circuit breaker, timeout, bulkhead ve fallback politikaları. HttpClient factory ile entegre — dış servis çağrılarında dayanıklılığı garanti eder.
Hangi Projelerde Kullanıyoruz?
Stok yönetimi, muhasebe, İK ve satış modüllerini entegre eden modüler monolith veya mikroservis tabanlı ERP çözümleri. RBAC, raporlama ve BI entegrasyonu.
IHostedService ve BackgroundService ile arka planda sürekli çalışan iş süreçleri. Windows Service veya Linux systemd uyumlu daemon uygulamaları.
Azure Service Bus, Blob Storage, Cosmos DB ve Azure AD B2C entegrasyonlu cloud-native uygulamalar. Managed Identity ile güvenli kaynak erişimi.
WPF ile Windows masaüstü, MAUI ile iOS/Android/macOS/Windows çapraz platform uygulamaları. MVVM pattern ve DI ile test edilebilir desktop yazılımı.
Minimal API veya controller tabanlı ASP.NET Core endpoint'leri. Response caching, rate limiting, gRPC ve output caching ile düşük gecikme süreli API'ler.
Docker ve Kubernetes üzerinde çalışan servisler arası gRPC/REST iletişim, Dapr entegrasyonu ve event-driven mimari ile ölçeklenebilir dağıtık sistemler.
.NET ile Kurumsal Projenizi Hayata Geçirelim
Clean Architecture, CQRS ve mikroservis konusundaki derin deneyimimizle ölçeklenebilir, bakımı kolay ve test edilebilir .NET çözümleri geliştiriyoruz.