Bu documentation, .NET projelerimde sıklıkla kullandığım temel kod parçalarını içermektedir.
- Entity Base Sınıfı
- SaveChanges Override
- Global Query Filters
- Password Hashing
- Validation Behavior
- Permission Behavior
- Exception Handler
- DbContext Configuration
- OData
Tüm entity'lerin türetileceği temel sınıf ve IdentityId value object'i içerir. Soft delete özelliği ve audit alanlarını yönetir.
public abstract class Entity
{
public Entity()
{
IdentityId identity = new(Guid.CreateVersion7());
Id = identity;
}
public IdentityId Id { get; private set; }
public IdentityId CreatedBy { get; private set; } = default!;
public DateTimeOffset CreatedAt { get; private set; } = default!;
public IdentityId? UpdatedBy { get; private set; }
public DateTimeOffset? UpdatedAt { get; private set; }
public IdentityId? DeletedBy { get; private set; }
public DateTimeOffset? DeletedAt { get; private set; }
public bool IsDeleted { get; private set; }
public void Delete()
{
DeletedAt = DateTimeOffset.Now;
IsDeleted = true;
}
}
public record IdentityId(Guid Value)
{
public static implicit operator Guid(IdentityId id) => id.Value;
public static implicit operator string(IdentityId id) => id.Value.ToString();
}DbContext'te SaveChangesAsync metodunu override ederek audit alanlarının otomatik doldurulmasını sağlar.
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var entries = ChangeTracker.Entries<Entity>();
HttpContextAccessor httpContextAccessor = new();
string userIdString =
httpContextAccessor
.HttpContext!
.User
.Claims
.First(p => p.Type == ClaimTypes.NameIdentifier)
.Value;
Guid userId = Guid.Parse(userIdString);
IdentityId identityId = new(userId);
foreach (var entry in entries)
{
if (entry.State == EntityState.Added)
{
entry.Property(p => p.CreatedAt)
.CurrentValue = DateTimeOffset.Now;
entry.Property(p => p.CreatedBy)
.CurrentValue = identityId;
}
if (entry.State == EntityState.Modified)
{
if (entry.Property(p => p.IsDeleted).CurrentValue == true)
{
entry.Property(p => p.DeletedAt)
.CurrentValue = DateTimeOffset.Now;
entry.Property(p => p.DeletedBy)
.CurrentValue = identityId;
}
else
{
entry.Property(p => p.UpdatedAt)
.CurrentValue = DateTimeOffset.Now;
entry.Property(p => p.UpdatedBy)
.CurrentValue = identityId;
}
}
if (entry.State == EntityState.Deleted)
{
throw new ArgumentException("Db'den direkt silme işlemi yapamazsınız");
}
}
return base.SaveChangesAsync(cancellationToken);
}Soft delete özelliğine sahip tüm entity'lere otomatik olarak query filter uygular.
public static class ExtensionMethods
{
public static void ApplyGlobalFilters(this ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var clrType = entityType.ClrType;
if (typeof(Entity).IsAssignableFrom(clrType))
{
var parameter = Expression.Parameter(clrType, "e");
var property = Expression.Property(parameter, nameof(IHasSoftDelete.IsDeleted));
var condition = Expression.Equal(property, Expression.Constant(false));
var lambda = Expression.Lambda(condition, parameter);
entityType.SetQueryFilter(lambda);
}
}
}
}Uygulamak için Dbcontext OnModelCreating'de bu metodu çağırıyoruz.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyGlobalFilters();
base.OnModelCreating(modelBuilder);
}Şifreleri güvenli bir şekilde hash'lemek için kullanılan Password value object'i.
public sealed record Password
{
private Password()
{
}
public Password(string password)
{
CreatePasswordHash(password);
}
public byte[] PasswordHash { get; private set; } = default!;
public byte[] PasswordSalt { get; private set; } = default!;
private void CreatePasswordHash(string password)
{
using var hmac = new System.Security.Cryptography.HMACSHA512();
PasswordSalt = hmac.Key;
PasswordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}User tablosunda çağırabilir bir metot olarak tasarlayıp şifre doğrulama için kullanıyorum.
public bool VerifyPasswordHash(string password)
{
using var hmac = new System.Security.Cryptography.HMACSHA512(Password.PasswordSalt);
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
return computedHash.SequenceEqual(Password.PasswordHash);
}MediatR pipeline'ında FluentValidation ile otomatik doğrulama yapan behavior.
public sealed class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (!_validators.Any())
{
return await next();
}
var context = new ValidationContext<TRequest>(request);
var errorDictionary = _validators
.Select(s => s.Validate(context))
.SelectMany(s => s.Errors)
.Where(s => s != null)
.GroupBy(
s => s.PropertyName,
s => s.ErrorMessage, (propertyName, errorMessage) => new
{
Key = propertyName,
Values = errorMessage.Distinct().ToArray()
})
.ToDictionary(s => s.Key, s => s.Values[0]);
if (errorDictionary.Any())
{
var errors = errorDictionary.Select(s => new ValidationFailure
{
PropertyName = s.Value,
ErrorCode = s.Key
});
throw new ValidationException(errors);
}
return await next();
}
}MediatR pipeline'ında yetki kontrolü yapan behavior.
public sealed class PermissionBehavior<TRequest, TResponse>(
IUserContext userContext,
IUserRepository userRepository) : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken = default)
{
var attr = request.GetType().GetCustomAttribute<PermissionAttribute>(inherit: true);
if (attr is null) return await next();
var userId = userContext.GetUserId();
var user = await userRepository.FirstOrDefaultAsync(p => p.Id == userId, cancellationToken);
if (user is null)
{
throw new ArgumentException("User bulunamadı");
}
// Eğer permission string'i varsa kontrol et
if (!string.IsNullOrEmpty(attr.Permission))
{
var hasPermission = user.Permissions.Any(p => p.Name == attr.Permission);
if (!hasPermission)
{
throw new AuthorizationException($"'{attr.Permission}' yetkisine sahip değilsiniz.");
}
}
// Eğer permission string'i yoksa sadece admin kontrolü yap
else if (!user.IsAdmin.Value)
{
throw new AuthorizationException("Bu işlem için admin yetkisi gereklidir.");
}
return await next();
}
}
public sealed class PermissionAttribute : Attribute
{
public string? Permission { get; }
public PermissionAttribute()
{
}
public PermissionAttribute(string permission)
{
Permission = permission;
}
}
public sealed class AuthorizationException : Exception
{
public AuthorizationException() : base("Yetkiniz bulunmamaktadır.")
{
}
public AuthorizationException(string message) : base(message)
{
}
}Kullanmak istediğiniz CQRS Request classınızın üstünde çağırıyorsunuz.
[Permission]
public sealed record DeveloperCreateCommand(
string Name
) : IRequest<Result<string>>;
[Permission("permission.create")]
public sealed record DeveloperCreateCommand(
string Name
) : IRequest<Result<string>>;Global exception handling için kullanılan handler.
public sealed class ExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
{
Result<string> errorResult;
httpContext.Response.ContentType = "application/json";
httpContext.Response.StatusCode = 500;
var actualException = exception is AggregateException agg && agg.InnerException != null
? agg.InnerException
: exception;
var exceptionType = actualException.GetType();
var validationExceptionType = typeof(ValidationException);
var authorizationExceptionType = typeof(AuthorizationException);
if (exceptionType == validationExceptionType)
{
httpContext.Response.StatusCode = 422;
errorResult = Result<string>.Failure(422, ((ValidationException)exception).Errors.Select(s => s.PropertyName).ToList());
await httpContext.Response.WriteAsJsonAsync(errorResult);
return true;
}
if (exceptionType == authorizationExceptionType)
{
httpContext.Response.StatusCode = 403;
errorResult = Result<string>.Failure(403, "Bu işlem için yetkiniz yok");
await httpContext.Response.WriteAsJsonAsync(errorResult);
return true;
}
errorResult = Result<string>.Failure(exception.Message);
await httpContext.Response.WriteAsJsonAsync(errorResult);
return true;
}
}IdentityId value object'i için value converter configurationu.
internal sealed class IdentityIdValueConverter : ValueConverter<IdentityId, Guid>
{
public IdentityIdValueConverter() : base(m => m.Value, m => new IdentityId(m)) { }
}
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<IdentityId>().HaveConversion<IdentityIdValueConverter>();
base.ConfigureConventions(configurationBuilder);
}OData yı kullanmak istiyorsak yapmamız gereken IoC service registration işlemi ve örnek controller
IoC Container (program.cs)
builder.Services
.AddControllers()
.AddOData(opt =>
opt.Select()
.Filter()
.Count()
.Expand()
.OrderBy()
.SetMaxTop(null)
);OData Controller
[Route("api/[controller]")]
[ApiController]
[EnableQuery]
public class ODataController : ControllerBase
{
public static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new();
builder.EnableLowerCamelCase();
//builder.EntitySet<UserResponse>("users");
return builder.GetEdmModel();
}
}- Bu kod parçaları Clean Architecture ve DDD prensipleri göz önünde bulundurularak hazırlanmıştır
- Entity Framework Core kullanımı için optimize edilmiştir
- MediatR kütüphanesi ile CQRS pattern'i uygulamalarını destekler
- Soft delete özelliği varsayılan olarak tüm entity'lerde aktiftir
- Audit alanları otomatik olarak doldurulur
- Permission attribute ile method bazlı yetkilendirme yapılabilir