Post #592
325

🔗Chain Of Responsibility
C# @csharpgeeks
Showing posts older than #593 · Back to latest


Forwarded from DotNet | دات نت
using System.Runtime.InteropServices;
// 1. تشخیص خودکار شناسه تایمزون بر اساس سیستمعامل
// Windows -> "Iran Standard Time"
// Linux/Docker -> "Asia/Tehran"
var tehranId = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "Iran Standard Time"
: "Asia/Tehran";
// 2. تنظیم ساعت به وقت ایران
RecurringJob.AddOrUpdate(
type.FullName,
() => job.ExecuteAsync(),
attribute.CronExpression,
new RecurringJobOptions
{
// خداحافظ UTC، سلام تهران! 👋
TimeZone = TimeZoneInfo.FindSystemTimeZoneById(tehranId)
}
);
وصل کردن همهچیز به هم (Wiring It All Together) 🔌
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect("<REDIS_CONNECTION_STRING>"));
// Register HybridCache (defaults generally work fine for L1)
builder.Services.AddHybridCache();
// Register our invalidation services
builder.Services.AddSingleton<ICacheInvalidator, RedisCacheInvalidator>();
builder.Services.AddHostedService<CacheInvalidationService>();
یک راه بهتر: FusionCache 🧠🔥
// Using FusionCache's implementation of HybridCache
builder.Services.AddFusionCache()
.WithBackplane(
new RedisBackplane(new RedisBackplaneOptions { Configuration = "<REDIS_CONNECTION_STRING>" }))
.AsHybridCache();
جمعبندی 📝
🔖هشتگها:
#DotNet #HybridCache #Redis #DistributedSystems #Caching #SystemDesign #FusionCache
پیادهسازی راهحل ⚙️
public interface ICacheInvalidator
{
Task InvalidateAsync(string key, CancellationToken cancellationToken = default);
}
public class RedisCacheInvalidator(
IConnectionMultiplexer connectionMultiplexer,
ILogger<RedisCacheInvalidator> logger)
: ICacheInvalidator
{
private const RedisChannel Channel = RedisChannel.Literal("cache-invalidation");
public async Task InvalidateAsync(string key, CancellationToken cancellationToken = default)
{
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.PublishAsync(Channel, new RedisValue(key));
logger.LogInformation("Published invalidation for key: {Key}", key);
}
}
public class UpdateUserProfileHandler(
AppDbContext dbContext,
ICacheInvalidator cacheInvalidator,
ILogger<UpdateUserProfileHandler> logger)
{
public async Task Handle(int userId, string newName, CancellationToken ct)
{
// 1. Update the database
var user = await dbContext.Users.FindAsync([userId], ct);
if (user is null)
{
return;
}
user.Name = newName;
await dbContext.SaveChangesAsync(ct);
// 2. Invalidate the cache (Distributed)
var cacheKey = $"user:{userId}";
await cacheInvalidator.InvalidateAsync(cacheKey, ct);
logger.LogInformation("Updated user and invalidated cache for {UserId}", userId);
}
}
Background Listener 🧵
public class CacheInvalidationService(
IConnectionMultiplexer connectionMultiplexer,
HybridCache hybridCache,
ILogger<CacheInvalidationService> logger)
: BackgroundService
{
private const RedisChannel Channel = RedisChannel.Literal("cache-invalidation");
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = connectionMultiplexer.GetSubscriber();
await subscriber.SubscribeAsync(Channel, (channel, value) =>
{
string key = value.ToString();
logger.LogInformation("Invalidating local cache for: {Key}", key);
// This removes the item from the local L1 cache
var task = hybridCache.RemoveAsync(key, stoppingToken);
if (!task.IsCompleted)
{
task.GetAwaiter().GetResult();
}
});
}
}
چرا فقط مدت زمان کش را کوتاه نکنیم؟ ⏱️🤔
این رویکرد دو مشکل جدید ایجاد میکند:
راهحل: Redis Pub/Sub بهعنوان Backplane 📡🧩
حل مشکل Invalidating کش توزیعشده با Redis و HybridCache 🧠🚀
اما یک نکته وجود دارد. ❗️
معضل کش توزیعشده (The Distributed Caching Dilemma)
builder.Property(x => x.Id)
.ValueGeneratedOnAdd();
سناریوی مشکلدار 🧨
public class Order
{
public int Id { get; private set; }
public Order()
{
if (Id <= 0)
throw new DomainException("Id must be positive");
}
}
set
{
if (value <= 0)
throw new DomainException();
_id = value;
}
دلیل واقعی چیـه؟ (رفتار داخلی EF) 🧠
Order.Id = -2147482647
✨️ءEF از این مقدار استفاده میکنه تا:
پس چرا برنامه کرش میکنه؟ 💣
💡نتیجه:
این دقیقاً چه درسی به ما میده؟ 🎯
راهحلهای درست (Professional Approaches) 🧩
// No validation on Id at all
public int Id { get; private set; }
Order.Id (int identity)
public bool IsPersisted => Id > 0;
نکتهی خیلی عمیق معماری 🧠🔥
جمعبندی طلایی 🏆
🔖هشتگها:
#EFCore #DDD #CleanArchitecture #ORM #DomainModel #SoftwareEngineering #DotNet
🧩 7️⃣ الگوی Sidecar (Sidecar Pattern) (بخش هفتم)
⚙️ نحوه کار (How it works):
✅ مزایا (Benefits):
⚠️ معایب (Drawbacks):
🎯 موارد استفاده (Use cases):


