C# Geeks (.NET) – Telegram
🧱 Partial Constructors و Events

در ‎C# 14‎ پشتیبانی از Partial Members گسترش یافته و حالا شامل Constructors و Events هم می‌شود.
این قابلیت به‌ویژه برای Source Generator‌ها بسیار مفید است ⚙️

🏗 Partial Constructors

یک ‎Partial Constructor‎ باید شامل یک بخش تعریف‌کننده (Defining Declaration) و یک بخش پیاده‌ساز (Implementing Declaration) باشد.

🔹 مثال:
public partial class User
{
// این بخش تعریف‌کننده است
public partial User(string name);
}

public partial class User
{
// این بخش پیاده‌ساز است
public partial User(string name)
: this() // فراخوانی یک سازنده دیگر در همان کلاس
{
Name = name;
}

public User() { }

public string Name { get; set; }
}


📘 قوانین مربوط به Partial Constructor‌ها:

فقط بخش پیاده‌ساز (implementing part) می‌تواند از ‎this()‎ یا ‎base()‎ برای فراخوانی یک constructor دیگر استفاده کند.

فقط یکی از بخش‌های کلاس می‌تواند از Primary Constructor استفاده کند.

🔧 این ویژگی در پروژه‌هایی که به صورت خودکار کد تولید می‌کنند (مانند source generators)، انعطاف زیادی ایجاد می‌کند و اجازه می‌دهد بخشی از logic در runtime تولید یا تزریق شود.

⚡️ Partial Events

همچنین، ‎Partial Events‎ نیز نیاز به دو بخش دارند — یکی تعریف‌کننده (Defining Declaration) و دیگری پیاده‌ساز (Implementing Declaration).

🔹 مثال:
public partial class Downloader
{
public partial event Action<string> DownloadCompleted;
}

public partial class Downloader
{
public partial event Action<string> DownloadCompleted
{
add { }
remove { }
}
}

📘 در اینجا بخش پیاده‌ساز باید شامل هر دو accessor یعنی ‎add‎ و ‎remove‎ باشد.

این قابلیت به ویژه در سناریوهایی مفید است که بخشی از تعریف event توسط ابزار یا generator تولید می‌شود و بخش دیگر توسط توسعه‌دهنده پیاده‌سازی می‌گردد.

🔗 برای مشاهده‌ی فهرست کامل ویژگی‌های جدید C# 14، می‌توانید به مستندات رسمی Microsoft مراجعه کنید.
🚀 جدیدترین قابلیت‌ها در ASP.NET Core نسخه‌ی ‎.NET 10‎


Validation Support در Minimal APIs
در ‎ASP.NET Core 10‎ پشتیبانی داخلی از Validation برای Minimal APIها اضافه شده است.
این ویژگی داده‌هایی که به endpointهای شما ارسال می‌شوند — شامل query parameters، headers و request bodies را به‌صورت خودکار اعتبارسنجی می‌کند.

برای فعال‌سازی، کافی است سرویس validation را ثبت کنید:
builder.Services.AddValidation();

سیستم validation به‌صورت خودکار نوع‌هایی را که در handlerهای Minimal API شما استفاده شده‌اند شناسایی می‌کند و با استفاده از attributeهای موجود در فضای نام ‎System.ComponentModel.DataAnnotations‎ آن‌ها را اعتبارسنجی می‌کند.

می‌توانید attributeهای اعتبارسنجی را مستقیماً روی پارامترهای endpoint اعمال کنید:
app.MapPost("/products",
([Range(1, int.MaxValue)] int productId, [Required] string name) =>
{
return TypedResults.Ok(new { productId, name });
});

نوع‌های record نیز با validation کار می‌کنند:
public record Product(
[Required] string Name,
[Range(1, 1000)] int Quantity);

app.MapPost("/products", (Product product) =>
{
return TypedResults.Ok(product);
});

وقتی اعتبارسنجی شکست بخورد، runtime به‌صورت خودکار پاسخ 400 Bad Request را همراه با جزئیات خطاهای validation بازمی‌گرداند.

می‌توانید برای endpointهای خاص، validation را غیرفعال کنید:
app.MapPost("/products", (int productId, string name) =>
TypedResults.Ok(productId))
.DisableValidation();

برای شخصی‌سازی پاسخ‌های خطا، می‌توانید رابط ‎IProblemDetailsService‎ را پیاده‌سازی کرده و در Dependency Injection Container ثبت کنید.
این کار امکان تولید پیام‌های خطای یکپارچه و کاربرپسند در کل برنامه را فراهم می‌کند.

🧩 JSON Patch Support در Minimal APIs

برای فعال‌سازی پشتیبانی از JSON Patch با ‎System.Text.Json‎، بسته‌ی زیر را نصب کنید:
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease


🔄 Server-Sent Events (SSE)

Server-Sent Events (SSE)
روشی سبک و قابل‌اعتماد برای ارسال مداوم داده‌ها از سمت سرور به کلاینت‌ها است — بدون پیچیدگی‌های پروتکل‌های دوطرفه مانند WebSocket.
در مدل SSE، سرور از طریق یک اتصال HTTP واحد، داده‌های بلادرنگ را به مرورگر ارسال می‌کند. برخلاف مدل request-response سنتی، نیازی نیست کلاینت‌ها مدام از سرور درخواست بفرستند.

برای ارسال SSE، باید یک جریان داده از نوع ‎IAsyncEnumerable<T>‎ فراهم کنید.

🔹 مثال سرویسی برای تولید داده‌های قیمت سهام:
public record StockPriceEvent(string Id, string Symbol, decimal Price, DateTime Timestamp);

public class StockService
{
public async IAsyncEnumerable<StockPriceEvent> GenerateStockPrices(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var symbols = new[] { "MSFT", "AAPL", "GOOG", "AMZN" };

while (!cancellationToken.IsCancellationRequested)
{
var symbol = symbols[Random.Shared.Next(symbols.Length)];
var price = Math.Round((decimal)(100 + Random.Shared.NextDouble() * 50), 2);
var id = DateTime.UtcNow.ToString("o");

yield return new StockPriceEvent(id, symbol, price, DateTime.UtcNow);

await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
}
}
}

حالا با استفاده از ‎TypedResults.ServerSentEvents‎ یک endpoint برای استریم داده می‌سازیم:
builder.Services.AddSingleton<StockService>();

app.MapGet("/stocks", (StockService stockService, CancellationToken ct) =>
{
return TypedResults.ServerSentEvents(
stockService.GenerateStockPrices(ct),
eventType: "stockUpdate"
);
});

کلاینت‌ها می‌توانند به این endpoint متصل شوند و به‌صورت بلادرنگ بروزرسانی‌های قیمت را دریافت کنند.

برای راهنمایی کامل‌تر درباره‌ی SSE در ‎ASP.NET Core‎، می‌توانید راهنمای رسمی Microsoft را مطالعه کنید.
📘 OpenAPI 3.1 Support

در ‎ASP.NET Core 10‎ پشتیبانی از تولید مستندات ‎OpenAPI 3.1‎ اضافه شده است.
اگرچه شماره نسخه ممکن است کوچک به نظر برسد، اما ‎OpenAPI 3.1‎ تغییرات مهمی دارد، از جمله پشتیبانی کامل از JSON Schema draft 2020-12.

🔹 تغییرات کلیدی در OpenAPI 3.1:
نوع‌های nullable به‌جای ‎nullable: true‎ از ‎type: [<type>, "null"]‎ استفاده می‌کنند.
نوع‌های عددی مانند ‎int‎ و ‎long‎ ممکن است بدون ‎type: integer‎ و با فیلد ‎pattern‎ ظاهر شوند (بسته به تنظیمات serialization).

نسخه‌ی پیش‌فرض OpenAPI اکنون ‎3.1‎ است.

