Blog
Mert Özen's blog posts on software development, backend, frontend, and DevOps.
End to End: Bookstore Order Documents — Invoice PDF, Confirmation Email and the Monthly Excel Report as a Job Chain
Connect the order documents in a two-step Hangfire chain: the invoice job writes the PDF to storage, the continuation puts the confirmation email in the outbox; each step's key is the very thing it writes, so repeats are harmless. In Bookstore only the invoice step was retried while storage was down, the same order placed twice gave one PDF and one email, and the monthly report is a recurring job on Istanbul time.
A File Storage Abstraction: Local Disk, Azure Blob Storage and S3; Direct Uploads with Presigned/SAS URLs
Keep files behind one IFileStore: validate the path twice on the local disk, hand out SAS or presigned URLs that live a few minutes for downloads and uploads on Azure Blob and S3, and check an uploaded file only in the "complete" step, because storage looks at neither size nor content. Bookstore's invoices and covers were tested with Azurite and RustFS; metadata stays in the database.
Image Processing in .NET: Resizing, Format Conversion, EXIF and Safe Processing with ImageSharp and SkiaSharp
Validate an uploaded image by its first bytes and pixel count first, then apply the EXIF orientation, shrink it and save it as WebP without metadata. Bookstore's cover thumbnails are made with SkiaSharp (MIT); ImageSharp 4 stays in a Debug-only example, because its Release build needs a license key and it keeps metadata by default.
Generating PDFs Part 2: HTML to PDF with Playwright, Reading and Merging PDFs and Which Approach When
Turn a document the designer built in HTML and CSS into a PDF with Playwright's headless Chromium: share the browser, open a context per document, turn JavaScript off and allow only your own fonts. Bookstore's institutional quote takes 38 milliseconds per document; merging and reading use PdfPig, because PDFsharp moved to the AGPL with 6.5.0.
Generating PDFs Part 1: Invoices and Reports with QuestPDF, Fonts and Turkish Characters
Build the invoice with QuestPDF's fluent layout, repeat the table header on every page, put page numbers in the footer and ship fonts with the app: since 2026.9 system fonts are off by default and a missing font or glyph throws. Bookstore's invoice prints Turkish characters correctly with the bundled Lato, in a container too; the Community License line is a declaration for companies under USD 1 million in annual revenue.
Creating and Reading Excel Files: Formatted Reports with ClosedXML, Large Exports and the EPPlus License
Produce formatted reports with ClosedXML; write dates as DateTime, ISBNs as text and money through number formats, render into memory and send with filename* in Content-Disposition. For large exports ClosedXML uses 368 MB at 100,000 rows and OpenXmlWriter 56 MB, but System.IO.Packaging pushes the latter to 583 MB at a million; SpreadCheetah stays at 8 MB. EPPlus needs a per-developer license for commercial use.
Email Templates and Reliable Delivery: Razor HtmlRenderer, Inline CSS, Outbox, Retry and Provider APIs
Write email HTML as a Razor component rendered to a string with HtmlRenderer, inline the CSS with PreMailer.Net, store the email as an outbox row in the same transaction as the business data, and retry on a schedule for 4xx while giving up on 5xx. In Bookstore the TR/EN approver reminder goes out once with the same Message-Id after two 451s from Mailpit's chaos mode, and an idempotency key stops the second run.
Sending Email in .NET: SMTP with MailKit, TLS and Ports, Local Testing with Mailpit and the State of SmtpClient
In new .NET projects send email with MailKit rather than System.Net.Mail.SmtpClient: use StartTls on 587 and SslOnConnect on 465, avoid Auto, reuse the connection and read the password from a secret. We send Bookstore's order confirmation with HTML, plain text, an inline logo and an attached invoice to a STARTTLS Mailpit and show the MIME structure and the up to sixfold connection gap over 50 emails with real output.
Documents and Notifications in the Backend: In the Request or a Background Job, Library Licenses and Abstractions
Render a small, fast document inside the request and stream it; accept slow, large or retryable work with 202 Accepted, hand it to a background job and email a link when it is ready. In Bookstore an 8-second report returns a 504 after 5 seconds on the synchronous path and a 202 in 16 ms on the asynchronous one; we track the QuestPDF, EPPlus and ImageSharp licenses, including ImageSharp 4's check that stops Release builds, with nuget-license in CI.
What Changed in the .NET SDK from .NET 6 to 11: Release Publish by Default, Terminal Logger, .slnx, dnx, File-Based Apps and the .NET 11 CLI
What breaks when you upgrade the SDK is usually not the code but the places that repeat the version: the errorMessage in global.json, container image tags and CI steps. We tabulate SDK changes from .NET 6 to 11, give a checklist for reading an old project, move Bookstore to SDK 11 while keeping it on net10.0, and measure the MSBuild server's effect in CI.
Understanding MSBuild: Properties, Items, Targets, Tasks, Custom Targets and Debugging Builds with Binlogs
MSBuild processes a project in two phases: evaluation reads properties and items, execution runs targets, and values created in targets are invisible during evaluation. We add targets to Bookstore that write commit and local-change information into the assembly and add the Angular client to publish incrementally with Inputs/Outputs, and we use a binlog to find where a property changed, with real output.
dotnet tools and dotnet new Templates: Local Tool Manifests, dnx, Packaging Your Own Tool and Template
The way to tie a .NET tool to a repo is a local tool manifest: the version is committed in dotnet-tools.json and CI starts with dotnet tool restore. We package bookstore-admin with PackAsTool and install it in Reporting from the internal feed, and show with real output how dnx respects source mapping, SDK 11's NETSDK1235 warning, the bookstore-module template package and a file-based script that uses #:include.