Excel Üretmek ve Okumak: ClosedXML ile Formatlı Rapor, Büyük Export'lar ve EPPlus Lisansı
Biçimlendirilmiş raporu ClosedXML ile üret; tarihi DateTime, ISBN'i metin, parayı number format ile yaz, dosyayı belleğe render edip Content-Disposition'daki filename* ile gönder. Büyük export'ta ClosedXML 100 bin satırda 368 MB, OpenXmlWriter 56 MB kullanıyor ama System.IO.Packaging yüzünden 1 milyonda 583 MB'a çıkıyor; sabit bellek için SpreadCheetah 8 MB'da kalıyor. EPPlus ise ticari kullanımda developer başına lisans istiyor.
Sürüm notu: Bu yazıdaki kodlar .NET 10, ClosedXML 0.105.1, DocumentFormat.OpenXml 3.5.1 ve SpreadCheetah 1.29.0 ile derlenip denenmiştir. EPPlus lisans bilgisi 15 Kasım 2026'da resmi sayfasından kontrol edildi.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) back office'i ekim ayının satışlarını Excel olarak istiyor. Üç soru:
Birincisi: 100 bin satırlık raporu ClosedXML ile üretmek ne kadar sürer? İlk denememizde sonuç 123 saniyeydi. Suçlu kim: ClosedXML mi, yoksa raporun tek bir satırı mı?
İkincisi: Open XML SDK'nın OpenXmlWriter'ı satırları SAX (XML'i ağaç kurmadan, akış hâlinde işleme) tarzında, yani DOM (belgenin bellekte ağaç olarak tutulması) kurmadan tek tek yazıyor. 100 bin, 300 bin ve 1 milyon satırda bellek sabit kalır mı?
Üçüncüsü: bir kitabın başlığı müşteri girdisinden geliyor ve =HYPERLINK("https://attacker.example.com","Dune") diye başlıyor. ClosedXML bunu dosyaya formül olarak mı yazar?
Cevaplar: ilkinde suçlu iki AdjustToContents() çağrısıydı; onları kaldırınca süre 3,9 saniyeye indi. İkincisinde bellek sabit kalmıyor: 56, 292 ve 583 MB. Üçüncüsünde değer metin olarak saklanıyor, formül değil. Ayrıntılar aşağıda.
ClosedXML ile Workbook, Hücre Tipleri ve Formatlar
ClosedXML, Open XML SDK üzerine kurulmuş, MIT (ticari kullanıma da izin veren kısa bir lisans) lisanslı bir kütüphane: workbook'u bellekte bir object modeli olarak tutuyor, sen hücrelere değer veriyorsun, SaveAs dosyayı yazıyor. Aylık rapor renderer'ı bir önceki yazılardaki IDocumentRenderer<MonthlySalesReport>'ın Excel implementasyonu:
public Task RenderAsync(MonthlySalesReport model, Stream output, CancellationToken cancellationToken = default)
{
using XLWorkbook workbook = new();
IXLWorksheet sales = workbook.Worksheets.Add("Sales");
// ... başlık satırı
int row = 2;
foreach (SalesRow item in model.Rows)
{
cancellationToken.ThrowIfCancellationRequested();
sales.Cell(row, 1).Value = item.OrderId;
sales.Cell(row, 2).Value = item.PlacedAtUtc; // string değil, gerçek tarih
SetText(sales.Cell(row, 3), item.CustomerEmail);
sales.Cell(row, 4).Value = item.Isbn; // metin: 13 hane 9.78044E+12 olmamalı
SetText(sales.Cell(row, 5), item.Title);
sales.Cell(row, 6).Value = item.Quantity;
sales.Cell(row, 7).Value = item.UnitPrice;
sales.Cell(row, 8).FormulaA1 = $"F{row}*G{row}";
row++;
}
IXLTable table = sales.Range(1, 1, row - 1, headers.Length).CreateTable("SalesTable");
table.ShowTotalsRow = true;
table.Field("Quantity").TotalsRowFunction = XLTotalsRowFunction.Sum;
table.Field("Total").TotalsRowFunction = XLTotalsRowFunction.Sum;
sales.Column(2).Style.DateFormat.Format = "yyyy-mm-dd hh:mm";
sales.Column(4).Style.NumberFormat.Format = "@";
sales.Columns(7, 8).Style.NumberFormat.Format = MoneyFormat;
sales.SheetView.FreezeRows(1);
// ... sabit sütun genişlikleri ve Summary sayfası
workbook.SaveAs(output);
return Task.CompletedTask;
}
Hücreye ne tipte değer verirsen dosyaya o tipte yazılıyor. Tarihi ToString() ile string'e çevirip yazarsan Excel onu sıralayamaz, filtreleyemez ve formülde kullanamaz; DateTime olarak yazınca Excel'in tarih seri numarası olarak saklanıyor ve görünümü yalnız format belirliyor. ISBN (kitapların uluslararası standart numarası) ise tersine metin olmalı: 13 haneli bir sayı Excel'de bilimsel gösterime düşer ve baştaki sıfırı olan kodlar sıfırını kaybeder. Para formatı #,##0.00 "₺"; ondalık ve binlik ayırıcıyı dosyayı açan kişinin Excel ayarları belirliyor.
CreateTable aralığı bir Excel tablosuna çeviriyor: başlıklarda autofilter düğmeleri, satır bantları ve toplam satırı geliyor. FreezeRows(1) başlığı kaydırırken sabit tutuyor. Dosyayı aynı kütüphaneyle geri okuyup ilk veri satırına baktık:
A2 Number 1791 format='' formula=''
B2 DateTime 10/1/2026 12:15:00 AM format='yyyy-mm-dd hh:mm' formula=''
D2 Text 9780441172719 format='@' formula=''
G2 Number 450 format='#,##0.00 "₺"' formula=''
H2 Blank 1350 format='#,##0.00 "₺"' formula='F2*G2'
H2'nin tipi Blank, çünkü formülün sonucu dosyada saklanmıyor; Excel dosyayı açınca hesaplıyor, ClosedXML de değeri okurken kendi hesaplama motoruyla 1350'yi buluyor. Özet sayfasında her kitap için SUMIFS formülü var. İlk hâlinde formülde tablo referansı (SalesTable[Total]) kullanmıştık; ClosedXML'in hesaplama motoru bunları 0 olarak hesapladı, dosyayı testte doğrulayabilmek için düz aralığa (Sales!$H$2:$H$2001) geçtik. Okunan özet ve kodda hesaplanan toplam aynı:
Title Copies Revenue
Dune 630 283500
Emma 600 144000
Middlemarch 665 212800
Nineteen Eighty-Four 686 178360
Pride and Prejudice 681 149820
The Hobbit 728 276640
rows=2000 sum(Total)=1245120.00
API'den Excel Göndermek: Content-Disposition ve Türkçe Dosya Adı
2.000 satırlık rapor bellekte birkaç yüz KB tutuyor; onu request içinde üretip göndermek sorun değil:
// Excel doğrudan yanıta: ClosedXML senkron yazdığı için önce belleğe render et
app.MapGet("/api/reports/monthly-sales/{month}/excel", async (string month, IDocumentRenderer<MonthlySalesReport> renderer, CancellationToken ct) =>
{
MonthlySalesReport report = BookstoreSampleData.MonthlySales(DateOnly.ParseExact(month + "-01", "yyyy-MM-dd"), rowCount: 2_000);
MemoryStream buffer = new();
await renderer.RenderAsync(report, buffer, ct);
buffer.Position = 0;
return Results.File(buffer, renderer.Format.ContentType, $"Aylık Satış Raporu {month}.xlsx");
});
HTTP/1.1 200 OK
Content-Length: 104746
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="Ayl_k Sat__ Raporu 2026-10.xlsx"; filename*=UTF-8''Ayl%C4%B1k%20Sat%C4%B1%C5%9F%20Raporu%202026-10.xlsx
HTTP header'ları ASCII (yalnız temel Latin harfleri ve işaretleri içeren karakter seti) bekliyor. ASP.NET Core bu yüzden iki parametre yazıyor: eski istemciler için Türkçe karakterleri _ ile değiştirilmiş filename, RFC (internet standardı dokümanı) 6266'yı, yani HTTP'de Content-Disposition kullanımını tanımlayan standardı destekleyen tarayıcılar için UTF (Unicode karakter kodlaması ailesi) ailesinden UTF-8 ile percent-encode edilmiş filename*. Güncel tarayıcılar filename*'ı kullanıyor ve dosya "Aylık Satış Raporu 2026-10.xlsx" adıyla iniyor. Header'ı elle yazacaksan ContentDispositionHeaderValue'nun FileNameStar property'sini kullan, dosya adını string birleştirerek header'a koyma.
Neden MemoryStream? Workbook'u doğrudan Response.Body'ye yazan bir endpoint'i de denedik:
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseStream.Write(Byte[] buffer, Int32 offset, Int32 count)
at ClosedXML.Excel.XLWorkbook.CopyStream(Stream input, Stream output)
at ClosedXML.Excel.XLWorkbook.SaveAs(Stream stream, SaveOptions options)
Kestrel varsayılan olarak senkron I/O'yu kapatıyor, ClosedXML ise senkron yazıyor. AllowSynchronousIO = true ile açmak thread pool thread'ini ağ beklerken bloklamak demek. Küçük dosyada belleğe render edip async göndermek, büyük dosyada ise bir sonraki bölümdeki gibi job ve storage doğru yol. Open XML SDK de okunabilir bir stream istiyor; yazma yetkisi olan ama okunamayan bir FileStream verdiğimizde The stream was not opened for reading. hatası aldık. Response.Body ikisini de karşılamıyor.
Excel'den Okumak: Doldurulabilir Kurumsal Sipariş Şablonu
Kurumsal müşteriler toplu siparişi Excel'de hazırlamayı seviyor. Nevin Hanım'ın okulu için bir şablon üretiyoruz: ISBN, birim ve adet hücreleri açık, başlık ve fiyat formülleri kilitli, birimler ve katalog gizli bir "Lists" sayfasında:
IXLRange isbn = order.Range(FirstInputRow, 1, last, 1);
isbn.Style.NumberFormat.Format = "@";
isbn.CreateDataValidation().TextLength.EqualTo(13);
IXLRange unit = order.Range(FirstInputRow, 3, last, 3);
unit.CreateDataValidation().List(lists.Range(1, 1, units.Count, 1));
IXLRange quantity = order.Range(FirstInputRow, 4, last, 4);
quantity.CreateDataValidation().WholeNumber.Between(1, 10_000);
for (int r = FirstInputRow; r <= last; r++)
{
// Gizli listeden aranır: müşteri başlığı ya da fiyatı değiştiremez
order.Cell(r, 2).FormulaA1 = $"IF(A{r}=\"\",\"\",IFERROR(VLOOKUP(A{r},Lists!$C:$E,2,FALSE),\"Unknown ISBN\"))";
// ... fiyat ve satır toplamı
}
order.Range(FirstInputRow, 1, last, 1).Style.Protection.SetLocked(false);
order.Range(FirstInputRow, 3, last, 4).Style.Protection.SetLocked(false);
order.Protect().AllowElement(XLSheetProtectionElements.FormatColumns);
Sheet protection bir güvenlik önlemi değil, yönlendirme: kullanıcının yanlışlıkla formülü silmesini engelliyor. Data validation da yalnız Excel arayüzünde girilen değeri denetliyor. Dosyayı Excel'de elle doldurmak yerine aynı ClosedXML koduyla, bir müşterinin yapabileceği iki hatayla doldurduk: bir ISBN'i sayı olarak yazdık, bir adedi "20 copies" diye metin girdik. Validation ikisini de durdurmadı. Sunucu tarafında okuma:
// Genel formatlı bir hücreye sayı olarak yazılan ISBN double olarak gelir
string isbn = isbnCell.Value.Type switch
{
XLDataType.Number => isbnCell.Value.GetNumber().ToString("0", System.Globalization.CultureInfo.InvariantCulture),
_ => isbnCell.GetString().Trim(),
};
if (!row.Cell(4).TryGetValue(out int quantity) || quantity < 1)
{
problems.Add($"Row {row.RowNumber()}: quantity '{row.Cell(4).GetString()}' is not a whole number");
continue;
}
row 6: 9780441172719 x25 for Science Department
row 7: 9780547928227 x30 for Library
Row 8: quantity '20 copies' is not a whole number
B6='Dune' E6=450 F6=11250 protected=True A6 locked=False B6 locked=True
Yedinci satırdaki ISBN dosyada sayıydı, okurken metne döndü. Sekizinci satır hata listesine düştü. Excel okumayı burada API seviyesinde bırakıyoruz. Satır satır doğrulama, hataları kullanıcıya bir rapor olarak dönme ve toplu import'un tamamı Çözüm Desenleri serisinin toplu import yazısının konusu.
Büyük Export'larda Bellek
Aynı raporu 10 bin ve 100 bin satırla, ayrı process'lerde ve Release build'de ürettik. Tablo, örnekleyerek ölçtüğümüz managed heap tepe değerini (başlangıca göre), toplam allocation'ı ve süreyi gösteriyor:
| Yöntem | Satır | Süre | Heap tepe | Allocation | Dosya |
|---|---|---|---|---|---|
ClosedXML (ilk hâl, AdjustToContents ile) | 100.000 | 123.509 ms | 457 MB | 2.832 MB | 4.583 KB |
| ClosedXML | 10.000 | 921 ms | 48 MB | 177 MB | 471 KB |
| ClosedXML | 100.000 | 3.910 ms | 368 MB | 1.607 MB | 4.583 KB |
Open XML SDK OpenXmlWriter | 100.000 | 686 ms | 56 MB | 515 MB | 1.176 KB |
Open XML SDK OpenXmlWriter | 1.000.000 | 3.438 ms | 583 MB | 5.089 MB | 13.206 KB |
| SpreadCheetah | 100.000 | 218 ms | 7 MB | 12 MB | 2.903 KB |
| SpreadCheetah | 1.000.000 | 1.259 ms | 8 MB | 127 MB | 29.026 KB |
İlk satır tahmin sorusunun cevabı. Rapor kodunda iki AdjustToContents() vardı. Sales sayfasındakini 500 satırla sınırlamıştık ama süre yine satır sayısıyla birlikte hızla büyüyordu. Summary sayfasındaki ise formül hücrelerini ölçüyordu; bir formül hücresinin genişliğini bulmak için önce değerini hesaplamak gerekiyor ve her SUMIFS 100 bin satırı tarıyor. İkisini sabit sütun genişlikleriyle değiştirince süre 123 saniyeden 3,9 saniyeye indi. Büyük raporda AdjustToContents kullanma ya da yalnız formülsüz, küçük bir aralıkta kullan.
ClosedXML her hücreyi bir object olarak tuttuğu için bellek satır sayısıyla birlikte büyüyor. OpenXmlWriter DOM kurmuyor ve 100 bin satırda yedi kat daha az bellek kullanıyor:
// Satırlar Open XML SDK'nın SAX tarzı OpenXmlWriter'ıyla yazılır, DOM kurulmaz
using SpreadsheetDocument document = SpreadsheetDocument.Create(output, SpreadsheetDocumentType.Workbook);
// ... workbook part ve stiller
using (OpenXmlWriter writer = OpenXmlWriter.Create(worksheetPart))
{
writer.WriteStartElement(new Worksheet());
writer.WriteStartElement(new SheetData());
foreach (SalesRow row in rows)
{
writer.WriteStartElement(new Row());
WriteNumber(writer, row.OrderId);
WriteNumber(writer, row.PlacedAtUtc.ToOADate(), DateStyle);
WriteText(writer, row.CustomerEmail);
// ...
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteEndElement();
}
Ama 1 milyon satırda tepe değer 583 MB'a çıkıyor; yani bellek sabit değil. Satırları tek tek üreten bir IEnumerable verdik ve GC'nin (garbage collector, kullanılmayan belleği geri toplayan bileşen) gen0 bütçesini küçültüp ölçümü tekrarladık, sonuç değişmedi. Neden System.IO.Packaging'de: worksheet part'ı zip'e yazılana kadar bellekte tutuluyor. Open XML SDK deposundaki 1951 numaralı issue aynı davranışı .NET 8 ve SDK 3.3 ile raporluyor; maintainer'ın .NET 10 preview ile tekrarladığı ve dotnet/runtime'daki 1544 numaralı zip issue'suna bağladığı not hâlâ açık.
Sabit bellek gerekiyorsa satırları sıkıştırıp doğrudan stream'e yazan bir kütüphane lazım. MIT lisanslı SpreadCheetah tablodaki gibi 1 milyon satırda 8 MB ile kaldı. Bedeli esneklik: satırları sırayla yazıyorsun, yazdığın bir hücreye geri dönemiyorsun ve özellik seti ClosedXML kadar geniş değil. Yine de onu da Response.Body'ye doğrudan bağlayamazsın: async-only bir stream ile denediğimizde zip arşivini kapatırken senkron bir Write çağırdı. Büyük export için birinci yazıdaki yol geçerli: job'da üret, IFileStore'a yaz, linki e-postayla gönder. 100 bin satırı tek seferde List<SalesRow> olarak belleğe almak yerine veritabanından IAsyncEnumerable ile akıtmak da bu hesaba dahil; tablodaki ölçümlerde veri baseline'ın içinde.
Formula Injection: CSV Injection'ın Excel Karşılığı
.NET'te I/O İşlemleri serisinin CSV (virgülle ayrılmış düz metin formatı) yazısında, = ile başlayan bir hücrenin CSV açıldığında formül olarak çalıştırılabildiğini gördük. XLSX'te (Excel'in zip tabanlı dosya formatı) durum biraz farklı, çünkü her hücrenin tipi dosyada yazılı. ClosedXML'e Value ile =HYPERLINK(...) verdiğimizde dosyadaki XML:
A1 DataType=Text HasFormula=False
<x:row r="1" spans="1:1"><x:c r="A1" s="0" t="s"><x:v>0</x:v></x:c></x:row><x:row r="2" spans="1:1"><x:c r="A2" s="1" t="s"><x:v>0</x:v></x:c></x:row>
<x:xf numFmtId="0" fontId="0" fillId="0" borderId="0" quotePrefix="1" applyNumberFormat="1" applyFill="1" applyBorder="0" applyAlignment="1" applyProtection="1"><x:protection locked="1" hidden="0" />
t="s" hücrenin shared string, yani metin olduğunu söylüyor; formül (<x:f>) yok. Risk üç yerden geri geliyor. Birincisi, kullanıcı girdisini FormulaA1'e string interpolation ile koymak; şablondaki VLOOKUP gibi formüllere yalnız kendi ürettiğin hücre adreslerini koy. İkincisi, dosyanın sonradan CSV olarak dışa aktarılması. Üçüncüsü, bir kullanıcının hücreyi açıp düzenlemesi. Son ikisi için =, +, -, @, sekme ya da satır başı karakteriyle başlayan metinlere quotePrefix stilini veriyoruz:
// Her durumda metin olarak saklanır; quotePrefix, kullanıcı hücreyi düzenlerse de metin kalmasını söyler
public static void SetText(IXLCell cell, string value)
{
cell.Value = value;
if (value.Length > 0 && value[0] is '=' or '+' or '-' or '@' or '\t' or '\r')
{
cell.Style.IncludeQuotePrefix = true;
}
}
quotePrefix, Excel'de bir değerin başına elle ' yazmanın dosyadaki karşılığı ve kesme işareti hücre içeriğine eklenmiyor. Yukarıdaki A2 hücresi bu yüzden ayrı bir stil (s="1") kullanıyor.
EPPlus'ın Lisans Modeli ve Ne Zaman Tercih Edilir?
EPPlus, .NET'te Excel denince ilk akla gelen kütüphanelerden biri ve özellik listesi ClosedXML'den geniş. Resmi sitesine göre bütün Excel 2019 chart tiplerini, pivot table'ları, 478 Excel fonksiyonunu hesaplayabilen bir formül motorunu, şifreleme ve dijital imzayı, VBA (Office'in makro dili) projelerini ve HTML/JSON export'u destekliyor. EPPlus 9'da PDF export'u preview olarak geliyor.
Lisansı ise birinci yazıda gördüğümüz gibi PolyForm Noncommercial 1.0.0: kişisel, hobi ve ticari olmayan kurumların kullanımı ücretsiz, ticari bir ortamda kullanım için ticari lisans gerekiyor. Ticari lisans developer başına, abonelik ya da süreli güncelleme haklı perpetual olarak satılıyor. EPPlus 8'de lisansı kodda, configuration'da ya da EPPlusLicense environment variable'ında belirtmeden ilk kullanımda LicenseNotSetException alıyorsun. Kişisel kullanım için SetNonCommercialPersonal çağrısı lisans bilgisini paketin Comment ve Tag alanlarına yazıyor ve pakete bir lisans dosyası ekliyor.
Bookstore gibi ticari bir uygulamada EPPlus'ı seçmek, developer sayısı kadar lisans almak demek. Raporlarında chart ya da pivot table gerekiyorsa, Excel'deki formül sonuçlarını sunucuda hesaplaman gerekiyorsa veya dosyaları şifrelemen gerekiyorsa bu bedel makul olabilir. Bizim raporumuz ve şablonumuz gibi biçimlendirilmiş tablo, formül ve validation için ClosedXML yeterli. Çok büyük export'larda ise ikisi de DOM tuttuğu için SpreadCheetah gibi streaming bir yazıcı ayrıca gerekiyor. Lisans kararı birinci yazıdaki nuget-license kontrolüne de takılıyor; EPPlus'ı eklemek bilinçli bir override gerektiriyor.
Bir not: ClosedXML'in kendisi MIT, ama transitive bağımlılıkları arasında SixLabors.Fonts var ve izin verdiği aralık 1.0.0 ile 3.0.0 arası. Bizim projede NuGet en düşük sürümü, Apache-2.0 lisanslı 1.0.0'ı seçti. Başka bir paket onu 2.x'e çekerse lisans Six Labors Split License'a dönüyor. Split License transitive kullanımı Apache 2.0 kapsamında saysa da bunu CI'daki lisans kontrolünün yakalaması iyi olur.
Üretimde Dikkat
Excel'in sınırı sayfa başına 1.048.576 satır. Bir milyon satıra yaklaşan bir export'u ya birden fazla sayfaya böl ya da CSV'ye yönlendir; kimse o satırları Excel'de gözle okumayacak.
Büyük export'u job'a al ve iptal edilebilir yaz. ClosedXML'in SaveAs'i iptal kabul etmiyor; bizim döngümüz satır başına ThrowIfCancellationRequested çağırıyor, kaydetme aşaması ise sonuna kadar sürüyor.
Sayı, tarih ve para birimini kültürden bağımsız yaz. Hücreye decimal ve DateTime ver, görünümü number format'a bırak. ToString("N2") ile yazılmış bir tutarı Excel metin olarak görür ve toplayamaz.
Yüklenen Excel dosyası da bir upload. Boyut limiti, içerik tipi kontrolü ve zip bombası riski için ASP.NET Core Web API serisinin dosya yükleme yazısına ve ASP.NET Core Güvenliği serisinin Kestrel limitleri yazısına bak. ClosedXML dosyayı açarken tamamını belleğe alıyor; 50 MB'lık bir sipariş şablonu ret sebebi olmalı.
Rapor dosyasını kimlerin indirebileceğini kontrol et. Aylık satış raporu müşteri e-postaları içeriyor; link'i bilen herkesin indirebildiği bir URL veri sızıntısıdır.
Sık Düşülen Tuzaklar
Tarihi string olarak yazmak. Görünüşte aynı, ama Excel sıralayamaz, filtreleyemez, ay bazında gruplayamaz.
ISBN, telefon ya da sipariş kodunu sayı olarak yazmak. Baştaki sıfırlar kaybolur, 13 hane bilimsel gösterime düşer.
100 bin satırı tek seferde belleğe almak ve ClosedXML ile request içinde üretmek. Bizim ölçümümüzde 368 MB heap ve 4 saniye; aynı anda gelen beş istek sunucuyu zorlar.
Büyük sayfada AdjustToContents çağırmak. Bizim raporumuzda süreyi 30 katından fazla uzattı.
Kullanıcı girdisini formüle koymak ya da formül gibi başlayan metni işaretlememek. Dosya CSV'ye çevrildiğinde ya da hücre düzenlendiğinde metin formüle dönebilir.
Küçük Bir Deneme
★☆☆ MonthlySalesExcelRenderer'da ISBN satırını sales.Cell(row, 4).Value = long.Parse(item.Isbn); yap, dosyayı üret ve ExcelLab'in report komutuyla D2'nin tipine ve formatına bak. Sonra dosyayı Excel ya da LibreOffice'te açıp sütunun nasıl göründüğünü kontrol et.
★★☆ ExcelLab'deki large komutunu 300 bin satırla üç yöntem için çalıştır ve tabloya ekle. Hangi yöntemin heap tepe değeri satır sayısıyla doğru orantılı büyüyor, hangisininki sabit kalıyor?
★★★ InstitutionalOrderTemplate.Read'i, okunan satırları katalogla karşılaştırıp bilinmeyen ISBN'leri, minimum 20 adet kuralını ve aynı ISBN'in iki satırda tekrarını raporlayacak şekilde genişlet. Hatalı hücreleri kırmızıya boyayıp dosyayı müşteriye geri gönderen bir endpoint yaz.
Excel tamam. Bir sonraki yazıda PDF'e geçiyoruz: Bookstore'un sipariş faturasını QuestPDF ile üretiyor, Türkçe karakterler için font gömüyor ve Community License'ın kodda nasıl belirtildiğine bakıyoruz.