برای تنظیم دستی نسخه‌ی OpenAPI:
builder.Services.AddOpenApi(options =>
{
options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;
});


📄 YAML Format Support

اکنون ‎ASP.NET Core‎ می‌تواند مستندات OpenAPI را در قالب YAML نیز ارائه دهد.
YAML خواناتر از JSON است و از رشته‌های چندخطی پشتیبانی می‌کند — مناسب برای مستندات طولانی.

برای فعال‌سازی:
if (app.Environment.IsDevelopment())
{
app.MapOpenApi("/openapi/{documentName}.yaml");
}


🧠 What's New in Blazor

در ‎.NET 10‎، بلیزر پیشرفت‌های زیادی داشته است: Hot Reload برای Blazor WebAssembly و ‎.NET on WebAssembly‎

⚙️ پیکربندی محیط (Environment Configuration) در Blazor WebAssembly مستقل

📊 پروفایلینگ و شمارنده‌های تشخیصی (Performance Profiling & Diagnostic Counters)

پارامتر جدید NotFoundPage برای router

⚡️ پیش‌لود دارایی‌های استاتیک (Static Asset Preloading) در Blazor Web Apps

بهبود در Form Validation
🚀 ویژگی‌های جدید EF Core 10 در 10 NET.

🧩 Complex Types (انواع پیچیده)

در EF Core 10 می‌توان داده‌هایی را مدل کرد که به یک Entity تعلق دارند اما هویت مستقل ندارند.
در حالی که Entityها به جدول‌های جداگانه در دیتابیس نگاشت می‌شوند، Complex Typeها می‌توانند به ستون‌هایی در همان جدول (Table Splitting) یا به یک ستون JSON نگاشت شوند.

این قابلیت امکان مدل‌سازی به سبک Document-Based را فراهم می‌کند و باعث کاهش JOINها و بهبود عملکرد دیتابیس می‌شود.

📘 مثال Table Splitting:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.ShippingAddress);
b.ComplexProperty(c => c.BillingAddress);
});


Optional Complex Types (انواع پیچیده اختیاری)

EF Core 10
از Complex Typeهای اختیاری نیز پشتیبانی می‌کند:
public class Customer
{
public int Id { get; set; }
public Address ShippingAddress { get; set; }
public Address? BillingAddress { get; set; }
}


🗃 JSON Mapping (نگاشت به JSON)

اکنون می‌توانید Complex Typeها را به ستون‌های JSON در دیتابیس نگاشت دهید:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.ShippingAddress, c => c.ToJson());
b.ComplexProperty(c => c.BillingAddress, c => c.ToJson());
});


💪 Struct Support (پشتیبانی از Struct)

Complex Type
ها حالا می‌توانند از Structها نیز استفاده کنند:
public struct Address
{
public required string Street { get; set; }
public required string City { get; set; }
public required string ZipCode { get; set; }
}


🔗 LeftJoin و RightJoin Operators

در نسخه‌های قبلی EF Core نوشتن LEFT JOIN بسیار پیچیده بود و نیاز به ترکیب SelectMany, GroupJoin, و DefaultIfEmpty داشت.
در .NET 10 حالا می‌توانید با متدهای LeftJoin و RightJoin، کوئری‌های تمیزتر و ساده‌تری بنویسید 👇
var query = context.Students
.LeftJoin(
context.Departments,
student => student.DepartmentID,
department => department.Id,
(student, department) => new { student.Name, Department = department?.Name }
);

🔍 این ویژگی‌ها EF Core را به سمت یک ORM مدرن‌تر، قدرتمندتر و سازگار با معماری‌های Document-oriented و Microservice سوق می‌دهد.
Nobody cares about your career progression more than you do.

Not your boss. Not your partner. Not your mom, dad, cat, dog, or fish.

You can be set up with an amazing manager who wants you to succeed and tries their best to help you grow.

But at the end of the day, YOU are in the driver's seat.
🚀 کاوش File-based Apps در #C در 10 NET.

سی‌شارپ همیشه یک‌جورهایی پُر از تشریفات و ساختارهای اضافی بوده، و خودمان هم این را خوب می‌دانیم. حتی ساده‌ترین برنامهٔ "Hello World" معمولاً به یک فایل راه‌حل (solution)، یک فایل پروژه (csproj)، و آن‌قدر کد boilerplate نیاز داشت که از خودتان می‌پرسیدید شاید بهتر بود از یک زبان اسکریپتی استفاده می‌کردید!

خب، مایکروسافت بالاخره صدایمان را شنید. با NET 10، File-based Apps. معرفی شده‌اند. و واقعاً وقتش بود.

📌 File-based Apps چیستند؟

ایده بسیار ساده است:
کد #C خود را در یک فایل تکی cs. بنویسید و مستقیم اجرا کنید. همین.

نیازی نیست برای یک اسکریپت کوچک که می‌خواهید چند فایل CSV را پردازش کنید یا یک API endpoint را تست کنید، یک ساختار کامل پروژه بسازید.

در این حالت همچنان همهٔ چیزهایی که #C را فوق‌العاده می‌کنند حفظ می‌کنید:
🔸️type safety
🔸️performance

کتابخانه‌های قدرتمند استاندارد💪🏻

اما حالا می‌توانید از #C برای اسکریپت‌های موقتی استفاده کنید؛ جاهایی که ساختن یک پروژهٔ کامل تبدیل به یک دردسر اضافه می‌شود.

و بله:

• می‌توانید NuGet package اضافه کنید
• می‌توانید پروژه‌های دیگر #C را reference کنید
• می‌توانید SDK خاصی را هدف قرار دهید
• و تنظیمات پروژه را فقط با یک فایل انجام دهید

همهٔ این کارها از طریق directive‌هایی که با :# شروع می‌شوند.

این قابلیت روی قابلیت top-level statements که در C# 9 معرفی شد ساخته شده. اگر قرار است اجازه دهیم مردم کلاس و متد Main را حذف کنند، خب چرا اجازه ندهیم کل فایل پروژه را هم حذف کنند؟

🚀 شروع کار با File-based Apps

فرض کنید می‌خواهید سریع بررسی کنید یک تاریخ مشخص مربوط به چه روزی از هفته است. کافی است یک فایل با نام date-checker.cs بسازید:
var targetDate = new DateTime(2025, 12, 31);
Console.WriteLine($"New Year's 2025 falls on a {targetDate.DayOfWeek}");
Console.WriteLine($"That's {(targetDate - DateTime.Today).Days} days from now");

و اجرا کنید:
dotnet run date-checker.cs

اولین باری که این دستور را اجرا می‌کنید، CLI یک مقدار جادوی پشت‌صحنه انجام می‌دهد:

🔹️یک پروژهٔ مجازی می‌سازد
🔹️کد را کامپایل می‌کند
🔹️همه‌چیز را cache می‌کند
🔹️و دفعات بعدی اجرای برنامه تقریباً لحظه‌ای است، چون به‌قدری هوشمند است که تشخیص دهد چیزی تغییر کرده یا نه.

📌مثال واقعی: پردازش سریع داده‌ها

در اینجا است که قضیه جذاب می‌شود. فرض کنیم لازم باشد خیلی سریع مقداری داده‌ی JSON را پردازش کنید و یک گزارش تولید کنید.

بیایید با System.Text.Json و CsvHelper یک کار عملی انجام بدهیم:
#:package CsvHelper@33.1.0
using System.Text.Json;
using CsvHelper;
using System.Globalization;

var json = await File.ReadAllTextAsync("sales_data.json");
var sales = JsonSerializer.Deserialize<List<SaleRecord>>(json);

var topProducts = sales
    .GroupBy(s => s.Product)
    .Select(g => new {
        Product = g.Key,
        TotalRevenue = g.Sum(s => s.Amount),
        UnitsSold = g.Count()
    })
    .OrderByDescending(p => p.TotalRevenue)
    .Take(10);