🔄 الگوی (Saga Pattern) Saga (بخش ششم)
⚙️ ءSaga چگونه کار میکند؟
🌟 مزایا (Benefits)
⚠️ معایب (Drawbacks)
🎯 موارد استفاده (Use cases)
✨ جمعبندی:

پند روز جمعه: 🔥 یه نظر متفاوت درباره «دوباره اختراع کردن چرخ»:
✨ چه اتفاقی میافته؟
🍳 بهش اینطوری فکر کن:
✅ بهترین روشها برای سازماندهی کدهای Extension در C# 14 🧩✨
🗂 چطور Extension Blockها را سازماندهی کنیم؟
public static class CollectionExtensions
{
extension<T>(IEnumerable<T> source)
{
public bool IsEmpty => !source.Any();
public bool HasItems => source.Any();
public int Count => source.Count();
}
extension(IEnumerable<string> source)
{
public string JoinWithComma() => string.Join(", ", source);
public IEnumerable<string> NonEmpty() => source.Where(s => !string.IsNullOrEmpty(s));
}
extension<T>(List<T> list)
{
public void AddIfNotExists(T item)
{
if (!list.Contains(item))
{
list.Add(item);
}
}
}
}
🔄 ترکیب syntax قدیمی و جدید
public static class StringExtensions
{
// Traditional syntax
public static bool IsEmail(this string value)
{
return value.Contains("@");
}
// New syntax
extension(string value)
{
public bool IsUrl =>
Uri.TryCreate(value, UriKind.Absolute, out _);
public string ToTitleCase() =>
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(value.ToLower());
}
}
🚫 از ساختن Extension Classهای غولپیکر اجتناب کنید
public static class ProductExtensions
{
extension(Product product)
{
// Product-specific extensions
}
}
public static class ValidationExtensions
{
extension(Order value)
{
// Validation logic
}
extension(OrderItem value)
{
// Validation logic
}
}
🧮 برای مقادیر محاسباتی از Extension Property استفاده کنید
extension(Order order)
{
public decimal TotalPrice =>
order.Items.Sum(item => item.Price * item.Quantity);
}
✍️ نامگذاری معنادار انتخاب کنید
// Good
public bool IsEmpty => !source.Any();
public string DisplayPrice => price.ToString("C");
📚 ءExtensionها را مستند کنید
extension(Product product)
{
/// <summary>
/// Gets whether the product is currently available for purchase.
/// </summary>
/// <remarks>
/// A product is available if its stock quantity is greater than zero.
/// </remarks>
public bool IsAvailable => product.StockQuantity > 0;
}
🧾 جمعبندی
🌍 مثالهای واقعی از Extension Memberها
public static class ApiHttpContextExtensions
{
extension(HttpContext context)
{
public string CorrelationId =>
context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
?? Guid.NewGuid().ToString();
public string ClientIp =>
context.Request.Headers["X-Forwarded-For"].FirstOrDefault()
?? context.Connection.RemoteIpAddress?.ToString()
?? "Unknown";
public bool IsApiRequest =>
context.Request.Path.StartsWithSegments("/api");
public string? GetBearerToken()
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.StartsWith("Bearer ") == true)
{
return authHeader.Substring("Bearer ".Length).Trim();
}
return null;
}
public T? GetQueryParameter<T>(string key)
{
if (context.Request.Query.TryGetValue(key, out var value))
{
try
{
return (T?)Convert.ChangeType(value.ToString(), typeof(T));
}
catch
{
return default;
}
}
return default;
}
public void AddResponseHeader(string key, string value)
{
context.Response.Headers[key] = value;
}
}
extension(HttpContext)
{
public static bool IsValidPath(string path) =>
!string.IsNullOrWhiteSpace(path) && path.StartsWith("/");
}
}
public class RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation(
"Request {Method} {Path} from {ClientIp} with CorrelationId {CorrelationId}",
context.Request.Method,
context.Request.Path,
context.ClientIp,
context.CorrelationId);
context.AddResponseHeader("X-Correlation-ID", context.CorrelationId);
await _next(context);
}
}
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet]
public IActionResult GetOrders(HttpContext httpContext)
{
var pageSize = httpContext.GetQueryParameter<int?>("pageSize") ?? 10;
var pageNumber = httpContext.GetQueryParameter<int?>("page") ?? 1;
var orders = orderService.GetPaged(pageNumber, pageSize);
return Ok(orders);
}
}
📌 نتیجه؟