using var writer = new StreamWriter("top_products.csv");
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(topProducts);

Console.WriteLine("Report generated! Check top_products.csv");

record SaleRecord(string Product, decimal Amount, DateTime Date);

توجه کنید که در اینجا داریم package reference، عملیات async، LINQ و record type‌ها را با هم ترکیب می‌کنیم—همه در یک فایل واحد که کاملاً شبیه یک اسکریپت منسجم عمل می‌کند. این همان کاری است که معمولاً برای انجامش سراغ Python می‌رفتید، اما حالا می‌توانید در دنیای #C بمانید.
🛠ساخت یک پروژه جاه‌طلبانه‌تر با Aspire

شما حتی می‌توانید یک Aspire AppHost را فقط در یک فایل بسازید—که اگر خوب فکر کنید واقعاً چیز عجیبی است:
#:sdk Aspire.AppHost.Sdk@13.0.0
#:package Aspire.Hosting.AppHost@13.0.0

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
.WithDataVolume();

var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.AddDatabase("tododb");

var todoApi = builder.AddProject<Projects.TodoApi>("api")
.WithReference(cache)
.WithReference(postgres);

builder.AddNpmApp("frontend", "../TodoApp")
.WithReference(todoApi)
.WithReference("api")
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();

builder.Build().Run();


با استفاده از دستور #:sdk Aspire.AppHost.Sdk@13.0.0، فایل تک‌ستونه‌ی شما تبدیل به یک orchestrator کامل برای یک distributed application می‌شود. شما در حال تعریف زیرساخت، اتصال وابستگی‌ها، و راه‌اندازی یک محیط توسعه‌ی کامل هستید—بدون اینکه هیچ فایل project بسازید.

این قابلیت به‌ویژه زمانی کاربردی است که دارید معماری را prototype می‌کنید یا لازم دارید خیلی سریع یک محیط تست بالا بیاورید.

Migrating to a Full Project 🚀 — مهاجرت به یک پروژه‌ی کامل

در نهایت، بعضی اسکریپت‌ها از ریشه‌ی تک‌فایلی خود بزرگ‌تر می‌شوند. شاید لازم باشد چیزها را به چند فایل تقسیم کنید، یا شاید بخواهید پشتیبانی کامل IDE برای Debugging داشته باشید. این انتقال بدون دردسر است:
dotnet project convert MyUtility.cs

این دستور یک ساختار پروژه‌ی کامل ایجاد می‌کند، درحالی‌که تمام package reference‌ها و انتخاب‌های SDK شما را حفظ می‌کند. کد شما به Program.cs منتقل می‌شود و یک .csproj دریافت می‌کنید که تمام آن #:directiveهایی را که استفاده کرده بودید منعکس می‌کند. 📦⚙️

Current Limitations ⚠️ — محدودیت‌های فعلی

در حال حاضر، این قابلیت کاملاً تک‌فایلی است. اگر به چند فایل نیاز دارید، باید تا NET 11. صبر کنید یا پروژه را تبدیل کنید. البته همچنان می‌توانید به پروژه‌ها و packageهای دیگر reference بدهید، اما فایل اصلی اسکریپت باید تنها یک فایل باشد. 📄

مکانیزم caching ممکن است گاهی دچار سردرگمی شود، مخصوصاً اگر سریع روی نسخه‌های package تکرار کنید. و با اینکه پشتیبانی IDE بهتر شده، هنوز به سطح پروژه‌های کامل نرسیده است—به‌خصوص برای IntelliSense مربوط به packageهایی که به صورت داینامیک reference می‌کنید. 💡

اما برای کاری که طراحی شده است (قابل‌دسترس‌تر کردن #C برای سناریوهای noscripting)، به‌طرز شگفت‌انگیزی خوب عمل می‌کند. می‌توانید از آن برای build noscriptها، کارهای یک‌باره‌ی data migration، تست سریع API، یا حتی آموزش #C بدون اینکه روز اول بخواهید ساختار یک solution را توضیح بدهید، استفاده کنید. 🧪⚙️

Where This Leaves Us 🎯 — نتیجه‌ی این قابلیت

File-based app
ها حسی ایجاد می‌کنند شبیه اینکه #C بالاخره پذیرفته است که همه‌چیز قرار نیست یک برنامه‌ی enterprise باشد. گاهی لازم دارید یک فایل log را parse کنید، گاهی باید سریع یک الگوریتم را تست کنید، و گاهی دارید برنامه‌نویسی را به کسی آموزش می‌دهید و نمی‌خواهید روز اول برایش از solution file حرف بزنید. 📝⚡️

این قابلیت انقلابی در توسعه‌ی #C ایجاد نمی‌کند، اما شکاف آزاردهنده‌ای را پر می‌کند که سال‌ها توسعه‌دهندگان را اذیت کرده بود. اگر تا امروز یک پوشه پر از اسکریپت‌های سریع داشتید چون #C برای چنین کارهایی سنگین به نظر می‌رسید، شاید وقتش رسیده دوباره به زبان محبوبتان شانس بدهید. 💙🤖

در نهایت، بهترین زبان برای یک اسکریپت سریع، همان زبانی است که از قبل بلد هستید—و حالا #C در این زمینه بسیار قانع‌کننده‌تر شده است. ⚡️💻
Forwarded from tech-afternoon (Amin Mesbahi)
📕 پیشنهاد کتاب: Crafting Engineering Strategy

کمتر از ۲ هفته دیگه (۲۵ نوامبر ۲۰۲۵)، کتاب

Crafting Engineering Strategy:
How Thoughtful Decisions Solve Complex Problems


یا: طراحی استراتژی مهندسی: چگونه تصمیم‌گیری‌های سنجیده مسائل پیچیده رو حل می‌کنن؛ منتشر می‌شه. توی معرفی اولیه اومده:
خیلی از مهندس‌ها تصور می‌کنن سازمانشون استراتژی مهندسی نداره! در حالی که در واقع، اغلبشون دارن؛ ولی ممکنه این حس ناشی از ناکارآمدی اسراتژی‌ها باشه. نویسنده، یعنی ویل لارسون (نویسنده کتاب‌هایی مثل Elegant Puzzle یا Staff Engineer) یک راهنمای کاربردی، و در عین حال غنی از مثال‌های واقعی، برای مسیریابی در پیچیدگی‌های فنی، و همچنین پیچیدگی‌های سازمانی از طریق «استراتژی ساختاریافته و هدفمند» ارائه می‌ده.

این کتاب که برای مهندس‌های ارشد، رهبران مهندسی، و معمارها نوشته شده. مثال‌های واقعی از شرکت‌هایی مثل استرایپ، اوبر، و کَلم استخراج ارائه میده. چارچوب پیشنهادی نویسنده، برای شکل‌دهی تصمیم‌گیری‌های حیاتی در مورد مهاجرت سیستم‌ها، منسوخ کردن APIها، سرمایه‌گذاری‌های پلتفرم و موارد مشابه کاربرد داره. کتاب، در طول مسیر، یاد قراره تا یاد بده برنامه‌ریزی فنی رو با ارتباطات، حاکمیت، و تفکر سیستمی تقویت کنید. چه در حال شکل‌دهی به مسیر تیمتون باشید و چه رهبری یک ابتکار در سطح شرکت رو به عهده داشته باشین، «طراحی استراتژی مهندسی» به شما کمک می‌کنه تصمیم‌های سنجیده‌ای بگیرید که پایدار باشن.

دلیل معرفی این کتاب اول موضوعش بود، دوم اینکه من چند کتاب از این نویسنده رو خوندم و سبک نوشتار و مسیر پرداختن به موضعش رو دوست دارم (این یک نظر شخصیه و شاید برای شما صدق نکنه)

نسخه کاغذی با قیمت 36.92€ عرضه خواهد شد.
در مورد نویسنده
Please open Telegram to view this post
VIEW IN TELEGRAM
🚀 Fast SQL Bulk Inserts With C# and EF Core

فرقی نمی‌کند دارید یک پلتفرم تحلیل داده می‌سازید، یک سیستم قدیمی را مهاجرت می‌دهید، یا یک موج بزرگ از کاربران جدید وارد سیستم‌تان می‌شوند—به‌احتمال زیاد زمانی می‌رسد که باید مقدار عظیمی داده را وارد دیتابیس کنید.

وارد کردن رکوردها یکی‌یکی مثل این است که خشک‌شدن رنگ را در حرکت آهسته تماشا کنید! روش‌های سنتی اصلاً جواب نمی‌دهند.
پس یاد گرفتن تکنیک‌های bulk insert سریع با #C و EF Core تبدیل به یک مهارت ضروری می‌شود.

در این مقاله، چندین روش برای انجام bulk insert در #C بررسی می‌کنیم:
• Dapper
• EF Core
• EF Core Bulk extensions
• SQL Bulk Copy
• Entity Framework Extensions

تمام مثال‌ها بر اساس یک کلاس User و جدول Users در SQL Server هستند:
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
}

این لیست کامل تمام روش‌های bulk insert نیست. چند گزینه دیگر بررسی نشده‌اند، مثل تولید دستی SQL یا استفاده از Table-Valued parameters.

📌EF Core Simple Approach

بیایید با یک مثال ساده با EF Core شروع کنیم. ما یک ApplicationDbContext می‌سازیم، یک شی User اضافه می‌کنیم و SaveChangesAsync را صدا می‌زنیم. این کار باعث می‌شود هر رکورد به‌صورت جداگانه در دیتابیس insert شود. یعنی برای هر رکورد یک round trip به دیتابیس نیاز داریم.
using var context = new ApplicationDbContext();

foreach (var user in GetUsers())
{
context.Users.Add(user);

await context.SaveChangesAsync();
}

و نتایج دقیقاً همان‌قدر ضعیف هستند که انتظار دارید:
EF Core - Add one and save برای 100 کاربر: 20 ms

EF Core - Add one and save برای 1,000 کاربر: 260 ms

EF Core - Add one and save برای 10,000 کاربر: 8,860 ms

نتایج 100,000 و 1,000,000 رکورد حذف شدند چون اجرای آنها بیش از حد طولانی شد.

این مثال را به‌عنوان «چگونه bulk insert انجام ندهیم» در نظر بگیرید.

🧩Dapper Simple Insert

Dapper
یک SQL-to-object mapper ساده برای NET. است. این ابزار اجازه می‌دهد مجموعه‌ای از اشیا را به‌راحتی در دیتابیس insert کنیم.

من از قابلیت Dapper برای unwrap کردن یک collection داخل یک SQL INSERT statement استفاده می‌کنم.
using var connection = new SqlConnection(connectionString);
connection.Open();

const string sql =
@"
INSERT INTO Users (Email, FirstName, LastName, PhoneNumber)
VALUES (@Email, @FirstName, @LastName, @PhoneNumber);
";

await connection.ExecuteAsync(sql, GetUsers());

نتایج بسیار بهتر از مثال ابتدایی هستند:
Dapper - Insert range برای 100 کاربر: 10 ms

Dapper - Insert range برای 1,000 کاربر: 113 ms

Dapper - Insert range برای 10,000 کاربر: 1,028 ms

Dapper - Insert range برای 100,000 کاربر: 10,916 ms

Dapper - Insert range برای 1,000,000 کاربر: 109,065 ms


🔸️EF Core Add and Save

اما EF Core هم هنوز کنار نکشیده. مثال اول عمداً خیلی ضعیف پیاده‌سازی شده بود. EF Core می‌تواند چندین SQL statement را با هم batch کند، پس از این قابلیت استفاده می‌کنیم.

اگر یک تغییر ساده بدهیم، می‌توانیم عملکرد به‌مراتب بهتری بگیریم. اول، همهٔ اشیا را به ApplicationDbContext اضافه می‌کنیم. سپس فقط یک‌بار SaveChangesAsync را صدا می‌زنیم.

این EF یک batched SQL statement می‌سازد—یعنی چندین INSERT را با هم گروه می‌کند—و همه را یکجا به دیتابیس می‌فرستد. این کار تعداد round trip ها را کم می‌کند و باعث بهبود سرعت می‌شود.
using var context = new ApplicationDbContext();

foreach (var user in GetUsers())
{
context.Users.Add(user);
}

await context.SaveChangesAsync();

نتایج benchmark این پیاده‌سازی:
EF Core - Add all and save برای 100 کاربر: 2 ms

EF Core - Add all and save برای 1,000 کاربر: 18 ms

EF Core - Add all and save برای 10,000 کاربر: 203 ms

EF Core - Add all and save برای 100,000 کاربر: 2,129 ms

EF Core - Add all and save برای 1,000,000 کاربر: 21,557 ms

به یاد داشته باشید: Dapper برای insert کردن 1,000,000 رکورد حدود 109 ثانیه زمان گرفت. با EF Core و batched queries، همان کار را در حدود 21 ثانیه انجام می‌دهیم.
EF Core AddRange و Save

این یک جایگزین برای مثال قبلی است. به جای فراخوانی Add برای همهٔ آبجکت‌ها، می‌توانیم AddRange را صدا بزنیم و یک کالکشن ارسال کنیم.

می‌خواستم این پیاده‌سازی را نشان دهم چون آن را به نمونهٔ قبلی ترجیح می‌دهم.
using var context = new ApplicationDbContext();

context.Users.AddRange(GetUsers());

await context.SaveChangesAsync();

نتایج بسیار مشابه نمونهٔ قبلی هستند: 📊
EF Core - Add range و Save، برای ۱۰۰ کاربر: ۲ ms
EF Core - Add range و Save، برای ۱,۰۰۰ کاربر: ۱۸ ms
EF Core - Add range و Save، برای ۱۰,۰۰۰ کاربر: ۲۰۴ ms
EF Core - Add range و Save، برای ۱۰۰,۰۰۰ کاربر: ۲,۱۱۱ ms
EF Core - Add range و Save، برای ۱,۰۰۰,۰۰۰ کاربر: ۲۱,۶۰۵ ms

EF Core Bulk Extensions ⚡️

یک کتابخانهٔ فوق‌العاده به نام EF Core Bulk Extensions وجود دارد که می‌توانیم برای افزایش عملکرد استفاده کنیم. شما با این کتابخانه می‌توانید کارهای بیشتری از جمله bulk insert انجام دهید، بنابراین ارزش بررسی دارد. این کتابخانه متن‌باز است و دارای لایسنس Community می‌باشد اگر شرایط استفادهٔ رایگان را داشته باشید. بخش لایسنس را برای جزئیات بررسی کنید.

برای استفادهٔ ما، متد BulkInsertAsync گزینهٔ عالی است. می‌توانیم کالکشن آبجکت‌ها را ارسال کنیم و یک SQL bulk insert انجام خواهد شد.
using var context = new ApplicationDbContext();

await context.BulkInsertAsync(GetUsers());

عملکرد آن نیز شگفت‌انگیز است: 🚀
EF Core - Bulk Extensions، برای ۱۰۰ کاربر: ۱.۹ ms
EF Core - Bulk Extensions، برای ۱,۰۰۰ کاربر: ۸ ms
EF Core - Bulk Extensions، برای ۱۰,۰۰۰ کاربر: ۷۶ ms
EF Core - Bulk Extensions، برای ۱۰۰,۰۰۰ کاربر: ۷۴۲ ms
EF Core - Bulk Extensions، برای ۱,۰۰۰,۰۰۰ کاربر: ۸,۳۳۳ ms

برای مقایسه، ما با EF Core batched queries برای درج ۱,۰۰۰,۰۰۰ رکورد حدود ۲۱ ثانیه نیاز داشتیم. می‌توانیم همان کار را با کتابخانهٔ Bulk Extensions تنها در ۸ ثانیه انجام دهیم. 😎

SQL Bulk Copy 🧩

اگر نتوانیم عملکرد دلخواه را از EF Core بگیریم، می‌توانیم از SqlBulkCopy استفاده کنیم. SQL Server عملیات bulk copy را به صورت native پشتیبانی می‌کند، پس از آن استفاده کنیم.

این پیاده‌سازی کمی پیچیده‌تر از مثال‌های EF Core است. باید یک نمونه SqlBulkCopy را کانفیگ کنیم و یک DataTable ایجاد کنیم که شامل آبجکت‌هایی باشد که می‌خواهیم درج کنیم.
using var bulkCopy = new SqlBulkCopy(ConnectionString);

bulkCopy.DestinationTableName = "dbo.Users";

bulkCopy.ColumnMappings.Add(nameof(User.Email), "Email");
bulkCopy.ColumnMappings.Add(nameof(User.FirstName), "FirstName");
bulkCopy.ColumnMappings.Add(nameof(User.LastName), "LastName");
bulkCopy.ColumnMappings.Add(nameof(User.PhoneNumber), "PhoneNumber");

await bulkCopy.WriteToServerAsync(GetUsersDataTable());

با این حال، عملکرد فوق‌العاده سریع است: 🔥
SQL Bulk Copy، برای ۱۰۰ کاربر: ۱.۷ ms
SQL Bulk Copy، برای ۱,۰۰۰ کاربر: ۷ ms
SQL Bulk Copy، برای ۱۰,۰۰۰ کاربر: ۶۸ ms
SQL Bulk Copy، برای ۱۰۰,۰۰۰ کاربر: ۶۴۶ ms
SQL Bulk Copy، برای ۱,۰۰۰,۰۰۰ کاربر: ۷,۳۳۹ ms

در ادامه نحوهٔ ایجاد DataTable و پر کردن آن با لیست آبجکت‌ها آمده است: 🏗
DataTable GetUsersDataTable()
{
var dataTable = new DataTable();

dataTable.Columns.Add(nameof(User.Email), typeof(string));
dataTable.Columns.Add(nameof(User.FirstName), typeof(string));
dataTable.Columns.Add(nameof(User.LastName), typeof(string));
dataTable.Columns.Add(nameof(User.PhoneNumber), typeof(string));

foreach (var user in GetUsers())
{
dataTable.Rows.Add(
user.Email, user.FirstName, user.LastName, user.PhoneNumber);
}

return dataTable;
}
Entity Framework Extensions 💎

آیا می‌توانیم بهتر از SqlBulkCopy عمل کنیم؟
شاید، حداقل نتایج بنچمارک من نشان می‌دهد که می‌توانیم.

یک کتابخانهٔ فوق‌العاده دیگر به نام Entity Framework Extensions وجود دارد. این فقط یک کتابخانهٔ bulk insert نیست - بنابراین توصیه می‌کنم حتماً بررسی شود. با این حال، امروز آن را برای bulk insert استفاده خواهیم کرد.

برای استفادهٔ ما، متد BulkInsertOptimizedAsync گزینهٔ عالی است. می‌توانیم کالکشن آبجکت‌ها را ارسال کنیم و یک SQL bulk insert انجام خواهد شد. همچنین برخی بهینه‌سازی‌ها زیر هود انجام می‌دهد تا عملکرد بهتر شود.
using var context = new ApplicationDbContext();

await context.BulkInsertOptimizedAsync(GetUsers());

عملکرد آن فوق‌العاده است: ⚡️🔥
EF Core - Entity Framework Extensions، برای ۱۰۰ کاربر: ۱.۸۶ ms
EF Core - Entity Framework Extensions، برای ۱,۰۰۰ کاربر: ۶.۹ ms
EF Core - Entity Framework Extensions، برای ۱۰,۰۰۰ کاربر: ۶۶ ms
EF Core - Entity Framework Extensions، برای ۱۰۰,۰۰۰ کاربر: ۶۳۶ ms
EF Core - Entity Framework Extensions، برای ۱,۰۰۰,۰۰۰ کاربر: ۷,۱۰۶ ms


جمع‌بندی 🎯

کار SqlBulkCopy برای حداکثر سرعت و سادگی بهترین است. 👑
با این حال، Entity Framework Extensions عملکرد فوق‌العاده‌ای ارائه می‌دهند و هم‌زمان سهولت استفاده‌ای که EF Core دارد را حفظ می‌کنند. 💎⚡️

بهترین گزینه به نیازهای خاص پروژهٔ شما بستگی دارد:
فقط عملکرد مهم است؟ SqlBulkCopy راه حل شماست. 🔥

به سرعت عالی و توسعهٔ آسان نیاز دارید؟ EF Core انتخاب هوشمندانه است. ⚡️

به دنبال تعادل بین عملکرد و سهولت استفاده هستید؟ Entity Framework Extensions ⚖️

تصمیم با شماست که بهترین گزینه برای پروژهٔ خودتان کدام است.

امیدوارم مفید بوده باشد. 🙌

🔖هشتگ‌ها:
#EFCore #Dapper #SqlBulkCopy #BulkExtensions #EntityFrameworkExtensions #Performance #CSharp #Database #DotNet #Programming #BulkInsert
Forwarded from tech-afternoon (Amin Mesbahi)
🧮 به نهضت NoEstimates# بپیوندیم؟ هنوز Story Point برای تخمین اندازه تسک‌ها لازمه؟


بعد از دیدن مطلبی که مسعود بیگی در کانال تندتک درباره حذف Story Point از تسک‌هاشون نوشت، و مواضع مختلفی که در اینباره توی کامیونیتی‌های انگلیسی و فارسی وجود داره، تصمیم گرفتم چند خطی رو به این بحث بپردازم. امیدوارم کمک کنه به تصمیم‌گیری بهتر دوستان...

مرگِ یک آیین قدیمی یا تکامل طبیعی؟

سوال تکراری همیشگی بعد از اینکه یک تیم یا شرکت تخمین Story Point رو کنار می‌گذاره؛ اینه: «خب جایگزینش چیه؟»
ولی عملا سوال بهتر اینه که اصلاً نیاز واقعی‌مون به Story Point چی بوده؟ و چرا به وجود اومده؟
این بحث، بحثِ خیلی از تیم‌های نوپا و بالغ دنیا طی سال‌های اخیر بوده.
عملا Story Point در سال‌های ابتدایی پیدایش مفهوم agile در توسعه نرم‌افزار، ناجی خیلی از تیم‌ها بود. یعنی وقتی تیم‌ها از تخمین زمانی (مثل person-hour یا person-day) خسته شده بودن و می‌دیدن این اعداد عموما دروغ می‌گن؛ Story Point با نگاه «بی‌خیال ساعت شید، فقط بگید این کار نسبت به اون یکی چقدر بزرگ‌تره» به وجود اومد. و واقعاً هم کمک کرد. تیم‌ها بهتر پیش‌بینی می‌کردن، Velocity داشتن، ظرفیت اسپرینت مشخص بود.

اما این سال‌ها بعضی از تیم‌ها Story Point رو کنار گذاشتن، مهم‌ترین دلایلی که من دیدم، اینا بوده:

۱. طی گذر زمان؛ Story Point تبدیل به دروغ شد!
همون چیزی که قرار بود جلوی دروغ رو بگیره، خودش تبدیل به دروغ شد.
چرا؟ چون مدیر محصول یا PO می‌گفت: «این فیچر باید تو همین اسپرینت جا بشه، پس ۸ نزنید، ۵ بزنید!»
یا توسعه‌دهنده از ترس فشار بعدی، همیشه عدد بزرگ‌تر می‌زد.
نتیجه؟ Velocity غیرواقعی، تخمین غیرواقعی، بی‌اعتمادی کامل. من از حدودای ۲۰۱۲-۲۰۱۳ یادم میاد که نهضت NoEstimates# با این گفتمان راه افتاده که تخمین، هزینه داره و اغلب دقتش اون‌قدری نیست که ارزش هزینه‌ش رو داشته باشه!

۲. تیم‌ها دیگه نیازی به «واحد خیالی» ندارن
وقتی تیم کوچیکه، در اثر تجربه و هم‌نشینی گاهی دیگه نیازی نمی‌بینن بگن «این ۵ پوینته، اون ۸ پوینته».
فقط می‌گن: «این کار حدود ۲-۳ روزه، اون یکی یه هفته».
و این تخمین «تقریبی و رنج‌دار» گاهی دقیق‌تر از Story Point در میاد!
از طرفی، تیم‌های خوب به جای اندازه تسک، روی شکل دادن (Shaping) فیچرها تمرکز می‌کنند و تخمین ریز نمی‌زنن.

۳. کار رو باید انجام داد چه یه روز چه ده روز!
بعضی تیم‌ها ماهیت تسک‌هاشون «ضرورتِ محصوله»، یعنی قابل حذف یا جایگزینی نیست، محصول هم باید تولید شه. لذا برای مدیر و شرکت مهمه که زودتر برسه، ولی اگر چند وقت دیرتر هم بشه، راهی جز پذیرش نداره. اینجاست که تیم می‌گه خب so what؟ تخمین بزنم که چی بشه؟ من که اول و آخر باید این کار رو انجام بدم!

۴. کارهای روتین، یا قابل پیش‌بینی هستن
وقتی کارها به طور نسبی زمان مشابهی برای اجرا نیاز دارن، یا توی چند دسته‌ی قابل پیش‌بینی قرار می‌گیرن، تکرار مکررات باعث می‌شه تیم بیخیال تخمین بشه و می‌دونه چه کاری حدودا طی چی زمانی انجام می‌شه. اینجا دیگه به جای story point گاها T-Shirt sizing هم جواب می‌ده (S, M, L, XL)

پس کِی و کجا Story Point هنوز مفیده؟

- انترپرایزها: تیم بزرگ، سازمان بزرگ، وابستگی‌های بین تیمی زیاد، مدیریت بودجه سخت‌گیرانه و... توی چنین محیط‌هایی اینقدر در هم‌تنیدگی کارها، بوجه، تیم‌ها، و.. زیاده که یکی از مهارت‌های مدیر تیم، کمک به بهبود و تدقیق تخمین‌هاست. اینقدری که توی شرکت‌های بزرگ tech با ماشین‌لرنینگ و هوش‌مصنوعی این تخمین‌ها بهبود پیدا می‌کنه، چون دیر رسیدن یک ماهه‌ی یک محصول گاها عدم‌النفع یا حتی خسارت‌های چند ده میلیون دلاری می‌تونه به بار بیاره.
قبلا همینجا هم نوشتم که انترپرایز الزاما تعداد کارمند زیاد نیست، باید ساختار و رویه‌ها قابل انترپرایز خطاب شدن باشه!

- تیم‌های جدید که هنوز Flow پایداری ندارن

- وقتی تیم‌ها توزیع‌شده (distributed) هستن و ارتباط لحظه‌ای کمه

یادمون نره که Agile یعنی انعطاف. اگر ابزاری به درد تیم شما نمی‌خورد، کنارش بگذارید. اینکه process templateهای متنوعی وجود داره، از مدل‌های ساده مثل kanban تا scrum و agile و حتی مدل‌های خیلی سخت‌گیر و دقیقی مثل CMMI هم هست، یعنی «نسخه واحد برای همه تیم‌ها و محصولات وجود نداره».

سوال این نیست که "Story Point خوبه یا بد؟"
سوال اینه: "برای تیم ما، در مرحله فعلی، چه چیزی بهترین کمک رو می‌کنه؟"


بعضی‌ها هم که story point رو گذاشتن کنار از سر بلد نبودنشون بوده! نه از سر مناسب نبودنش! و اساسا باید عارضه رو در خودشون پیدا می‌کردن یا اینکه تا زمان بلوغ، می‌رفتن سراغ متد دیگه‌ای که مبتنی بر SP نباشه، نه اینکه متد رو نگه دارن و SP رو از دلش بکشن بیرون.

این موضوع خیلی مفصله ولی امیدوارم در حد سرنخ دادن کمک کرده باشه... 😊
Please open Telegram to view this post
VIEW IN TELEGRAM
I don't love programming for the same reasons as when I started.

What started like infinite Lego bricks has changed over time for me.

Don't get me wrong -- I still love building things. I still love being able to solve complex problems by putting systems together.

There's genuinely something awesome about seeing parts of a system come together.

But there's something that excites me more. More exciting than when I was building text-based RPGs when I was a kid.

It comes down to people.

As software engineers, we're going to have projects and challenges that are draining. They might seem like a mountain to climb or a project with no end in sight.

But if you're fortunate enough to work around other developers that:
• Don't shy away from crazy ideas
• Are driven to solve problems
• Aren't afraid of challenges
• Iterate quickly

I've found that this always gives me tons of energy. Far more than I've ever had from working on projects by myself -- regardless of how daunting the challenge at hand might be.

What gets your energy levels up on a project?
گزارش‌دهی PDF انعطاف‌پذیر در NET. با استفاده از Razor Views 📄

هرگز فراموش نمی‌کنم زمانی که روی پروژه‌ای کار می‌کردم که نیاز به تولید گزارش‌های فروش هفتگی برای یک مشتری داشت. راه‌حل اولیه شامل فرآیندی خسته‌کننده بود: خروجی گرفتن از داده‌ها، دستکاری آن‌ها در spreadsheets، و ساخت PDFها به صورت دستی. 😓 این روش وقت‌گیر، پرخطا و انرژی‌بر بود. من می‌دانستم که حتماً باید راه بهتری وجود داشته باشد. 💡

در این زمان بود که قدرت ترکیب Razor views با تبدیل HTML به PDF را کشف کردم. شما کنترل بیشتری روی فرمت‌دهی سند دارید. می‌توانید از CSS مدرن برای استایل‌دهی HTML استفاده کنید، که هنگام خروجی گرفتن به PDF اعمال خواهد شد. این کار همچنین در ASP.NET Core به راحتی قابل پیاده‌سازی است. 🖥

📌 در این مقاله، موارد زیر را پوشش می‌دهیم:

• درک Razor views
• تبدیل Razor views به HTML
• تبدیل HTML به PDF در .NET
• ترکیب همه چیز با Minimal APIs
بیایید شروع کنیم!

🚀Razor Views

یک قالب HTML با markupهای تعبیه‌شده Razor است. Razor به شما اجازه می‌دهد تا کد NET. را داخل یک صفحه وب بنویسید و اجرا کنید. فایل‌های Views دارای پسوند ویژه cshtml. هستند و معمولاً در ASP.NET Core MVC، Razor Pages و Blazor استفاده می‌شوند.
با این حال، می‌توانید Razor views را در یک class library یا پروژه ASP.NET Core Web API تعریف کنید.

می‌توانید از Model برای ارسال داده به Razor view استفاده کنید. داخل فایل .cshtml، نوع مدل را با کلمه کلیدی model@ مشخص می‌کنید. در مثال زیر، مشخص کرده‌ایم که کلاس Invoice مدل این view است. می‌توانید به نمونه مدل از طریق property Model در view دسترسی داشته باشید.

این همان view به نام InvoiceReport.cshtml است که برای تولید یک فاکتور PDF استفاده خواهیم کرد.

می‌توانید CSS را در Razor view به صورت inline بنویسید یا به یک stylesheet ارجاع دهید. من از فریم‌ورک Tailwind CSS استفاده می‌کنم که CSS به صورت inline است. معمولاً این کار را به یک front-end engineer در تیمم می‌سپارم تا گزارش را مطابق نیاز استایل‌دهی کند. 🎨
@using System.Globalization
@using HtmlToPdf.Contracts

@model HtmlToPdf.Contracts.Invoice

@{
    IFormatProvider cultureInfo = CultureInfo.CreateSpecificCulture("en-US");
    var subtotal = Model.LineItems.Sum(li => li.Price * li.Quantity).ToString("C", cultureInfo);
    var total = Model.LineItems.Sum(li => li.Price * li.Quantity).ToString("C", cultureInfo);
}

<noscript src="https://cdn.tailwindcss.com"></noscript>

<div class="min-w-7xl flex flex-col bg-gray-200 space-y-4 p-10">
    <h1 class="text-2xl font-semibold">Invoice #@Model.Number</h1>

    <p>Issued date: @Model.IssuedDate.ToString("dd/MM/yyyy")</p>
    <p>Due date: @Model.DueDate.ToString("dd/MM/yyyy")</p>

    <div class="flex justify-between space-x-4">
        <div class="bg-gray-100 rounded-lg flex flex-col space-y-1 p-4 w-1/2">
            <p class="font-medium">Seller:</p>
            <p>@Model.SellerAddress.CompanyName</p>
            <p>@Model.SellerAddress.Street</p>
            <p>@Model.SellerAddress.City</p>
            <p>@Model.SellerAddress.State</p>
            <p>@Model.SellerAddress.Email</p>
        </div>
        <div class="bg-gray-100 rounded-lg flex flex-col space-y-1 p-4 w-1/2">
            <p class="font-medium">Bill to:</p>
            <p>@Model.CustomerAddress.CompanyName</p>
            <p>@Model.CustomerAddress.Street</p>
            <p>@Model.CustomerAddress.City</p>
            <p>@Model.CustomerAddress.State</p>
            <p>@Model.CustomerAddress.Email</p>
        </div>
    </div>

    <div class="flex flex-col bg-white rounded-lg p-4 space-y-2">
        <h2 class="text-xl font-medium">Items:</h2>
        <div class="">
            <div class="flex space-x-4 font-medium">
                <p class="w-10">#</p>
                <p class="w-52">Name</p>
                <p class="w-20">Price</p>
                <p class="w-20">Quantity</p>
            </div>
@foreach ((int index, LineItem item) in Model.LineItems.Select((li, i) => (i + 1, li)))
{
<div class="flex space-x-4">
<p class="w-10">@index</p>
<p class="w-52">@item.Name</p>
<p class="w-20">@item.Price.ToString("C", cultureInfo)</p>
<p class="w-20">@item.Quantity.ToString("N2")</p>
</div>
}
</div>
</div>

<div class="flex flex-col items-end bg-gray-50 space-y-2 p-4 rounded-lg">
<p>Subtotal: @subtotal</p>
<p>Total: <span class="font-semibold">@total</span></p>
</div>
</div>
تبدیل Razor Views به HTML 🖥➡️📄

گام بعدی که نیاز داریم، راهی برای تبدیل Razor view به HTML است. می‌توانیم از کتابخانه Razor.Templating.Core استفاده کنیم. این کتابخانه یک API ساده ارائه می‌دهد تا فایل .cshtml را به یک رشته (string) تبدیل کند.
Install-Package Razor.Templating.Core

می‌توانید از کلاس static RazorTemplateEngine برای فراخوانی متد RenderAsync استفاده کنید. این متد مسیر Razor view و نمونه مدل (model) که به view منتقل می‌شود را می‌پذیرد.

نمونه کد:
Invoice invoice = invoiceFactory.Create();

string html = await RazorTemplateEngine.RenderAsync(
"Views/InvoiceReport.cshtml",
invoice);

همچنین می‌توانید به جای کلاس static از IRazorTemplateEngine استفاده کنید. در این حالت باید متد AddRazorTemplating را برای ثبت سرویس‌های مورد نیاز با DI فراخوانی کنید. این کار همچنین زمانی لازم است که بخواهید از dependency injection در داخل Razor views با @inject استفاده کنید. توصیه می‌شود بعد از ثبت تمام dependencies، AddRazorTemplating را فراخوانی کنید.
services.AddRazorTemplating();


تبدیل HTML به PDF 📄

حالا که Razor view ما به HTML تبدیل شد، می‌توانیم از آن برای تولید یک گزارش PDF استفاده کنیم. کتابخانه‌های زیادی این قابلیت را ارائه می‌دهند. کتابخانه‌ای که من بیشترین استفاده را از آن داشته‌ام IronPDF است. این یک کتابخانه پولی است (و واقعاً ارزشش را دارد)، اما می‌دانم توسعه‌دهندگان به گزینه‌های رایگان هم علاقه دارند، بنابراین چند گزینه رایگان را در انتها معرفی می‌کنم.

می‌توانیم از ChromePdfRenderer در IronPDF استفاده کنیم، که از مرورگر Chrome تعبیه‌شده استفاده می‌کند. این renderer متد RenderHtmlAsPdf را ارائه می‌دهد که یک PdfDocument تولید می‌کند. پس از داشتن سند، می‌توانید آن را در سیستم فایل ذخیره کنید یا به صورت داده باینری صادر کنید.
var renderer = new ChromePdfRenderer();

using var pdfDocument = renderer.RenderHtmlAsPdf(html);

pdfDocument.SaveAs($"invoice-{invoice.Number}.pdf");

اگر به دنبال گزینه‌های رایگان هستید، کتابخانه Puppeteer Sharp را بررسی کنید. این یک نسخه NET. از کتابخانه Puppeteer است و امکان اجرای headless Chrome را فراهم می‌کند.

گزینه دیگر که به صورت مشروط رایگان است، NReco.PdfGenerator می‌باشد، اما تنها برای استقرار روی یک سرور (single-server) رایگان است.

ترکیب همه چیز با هم 🛠

بیایید همه مواردی که بررسی کردیم را استفاده کنیم تا یک Minimal API endpoint برای تولید گزارش PDF فاکتور بسازیم و آن را به صورت فایل بازگردانیم.
app.MapGet("invoice-report", async (InvoiceFactory invoiceFactory) =>
{
Invoice invoice = invoiceFactory.Create();

var html = await RazorTemplateEngine.RenderAsync(
"Views/InvoiceReport.cshtml",
invoice);

var renderer = new ChromePdfRenderer();

using var pdfDocument = renderer.RenderHtmlAsPdf(html);

return Results.File(
pdfDocument.BinaryData,
"application/pdf",
$"invoice-{invoice.Number}.pdf");
});

این همان PDF گزارش تولید شده است که خروجی می‌دهد: 📄
خلاصه 📝

در این مقاله، ما قدرت استفاده از Razor views برای گزارش‌دهی PDF انعطاف‌پذیر در NET. را بررسی کردیم. ما دیدیم چگونه می‌توان قالب‌های گزارش را با Razor views ایجاد کرد، آن‌ها را به HTML تبدیل کرد و سپس آن HTML را به اسناد PDF با فرمت زیبا تبدیل نمود. 📄💻

فرقی نمی‌کند که بخواهید فاکتور، گزارش فروش یا هر نوع سند ساختاریافته دیگری تولید کنید، این روش یک راهکار ساده و قابل تنظیم ارائه می‌دهد. ⚡️

همچنان فوق‌العاده بمانید! 😎🚀

🔖هشتگ‌ها:
#DotNet #RazorViews #PDFReporting #QuestPDF #HTMLtoPDF
MVP Wall at Microsoft Ignite🔥
🧩 راحتیِ اشتباهِ “Happy Path”: جدا کردن سرویس‌ها از یکدیگر

بیایید صادق باشیم: همهٔ ما این کد را نوشته‌ایم. 😅
صبح دوشنبه است، یک ددلاین دارید، و باید یک قابلیت ثبت‌نام کاربر را پیاده‌سازی کنید. کار ساده‌ای است: کاربر را ذخیره کنید، یک ایمیل خوش‌آمد بفرستید، و ثبت‌نام را در داشبورد analytics خود ثبت کنید.
شما این را می‌نویسید:
public class UserService(
IUserRepository userRepository,
IEmailService emailService,
IAnalyticsService analyticsService)
{
public async Task RegisterUser(string email, string password)
{
var user = new User(email, password);
await userRepository.SaveAsync(user);

// 1. Directly coupled to email service (external API)
await emailService.SendWelcomeEmail(user.Email);

// 2. Directly coupled to analytics (this could be an external API)
await analyticsService.TrackUserRegistration(user.Id);

// What if we need to add more features?
// This method will keep growing...
}
}

ظاهرش تمیز است. خوانا است. روی ماشین شما هم کار می‌کند.
اما این متد یک بمب ساعتی است. 💣

این متد فرض می‌کند فقط Happy Path وجود دارد.
فرض می‌کند شبکه همیشه پایدار است، سرویس ایمیل همیشه در دسترس است، و API مربوط به analytics همیشه سریع است.
در محیط production هیچ‌کدام تضمین‌شده نیست. ⚠️

اگر بیشتر فکر کنید، احتمالاً مثال مشابهی را در پروژه‌های خودتان هم دیده‌اید. ممکن است همین سناریو نباشد، اما الگو یکسان است:
یک متد که به شکل خطی چندین Side Effect را مدیریت می‌کند.

بیایید بررسی کنیم چرا این کد خطرناک است و چطور می‌توان آن را به یک معماری Event-driven مقاوم تبدیل کرد. 🚀

🔍 خطرات پنهان در “God Method”

سه مشکل اساسی در همین ده خط کد وجود دارد.
1️⃣ Temporal Coupling (تأخیر و زمان‌وابستگی)

وقتی کاربر روی "Register" کلیک می‌کند، باید منتظر بماند برای:
Database
SMTP Server
Analytics API
اگر سرویس analytics امروز حالش خوب نباشد و پاسخ‌گویی‌اش ۳ ثانیه طول بکشد،
کاربر شما هم باید ۳ ثانیه صبر کند.

شما در واقع کاربر را به خاطر کندی یک سیستم پس‌زمینه که حتی برایش مهم نیست مجازات می‌کنید. 😐

2️⃣ Partial Failure State (وضعیت شکست جزئی) 💥

این مهم‌ترین ریسک است. تصور کنید:

SaveAsync(user) موفق می‌شود →
کاربر در DB ذخیره می‌شود.

SendWelcomeEmail موفق می‌شود
→ کاربر ایمیل خوش‌آمد را دریافت می‌کند.

TrackUserRegistration یک خطای
503 Service Unavailable می‌دهد.

حالا چه؟ 🤔
اگر همه چیز را داخل transaction بگذارید و rollback کنید:
🔸️کاربر از دیتابیس حذف می‌شود
🔸️اما ایمیل خوش‌آمد را قبلاً دریافت کرده است
🔸️کاربر تلاش می‌کند وارد شود → وجود ندارد
🔸️یک تجربهٔ کاربری فاجعه‌بار.

اگر rollback نکنید:
🔹️کاربر در سیستم هست
🔹️اما در analytics ثبت نشده
🔹️عدم سازگاری داده (Data Inconsistency) ایجاد شده است
3️⃣ Violation of Single Responsibility (SRP)

❗️ شما ممکن است استدلال کنید که چون ما از interfaceها (مثل IEmailService) استفاده می‌کنیم، decoupled هستیم. این برای جزئیات پیاده‌سازی درست است، اما برای orchestration اشتباه است.
UserService در حال حاضر دو دلیل برای تغییر دارد:

• Core Domain Logic:
"اکنون علاوه بر email به username نیز نیاز داریم."

• Notification Policy:
"تیم Marketing می‌خواهد علاوه بر Email یک SMS هم ارسال شود."

UserService
باید فقط مسئول state change باشد (ایجاد کاربر).نباید مسئول orchestrating کردن side effectها باشد.
🔥 این نقض کامل اصل Single Responsibility است.

Level 1: Logical Decoupling with Domain Events

اولین گام برای رفع مشکل، معکوس‌کردن کنترل است.
به جای اینکه UserService به سرویس‌های دیگر دستور بدهد چه کنند، فقط اعلام می‌کند که چه اتفاقی افتاده است.

برای این کار از Domain Events استفاده می‌کنیم.
در اینجا نسخهٔ refactor شدهٔ UserService را می‌بینید:
public class UserService(
IUserRepository userRepository,
IDomainEventDispatcher dispatcher,
IUnitOfWork unitOfWork)
{
public async Task RegisterUser(string email, string password)
{
// 1. Create the User Entity
var user = new User(email, password);

// 2. Capture the side effect as an event object
var userRegisteredEvent = new UserRegisteredEvent(user.Id, user.Email);

// 3. Add the entity to the repository
await userRepository.AddAsync(user);

// 4. Dispatch the event (Assuming in-process dispatching here for simplicity)
// Note: Handlers for Email and Analytics are now completely separate classes.
await dispatcher.Dispatch(userRegisteredEvent);

await unitOfWork.SaveChangesAsync();
}
}

اکنون UserService پایدار است.
اگر فردا بخواهیم یک قابلیت جدید مثل "Loyalty Points" اضافه کنیم، این متد هیچ تغییری نمی‌کند.
فقط یک handler جدید برای UserRegisteredEvent اضافه می‌کنیم.

اما هنوز مشکل reliability حل نشده. اگر سیستم دقیقاً بعد از Dispatch ولی قبل از SaveChangesAsync کرش کند چه؟
ممکن است email ارسال شود اما user ذخیره نشود.
یا برعکس: user ذخیره شود اما event از دست برود.

Level 2: Reliability with the Outbox Pattern

برای رفع این مشکل، ما به Atomicity نیاز داریم.Atomicity یعنی مجموعه‌ای از عملیات یا همه باهم موفق شوند یا همه باهم شکست بخورند.

باید تضمین کنیم که اگر User ذخیره شد، UserRegisteredEvent نیز ذخیره شود.

اینجاست که Outbox Pattern وارد می‌شود.
به جای ارسال مستقیم event به message bus، ما event را در یک جدول OutboxMessages ذخیره می‌کنیم.
و این کار در همان transaction ذخیرهٔ user انجام می‌شود.

اینجا پیاده‌سازی کامل logic را می‌بینید:
public async Task RegisterUser(string email, string password)
{
// 1. Create the Domain Event
var user = new User(email, password);
var domainEvent = new UserRegisteredEvent(user.Id, user.Email);

// 2. Open a Transaction
using var transaction = dbContext.Database.BeginTransaction();

try
{
// 3. Save the User to the Users Table
dbContext.Users.Add(user);

// 4. Serialize the Event and Save to Outbox Table
var outboxMessage = new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(UserRegisteredEvent),
Content = JsonSerializer.Serialize(domainEvent),
OccurredOn = DateTime.UtcNow,
ProcessedOn = null // Null means it hasn't been handled yet
};

dbContext.OutboxMessages.Add(outboxMessage);

// 5. Commit BOTH changes atomically
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}

اکنون یک background worker (در یک process جدا) جدول OutboxMessages را poll می‌کند.
پیام را برداشته و آن را به message bus (RabbitMQ، Azure Service Bus و...) publish می‌کند.

اگر email service از کار بیفتد؟worker دوباره تلاش می‌کند.با این روش به At-Least-Once Delivery رسیده‌ایم.