<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Marius Schröder on Medium]]></title>
        <description><![CDATA[Stories by Marius Schröder on Medium]]></description>
        <link>https://medium.com/@mariusschroeder?source=rss-4bc650e7a5a6------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*p_QBZqMOum-fPC9it0DM0w.jpeg</url>
            <title>Stories by Marius Schröder on Medium</title>
            <link>https://medium.com/@mariusschroeder?source=rss-4bc650e7a5a6------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 04:43:06 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mariusschroeder/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Defender for Storage Bicep Fix: Malware Scanning]]></title>
            <link>https://medium.com/medialesson/defender-for-storage-bicep-fix-malware-scanning-547251e53f21?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/547251e53f21</guid>
            <category><![CDATA[bicep]]></category>
            <category><![CDATA[azure-event-grid]]></category>
            <category><![CDATA[azure-storage]]></category>
            <category><![CDATA[microsoft-defender]]></category>
            <category><![CDATA[azure]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Mon, 13 Apr 2026 07:52:50 GMT</pubDate>
            <atom:updated>2026-04-13T07:52:50.549Z</atom:updated>
            <content:encoded><![CDATA[<h3>🚨 Could not enable on-upload malware scanning in Defender for Storage with Bicep? Here’s what actually fixed it</h3><h4>How to fix the Azure and Bicep setup when Defender for Storage is enabled, but on-upload malware scanning still refuses to turn on.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lR3SPRwMCCaIruieey3jXQ.png" /></figure><p>Enabling Microsoft Defender for Storage with Bicep sounds straightforward. In practice, you can end up in a frustrating half-working state: the Defender resource is created, you are Owner on the subscription, and on-upload malware scanningstill stays disabled.</p><p>One of those Azure moments: everything looks right, you are Owner on the subscription, the Bicep deployment says the Defender resource was created, and the portal still shows this gem:</p><blockquote>Could not enable on-upload malware scanning</blockquote><p>Even better, the storage account is already StorageV2, soft delete is enabled, and you are now deep enough into the rabbit hole that every new portal blade feels slightly personal.</p><p>We ran into exactly that while enabling Microsoft Defender for Storage with Bicep. The good news: it does work. The annoying news: there are a couple of hidden prerequisites and one very misleading failure mode.</p><p>If you found this post because you searched for one of these errors, you are in the right place:</p><ul><li>Could not enable on-upload malware scanning</li><li>Plan enablement partially succeeded. Could not enable on-upload malware scanning: Failed to update malware scanning.</li><li>The attempt to configure storage notifications for the provided storage account failed</li><li>Failed to retrieve credentials ... listAccountSas ... InvalidAuthenticationToken</li><li>at least one of the claims &#39;puid&#39; or &#39;altsecid&#39; or &#39;oid&#39; should be present</li></ul><h3>TL;DR</h3><p>If you just want the shortest possible answer, this is what solved it for us:</p><ol><li>Register Microsoft.EventGrid on the subscription</li><li>Set dataScannerResourceId explicitly in the Bicep resource</li><li>Re-run the same deployment once after provider registration</li></ol><p>If step 2 is missing, the Defender resource may be created while malwareScanning.onUpload still stays disabled. If step 1 was done only moments ago, the first retry can still fail with Storage Notification or InvalidAuthenticationToken before the next run succeeds.</p><h3>🧩 The Setup</h3><p>The goal was straightforward:</p><ul><li>Enable Defender for Storage on a StorageV2 account via Bicep</li><li>Turn on on-upload malware scanning</li><li>Write scan results to blob index tags / Log Analytics</li><li>Use built-in remediation with BlobSoftDelete</li></ul><p>In our case the Bicep module looked roughly like this:</p><p>That any(...) is not there because I enjoy chaos. It is there because the API accepted a property the published Bicep type definition did not properly expose in our case.</p><pre>var defenderDataScannerResourceId = &#39;/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Security/datascanners/StorageDataScanner&#39;<br><br>var defenderMalwareScanning = {<br>  onUpload: {<br>    isEnabled: true<br>    capGBPerMonth: 100<br>    filters: {}<br>  }<br>  blobScanResultsOptions: &#39;BlobIndexTags&#39;<br>  automatedResponse: &#39;BlobSoftDelete&#39;<br>}<br><br>resource defenderForStorageSetting &#39;Microsoft.Security/defenderForStorageSettings@2025-09-01-preview&#39; = {<br>  scope: storageAccount<br>  name: &#39;current&#39;<br>  properties: any({<br>    isEnabled: true<br>    dataScannerResourceId: defenderDataScannerResourceId<br>    malwareScanning: defenderMalwareScanning<br>    overrideSubscriptionLevelSettings: true<br>    sensitiveDataDiscovery: {<br>      isEnabled: false<br>    }<br>  })<br>}</pre><h3>😵 The Misleading Part: Owner Was Not Enough</h3><p>At first glance, this smelled like a permissions problem. After all, the error included things like Unauthorized, InvalidAuthenticationToken, and listAccountSas.</p><p>Naturally, the first reaction is:</p><p>“But I am Owner on the subscription. What else do you want from me, Azure?”</p><p>Turns out: quite a bit.</p><p>Being Owner was not the actual missing piece in our case.</p><p>The real blocker was that Azure still needed infrastructure behind the scenes for storage notifications, and that depends on the Microsoft.EventGrid resource provider being registered in the subscription.</p><p>So yes, RBAC and resource provider registration are two different levers. Azure was perfectly happy to let us be Owner while still refusing to make progress because the required provider was not available. Classic.</p><h3>🔌 The Hidden Dependency: Microsoft.EventGrid</h3><p>Even though our Bicep did not directly declare an Event Grid resource, Defender for Storage on-upload malware scanning still depended on it.</p><p>Why? Because Azure configures storage notifications behind the scenes for on-upload scanning.</p><p>This was the missing step:</p><pre>az provider register -n Microsoft.EventGrid</pre><p>And if you want to verify it:</p><pre>az provider show -n Microsoft.EventGrid --query registrationState -o tsv</pre><p>If the provider is not registered, you can end up with errors like:</p><blockquote>Could not enable on-upload malware scanning: The attempt to configure storage notifications for the provided storage account failed.</blockquote><p>That is the part I wish someone had highlighted in giant flashing letters somewhere in the docs.</p><h3>🕵️ Another Gotcha: dataScannerResourceId</h3><p>There was one more surprise.</p><p>After working with support, it turned out that for our scenario the API only reliably persisted the malware scanning values when we explicitly set:</p><pre>dataScannerResourceId: &#39;/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Security/datascanners/StorageDataScanner&#39;</pre><p>Without that, Defender itself could be enabled, but the malwareScanning.onUpload block was not applied the way we expected.</p><p>So if your deployment says the resource was created, but the portal still shows something like this:</p><pre>{<br>  &quot;isEnabled&quot;: false,<br>  &quot;capGBPerMonth&quot;: -1<br>}</pre><p>then this property is worth checking.</p><h3>⚠️ The Weird 401 After EventGrid Registration</h3><p>Here comes the part that made this feel extra fun.</p><p>After registering Microsoft.EventGrid, we expected the next deployment to just work.</p><p>It did not.</p><p>Instead, we got this:</p><blockquote>Could not enable on-upload malware scanning: The attempt to configure storage notifications for the provided storage account YourStorageAccountName failed.</blockquote><blockquote>Failed to retrieve credentials for request=RequestUri=<a href="https://management.azure.com/.../listAccountSas">https://management.azure.com/.../listAccountSas</a></blockquote><blockquote>StatusCode=401<br>InvalidAuthenticationToken<br>The received access token is not valid: at least one of the claims ‘puid’ or ‘altsecid’ or ‘oid’ should be present.</blockquote><blockquote>ErrorCode: Storage Notification</blockquote><p>At that point the natural instinct is to start tearing apart your Bicep, your identities, your storage config, your life choices, and possibly your coffee machine.</p><p>In our case, that would have been the wrong move.</p><p>The actual fix was simply:</p><ol><li>Register Microsoft.EventGrid</li><li>Re-run the same deployment</li></ol><p>That second run worked.</p><p>So at least in our case, this looked very much like a race condition or delayed backend propagation immediately after provider activation.</p><h3>🧪 Things We Checked That Were Not the Root Cause</h3><p>To save you a few hours of random portal clicking:</p><ul><li>The storage account was already StorageV2</li><li>Soft delete was enabled manually and it still did not fix the problem</li><li>The subscription role was already Owner</li></ul><p>Soft delete is relevant for BlobSoftDelete remediation, but it was not the missing prerequisite that made on-upload malware scanning start working.</p><h3>✅ What Finally Worked</h3><p>For us, the practical checklist was:</p><ol><li>Use Microsoft.Security/defenderForStorageSettings@2025-09-01-preview</li><li>Set the malwareScanning block explicitly in Bicep</li><li>Set dataScannerResourceId explicitly</li><li>Make sure the subscription has Microsoft.EventGrid registered</li><li>If the first deployment after provider registration fails with Storage Notification / InvalidAuthenticationToken, run the same deployment again</li></ol><h3>🔗 Related Azure Posts</h3><p>If you are currently wiring up more of your Azure platform around the same solution, these might also be useful:</p><ul><li><a href="https://medium.com/medialesson/configuring-scoped-settings-from-app-configurations-or-secrets-and-injecting-them-into-providers-3ceed62a1c99">Configuring Scoped Settings from App Configurations or Secrets and injecting them into Providers</a></li><li><a href="https://medium.com/medialesson/simplify-your-life-logging-to-application-insights-with-serilog-a59d15280561">Simplify Your Life: Logging to Application Insights with Serilog</a></li></ul><h3>📌 Final Takeaway</h3><p>If you are trying to enable Defender for Storage on-upload malware scanning with Bicep and it keeps failing, the short version is:</p><ul><li>Owner on the subscription does not automatically mean all hidden platform prerequisites are in place</li><li>Microsoft.EventGrid may be required even if your Bicep never mentions Event Grid directly</li><li>dataScannerResourceId can be the difference between &quot;resource created&quot; and &quot;settings actually applied&quot;</li><li>A 401 right after Event Grid registration may just be a transient first-run failure, not proof that your entire setup is broken</li></ul><p>In other words: if Azure tells you that malware scanning could not be enabled, it might not be your Bicep that is wrong. Sometimes Azure just wants one more provider registration, one more backend sync, and one more deployment run for emotional support.</p><p>Happy deploying, and may your onUpload.isEnabled finally stay true.</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*MlzgcUGG0xxWnp0N.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=547251e53f21" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/defender-for-storage-bicep-fix-malware-scanning-547251e53f21">Defender for Storage Bicep Fix: Malware Scanning</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Knowledge Graphs and AI: Why Semantics Matter Again]]></title>
            <link>https://medium.com/medialesson/knowledge-graphs-and-ai-why-semantics-matter-again-77ff8020b3d4?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/77ff8020b3d4</guid>
            <category><![CDATA[ontology]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[knowledge-graph]]></category>
            <category><![CDATA[graph-database]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Wed, 04 Feb 2026 07:17:05 GMT</pubDate>
            <atom:updated>2026-02-04T07:17:05.476Z</atom:updated>
            <content:encoded><![CDATA[<h3>Why Knowledge Graphs Suddenly Matter Again (Especially for AI)</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HbSng6RYpTymwislZeepDw.png" /></figure><p>A few days ago I listened <a href="https://open.spotify.com/episode/6vWaRzLqgXYAtA8XcocLRK">to a podcast episode about knowledge graphs</a>, ontologies and semantics. One of those episodes where you start with “<em>yeah yeah, sounds academic</em>” and end up thinking “<em>wait… this explains half of the problems we currently have with AI</em>”.</p><p>This post is my attempt to structure and digest what I learned. If you are curious, you can find the podcast episode here.</p><p>A big shoutout to <a href="https://www.linkedin.com/in/tobias-rebert/">Tobias</a> from <a href="https://tentris.io/">Tentris.io</a> and also to <a href="https://heisenware.com/">Heisenware</a> as the podcast hosts. The discussion was genuinely insightful and highly relevant for anyone building data driven or AI powered systems today.</p><h3>From relational tables to connected knowledge</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gJ3Liujs0o89L4gbWro1-A.png" /><figcaption>Relational tables vs. graph thinking</figcaption></figure><p>Most of us grew up with relational databases. Tables, rows, columns, foreign keys. SQL joins everywhere. And to be fair: relational databases are amazing at what they do. Transactional workloads, clear schemas, predictable queries.</p><p>But they start to struggle once relationships become the core of the problem.</p><p>Imagine dozens of tables, joined again and again, just to answer questions like:</p><ul><li>Who is related to whom?</li><li>What belongs together?</li><li>How does A influence B indirectly?</li></ul><p>At some point the query becomes harder to read than to explain in plain English. And performance usually suffers as well.</p><p>This is exactly where knowledge graphs enter the stage.</p><p>A knowledge graph stores information as <strong>nodes and edges</strong>. Or in simpler terms: things and their relationships. Instead of flattening knowledge into tables, you model it closer to how humans actually think about the world. More like a big structured mind map.</p><h3>Ontologies: the missing layer of meaning</h3><p>What really made it click for me was the role of ontologies.</p><p>An ontology defines what things are and how they are allowed to relate to each other. For example:</p><ul><li>A car is a vehicle</li><li>A car has exactly one engine</li><li>An engine is not a vehicle</li><li>An electric car is a car</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4Xt5SxsFdpPhSgT98O10GQ.png" /><figcaption>A beaver as a builder of structure</figcaption></figure><p>This may sound trivial, but it introduces something most databases do not have: <strong>formal semantics</strong>.</p><p>Instead of just storing data, you store meaning and rules. The data inside the graph then follows this structure. Technically this is often represented as triples:</p><blockquote>Subject — Predicate — Object</blockquote><p>For example:</p><blockquote>CarA — hasEngine — Engine1</blockquote><p>Once this structure exists, machines can start reasoning about data instead of just retrieving it.</p><h3>Why graphs outperform joins at scale</h3><p>Relational databases can model relationships. But every additional join increases complexity and cost. Deeply connected data quickly becomes painful.</p><p>Graph databases are optimized for exactly this problem. Traversing relationships is cheap. Asking questions like “show me all entities connected within three hops” is a natural operation, not an edge case.</p><p>That does not mean graph databases replace relational ones. Each has its sweet spot:</p><ul><li>Relational databases: structured data and transactions</li><li>Document stores: scalability and flexibility</li><li>Vector databases: similarity search</li><li>Knowledge graphs: relationships and semantics</li></ul><p>There is no silver bullet. Only better tools for specific problems.</p><h3>Real world examples you already use</h3><p>One of the nicest parts of the podcast was how grounded the examples were.</p><p>You are probably using knowledge graphs every day without knowing it:</p><ul><li>Netflix recommendations</li><li>Spotify music discovery</li><li>Google search and knowledge panels</li><li>Large ecommerce platforms that actually understand what you are looking for</li></ul><p>In the episode, Netflix was contrasted with Amazon Prime. Not as a product comparison, but as an architectural one. Netflix heavily relies on knowledge graphs to connect users, content, genres, actors and behavior. This allows both better recommendations and better business decisions.</p><h3>Knowledge graphs meet AI</h3><p>This is where things get really interesting.</p><p>Most current AI systems use <strong>Retrieval Augmented Generation</strong>. Documents are chunked, converted into vectors and stored in vector databases.</p><p>Queries are matched via similarity search and the retrieved chunks are injected into the prompt.</p><p>This works surprisingly well for small and medium datasets.</p><p>But it has limitations:</p><ul><li>No real understanding of relationships</li><li>No explainability</li><li>Increasing noise with growing data</li></ul><p>The podcast introduced the idea of <strong>Graph RAG</strong>.</p><p>Instead of retrieving chunks purely by vector similarity, the system first navigates a knowledge graph. The ontology already encodes structure and meaning. The retrieved subgraph is then passed to the language model.</p><p>The benefits are huge:</p><ul><li>Far fewer hallucinations</li><li>Explainable results, because the graph path is visible</li><li>Better scaling across different domains and data silos</li></ul><p>A vector space can tell you <em>what looks similar</em>. A knowledge graph can tell you <em>what is related and why</em>.</p><h3>Why this suddenly matters now</h3><p>Knowledge graphs are not new. The idea goes back to the early days of the Semantic Web.</p><p>What <em>is</em> new is the combination with modern language models.</p><p>LLMs are fantastic at language. But they are not knowledge systems. They need structured, meaningful context. Knowledge graphs can provide exactly that.</p><p>In a way, LLMs become the voice. Knowledge graphs become the memory.</p><h3>Where this goes next</h3><p>This post is intentionally conceptual. I wanted to first understand why knowledge graphs are relevant again before touching code.</p><p>In the next post I want to explore this hands on:</p><ul><li>Build a tiny knowledge graph</li><li>Define a minimal ontology</li><li>Query it from a .NET application</li><li>And eventually combine it with an AI workflow</li></ul><p>Because as always: things only really click once you build them yourself.</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SSjbBBC5CK2zqsSC.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=77ff8020b3d4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/knowledge-graphs-and-ai-why-semantics-matter-again-77ff8020b3d4">Knowledge Graphs and AI: Why Semantics Matter Again</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Structured Prompting in real projects — checklist & best practices]]></title>
            <link>https://medium.com/medialesson/structured-prompting-in-real-projects-checklist-best-practices-c39fa789856b?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/c39fa789856b</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[prompting-technique]]></category>
            <category><![CDATA[structured-data]]></category>
            <category><![CDATA[best-practices]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Fri, 12 Dec 2025 10:07:08 GMT</pubDate>
            <atom:updated>2025-12-12T10:07:08.541Z</atom:updated>
            <content:encoded><![CDATA[<h3>Structured Prompting in real projects — checklist &amp; best practices</h3><h4>Why you should care about structured prompting</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*btPtpI5W39HKfn2w_6DI8g.png" /></figure><p>Working with LLMs is fun. At first you write a prompt, get a response, copy-paste, maybe tweak it by hand. But once you start building real systems (chatbots, pipelines, data extraction tools…), suddenly you’re dealing with a mix of inconsistent outputs, parsing failures, manual cleanup … and a creeping sense you might not hire yourself in six months if things grow.</p><p>Free-form prompting feels flexible, but in the long run it becomes breakable. Small changes to phrasing or context can lead to big shifts in output structure. That means fragile regex-parsers, subtle bugs, and unpredictable integration behavior. This ambiguity is exactly what structured prompting helps you avoid.</p><p>If you followed my earlier deep dive into <a href="https://github.com/toon-format/">TOON</a> <br>(<a href="https://medium.com/medialesson/json-vs-toon-a-new-era-of-structured-input-19cbb7fc552b">JSON vs. TOON: A New Era of Structured Input</a>), you already know how a more compact and semantically clean format can reduce ambiguity before the prompt even reaches the model. TOON is not just a clever encoding trick, it is an example of how structured input and structured output naturally complement each other. If you combine both ideas, you get a workflow that is more predictable, more token-efficient, and less fragile as your system grows. <br>For this post I am taking the next step in that direction and focusing on structured <em>output</em>, backed by strict schemas and automatically validated responses. You can also find the <a href="https://github.com/schroedermarius/StructuredPromptingMedium">full sample project on GitHub</a>.</p><p>Enter structured prompting: You treat your prompt as a data contract: you define exactly what the output should look like (fields, types, maybe even constraints), you ask the LLM to honour that contract, and then you parse/validate the output just like you would any other external data source. At this point, you can think of an simple API you are requesting for data.</p><p>If you build your LLM integrations right, with schema definitions, validation and a robust workflow, you get reliability, predictable output, and cleaner integration.</p><h3>Practical benefits &amp; where it pays off</h3><p>Structured prompting shines especially in these scenarios:</p><ul><li>Data extraction from unstructured text (emails, support tickets, logs, scraped HTML…) → output an object, not a story</li><li>Automating pipelines: Let LLMs produce JSON (or another structured format), then feed that directly into databases, APIs or further processing → no manual “cleanup work” needed</li><li>Generating content with a fixed template (e.g. product descriptions, metadata, summaries, configuration files) → you can constrain length, force required fields, enforce enums instead of free-text chaos</li><li>Maintaining and versioning prompts over time. When prompts are structured, you can treat them more like code → diffable, testable, maintainable</li></ul><p>In larger projects or production environments, structured prompting often isn’t “nice to have”, it becomes essential.</p><h3>Checklist: Best Practices for Structured Prompting 🧰</h3><p>Here’s a practical checklist for you (or any developer) to follow when using structured prompts:</p><ul><li><strong>Define a clear schema</strong><br>Use JSON Schema, or your language’s equivalent (e.g. typed data models) to define exactly what output you expect — field names, data types, required vs. optional, enums or value constraints where appropriate.</li><li><strong>Provide a template / example output in your prompt<br></strong>Instead of saying “please output JSON”, show a minimal example (empty or with placeholder values). This guides the LLM on structure, order, and expected types, and dramatically increases consistency.</li><li><strong>Keep schemas as simple as possible initally<br></strong>Start with only a few required fields. Once basic outputs work reliably, you can extend the schema. This reduces initial complexity and debugging pain.</li><li><strong>Validate every output programmatically<br></strong>Do not assume the LLM always follows the schema. Always parse and validate. If parsing fails, implement retry logic or fallback handling.</li><li><strong>Make schema and prompt versioned &amp; maintainable<br></strong>Treat prompt and schema like source code. Put them under version control. Keep them modular and documented. That way changes are traceable, and you avoid “works on my machine” secnarios.</li><li><strong>Know when not to use structured prompting<br></strong>For creative, open-ended tasks (storytelling, marketing copy, brainstorming, image generation) a rigid schema may hamper quality and expressiveness. Use structured prompting when you want precision, consistency, and machine-readable outputs, otherwise free-form might still win.</li></ul><h3>Minimal .NET / pseudocode example — how it could look in a real project</h3><pre>// Define a schema (DTO + JSON Schema)<br>public class SalaryByDepartment<br>{<br>    public string Department { get; set; }<br>    public decimal AverageSalary { get; set; }<br>}<br><br>public class SalaryByDepartmentResponse<br>{<br>    public List&lt;SalaryByDepartment&gt; Items { get; set; }<br>}<br><br>// Create JSON schema for the model (simplified)<br>var schema = @&quot;<br>{<br>  &quot;&quot;type&quot;&quot;: &quot;&quot;object&quot;&quot;,<br>  &quot;&quot;properties&quot;&quot;: {<br>    &quot;&quot;Items&quot;&quot;: {<br>      &quot;&quot;type&quot;&quot;: &quot;&quot;array&quot;&quot;,<br>      &quot;&quot;items&quot;&quot;: {<br>        &quot;&quot;type&quot;&quot;: &quot;&quot;object&quot;&quot;,<br>        &quot;&quot;properties&quot;&quot;: {<br>          &quot;&quot;Department&quot;&quot;: { &quot;&quot;type&quot;&quot;: &quot;&quot;string&quot;&quot; },<br>          &quot;&quot;AverageSalary&quot;&quot;: { &quot;&quot;type&quot;&quot;: &quot;&quot;number&quot;&quot; }<br>        },<br>        &quot;&quot;required&quot;&quot;: [&quot;&quot;Department&quot;&quot;, &quot;&quot;AverageSalary&quot;&quot;],<br>        &quot;&quot;additionalProperties&quot;&quot;: false<br>      }<br>    }<br>  },<br>  &quot;&quot;required&quot;&quot;: [&quot;&quot;Items&quot;&quot;],<br>  &quot;&quot;additionalProperties&quot;&quot;: false<br>}&quot;;<br><br>// Configure the model to *always* return structured output<br>var options = new ChatCompletionOptions<br>{<br>    ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat(<br>        &quot;SalaryByDepartmentResponse&quot;,<br>        BinaryData.FromString(schema),<br>        &quot;Schema for average salary grouped by department&quot;,<br>        true<br>    )<br>};<br><br>// Prompt template<br>var messages = new List&lt;ChatMessage&gt;<br>{<br>    new SystemChatMessage(<br>        &quot;You are a precise data analyst. &quot; +<br>        &quot;Return ONLY valid JSON matching the JSON Schema.&quot;<br>    ),<br>    new UserChatMessage(&quot;Compute the average salary per department from this data: ...&quot;)<br>};<br><br>// Send request<br>var result = await chatClient.CompleteChatAsync(messages, options);<br>var rawJson = result.Value.Content[0].T</pre><p>This is rudimentary, but it is precisely this approach that makes your LLM code maintainable, robust, and automatable.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GTdNGkrQC6-7Q3RzQccmXw.png" /><figcaption>Output of the sample application</figcaption></figure><h3>What structured prompting does not solve — caveats &amp; limitations</h3><p>A schema can guarantee structure, but not correctness. The model may still output garbage, structurally valid JSON with wrong data. Structured prompting helps with format, not with semantics oder qualitative kindness.</p><p>Complex or deeply nested schemas increase the risk of parsing failures or token limits. With very large outputs, the model may intentionally or unintentionally cut off parts.</p><p>When used in creative scenarios, structure can limit freedom, sometimes you just want free text, not rigid objects.</p><h3>Final Thoughts</h3><p>For me, structured prompting is no longer a toy. It is a serious tool when working with LLMs, especially if you want more than just to “try it out quickly.”</p><p>If you expect outputs to be further processed, versioned, or automated, then the effort is immediately worthwhile. The small overhead of schema definition and validation quickly pays off, through fewer errors, more consistency, and more elegant integration into real software.</p><p>If you continue as before with <a href="https://medium.com/medialesson/json-vs-toon-a-new-era-of-structured-input-19cbb7fc552b">JSON vs. TOON</a>, for token efficiency, performance, and clean data formats, then structured prompting could be your next upgrade.</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*nJAj6O9TSIVvISwg.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c39fa789856b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/structured-prompting-in-real-projects-checklist-best-practices-c39fa789856b">Structured Prompting in real projects — checklist &amp; best practices</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Combining Upload Progress with API Progress ]]></title>
            <link>https://medium.com/medialesson/combining-upload-progress-with-api-progress-a6f655d88c81?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/a6f655d88c81</guid>
            <category><![CDATA[server-sent-events]]></category>
            <category><![CDATA[aspnetcore]]></category>
            <category><![CDATA[c-sharp-programming]]></category>
            <category><![CDATA[progress]]></category>
            <category><![CDATA[blazor]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Thu, 27 Nov 2025 07:03:08 GMT</pubDate>
            <atom:updated>2025-11-27T07:03:08.123Z</atom:updated>
            <content:encoded><![CDATA[<h4>How to marry two progress bars without making it weird 😄</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*74YKpb88ZnpE9fogK2Yaqg.png" /></figure><p>When you build a file upload feature, you usually think about one progress bar: the one showing how much of your file has already made it to the server. But what if the real work starts only after the upload?</p><p>Maybe you‘re transcoding a video, importing rows into a database, or sending the file to another service. In these cases, you end up with two progress sources:</p><ol><li><strong>The client upload</strong> (how much data is already sent) 📤</li><li><strong>The backend processing</strong> (how much work the API has done so far) 🔄</li></ol><p>Let‘s talk about how to combine both in one beautiful, synchronized progress bar. 🎯</p><h3>The problem 😬</h3><p>Most apps stop updating progress once the upload finishes. The file is on the server, and now the user just… waits. It‘s not great UX. Users want to see what‘s happening — even if it‘s just „Processing 73%“.</p><p>The trick is to link your <strong>upload progress</strong> (client → backend) with your <strong>backend progress</strong> (API → client).</p><h3>The idea 💡</h3><p>The frontend tracks how many bytes have been sent. Meanwhile, the backend emits Server-Sent Events (SSE) with progress updates while it processes the uploaded file.</p><p>So we:</p><ul><li>Report upload progress during the upload (0–50%) 🎬</li><li>Report backend progress via SSE (50–100%) 📈</li></ul><p>Both values feed into one single IProgress&lt;double&gt; on the client side.</p><h3>The clien️t 🖥️</h3><p>On the client, we wrap the upload stream in a ProgressableStreamContent so each chunk reports how much of the file is uploaded. Once the upload is done, the same progress reporter is used for backend events received via SSE.</p><pre>var streamContent = new ProgressableStreamContent(stream, progress: progress);<br>streamContent.Headers.ContentType = new MediaTypeHeaderValue(&quot;application/octet-stream&quot;);<br><br>var body = new MultipartFormDataContent<br>{<br>    { streamContent, &quot;file&quot;, fileName },<br>};<br><br>var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);<br>var responseStream = await response.Content.ReadAsStreamAsync();<br><br>var parser = SseParser.Create(responseStream, SseItemParser);<br>foreach (var sseItem in parser.Enumerate())<br>{<br>    if (sseItem.Data is ProgressEventMessage message)<br>    {<br>        progress.Report(message.Progress.Percentage);<br>    }<br>}</pre><p>The magic part is that both upload and backend processing share the same progress reporter. The user sees one continuous bar moving from 0 to 100%. 🎉</p><h3>The backend 🧩</h3><p>On the backend, we stream the uploaded file to storage and push progress updates via SSE:</p><pre>var throttler = new ProgressThrottler(TimeSpan.FromMilliseconds(200), minPercentageStep: 1.0);<br>var sseProgress = new Progress&lt;ReportProgress&gt;(p =&gt;<br>{<br>    if (!throttler.ShouldSend(p.Percentage))<br>    {<br>        return;<br>    }<br><br>    var message = new ProgressEventMessage(p);<br>    channel.Writer.TryWrite(new EventMessageBase(EventMessageType.Progress, JsonSerializer.Serialize(message)));<br>});</pre><p>Then, as we process the file (e.g. upload to S3 or do other work), we map its progress range to 50–100% and report that through the same channel. 🧮</p><h3>Why this works so well 🏆</h3><ul><li><strong>Single source of truth for progress:</strong> the client doesn‘t have to guess what‘s happening.</li><li><strong>Real-time UX:</strong> users never see a „frozen“ bar after the upload finishes.</li><li><strong>Extensible:</strong> works with any long-running API action — imports, analysis, background tasks, etc.</li></ul><p>And yes, technically, you‘re „marrying“ two progress bars. But in this case, it‘s a happy marriage. 💍</p><h3>Final thoughts 📝</h3><p>This approach gives your users feedback through the entire lifecycle of an upload — from „sending file“ to „processing complete“.</p><p>You can adapt this pattern even if your backend doesn‘t upload the file further. As long as the backend sends progress via SSE, you can merge it with the client‘s upload progress and create a seamless experience. ✨</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tC95ijFrcyHBuSOW.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a6f655d88c81" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/combining-upload-progress-with-api-progress-a6f655d88c81">Combining Upload Progress with API Progress 🚀</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[JSON vs TOON — A new era of structured input?]]></title>
            <link>https://medium.com/medialesson/json-vs-toon-a-new-era-of-structured-input-19cbb7fc552b?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/19cbb7fc552b</guid>
            <category><![CDATA[toon]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[gpt]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Mon, 03 Nov 2025 10:21:22 GMT</pubDate>
            <atom:updated>2025-11-03T10:21:22.009Z</atom:updated>
            <content:encoded><![CDATA[<h3>JSON vs TOON — A new era of structured input?</h3><h4>Why structure matters more than ever</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RqEGnN61hVSWk3CIB0dtUw.png" /></figure><p>In times where prompts are growing larger and AI models are getting more powerful, one question keeps coming up: how can we keep costs and processing times low?</p><p>When working programmatically with LLMs, structured outputs have already become a standard approach. You can tell the AI to respond in a certain format, for example JSON. By defining a model schema , and ideally describing what each field means , the AI tries to understand the context and fill in the output “to the best of its knowledge and belief” (or however close an LLM gets to that).</p><p>This has made it easier than ever to work with the <em>results</em> of an AI.<br>But what about the <em>input</em>?</p><p>Even though we can make the output neat and structured, most of us still dump huge JSON, YAML, or even plain text datasets into prompts. That’s not only slow and expensive, it’s also far from token-efficient.</p><p>So it was only a matter of time until a new format would appear to fix exactly that.<br>And here comes <strong>TOON</strong> 🎉</p><h3>Meet TOON — The token-friendly cousin of JSON</h3><p><a href="https://github.com/toon-format/toon">TOON</a> is a new file format that sits somewhere between JSON and CSV. It’s still human-readable, but optimized for LLMs and tokenization efficiency. According to its creators, it can reduce token counts by <strong>30–60%</strong> which, given how token pricing works, can translate into serious cost savings.</p><p>Here’s what makes TOON so interesting:</p><ul><li>💸 <strong>Token-efficient</strong>: typically 30–60% fewer tokens than JSON</li><li>🤿 <strong>LLM-friendly guardrails</strong>: explicit lengths and fields enable validation</li><li>🍱 <strong>Minimal syntax</strong>: removes redundant punctuation (braces, brackets, most quotes)</li><li>📐 <strong>Indentation-based structure</strong>: like YAML, uses whitespace instead of braces</li><li>🧺 <strong>Tabular arrays</strong>: declare keys once, stream data as rows</li></ul><h4>JSON</h4><pre>[<br>  {&quot;id&quot;: 1, &quot;name&quot;: &quot;Alice&quot;, &quot;department&quot;: &quot;Engineering&quot;, &quot;salary&quot;: 120000},<br>  {&quot;id&quot;: 2, &quot;name&quot;: &quot;Bob&quot;, &quot;department&quot;: &quot;Marketing&quot;, &quot;salary&quot;: 95000},<br>  {&quot;id&quot;: 3, &quot;name&quot;: &quot;Charlie&quot;, &quot;department&quot;: &quot;Engineering&quot;, &quot;salary&quot;: 110000}<br>]</pre><h4>TOON</h4><pre>[3]{Id,Name,Department,Salary}:<br>  1,Alice,Engineering,120000<br>  2,Bob,Marketing,95000<br>  3,Charlie,Engineering,110000</pre><p>If you look at it closely, it feels a bit like YAML met CSV for a coffee and decided to raise a structured baby together.</p><p>Benchmarks from the creators already show impressive results:<br> 👉 <a href="https://github.com/toon-format/toon#benchmarks">Toon Benchmarking and Key Features</a></p><h3>So… why should I care?</h3><p>If you’re building anything that regularly feeds structured data into LLMs, for example, chatbots, AI-assisted code generation, or multi-step workflows, TOON can significantly reduce prompt size.</p><p>It’s not just about the money (although saving around 50% on token usage isn’t bad at all).<br>It’s also about speed. Fewer tokens mean faster inference and potentially lower latency, especially in real-time systems or when using streaming APIs.</p><p>And the best part: it’s already available for multiple languages (including):</p><p>🟦 <strong>.NET:</strong> <a href="https://github.com/0xZunia/ToonSharp">ToonSharp</a></p><p>🐍 <strong>Python</strong>: <a href="https://github.com/xaviviro/python-toon">python-toon</a> / <a href="https://github.com/bpradana/pytoon">pytoon</a></p><p>🦫 <strong>Go</strong>: <a href="https://github.com/alpkeskin/gotoon">gotoon</a></p><h3>Real-world evaluation</h3><p>So I’ve built a small benchmark tool to see how TOON actually performs compared to JSON.</p><p>Using a simple dataset of employees, I asked GPT to <em>analyze the data and calculate the average salary by department</em>. The tool measures prompt size, completion token count, and overall response time.</p><p>A big thanks to <a href="https://medium.com/u/c8f6762e0e4b">Sebastian Jensen</a> for the neat ConsoleHelper class, it made the output look way more professional than I expected from a console app! 😄</p><p>Here are the results from my run:</p><pre>+---------------------------------------------------------------------------+<br>| Type         | Prompt Tokens   |  Completions Tokens  |  Duration         |<br>|--------------+-----------------+----------------------+-------------------|<br>| JSON         | 1344            |  3475                |  00:00:28.3932721 |<br>| TOON         | 589             |  2928                |  00:00:23.4953152 |<br>+---------------------------------------------------------------------------+</pre><p>That’s roughly a <strong>56% reduction</strong> in prompt tokens and a noticeable 5-second speed improvement, with the same output quality from the model.<br>So yes, TOON doesn’t just look good on paper. It’s actually faster, cheaper, and still easy to read.</p><h3>Closing thoughts</h3><p>It’s fascinating to see how we’ve come full circle: we spent years teaching AIs to output structured data, and now we’re optimizing our input to speak their language better.</p><p>Whether TOON becomes the new standard or just another clever niche idea, it’s definitely worth keeping an eye on. Especially if you care about performance, cost, and efficiency (and let’s be honest, who doesn’t?).</p><p>Please see the complete <a href="https://github.com/schroedermarius/ToonBenchmark">Project</a> on my <a href="https://github.com/schroedermarius">GitHub</a>.</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2-1dAaq3aTEOmACp.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=19cbb7fc552b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/json-vs-toon-a-new-era-of-structured-input-19cbb7fc552b">JSON vs TOON — A new era of structured input?</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[⚡ Real-Time Progress Updates for Long-Running API Tasks with Server-Sent Events (SSE) in ASP.NET]]></title>
            <link>https://medium.com/medialesson/real-time-progress-updates-for-long-running-api-tasks-with-server-sent-events-sse-in-asp-net-1c5fdbac6065?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/1c5fdbac6065</guid>
            <category><![CDATA[server-sent-events]]></category>
            <category><![CDATA[c-sharp-programming]]></category>
            <category><![CDATA[realtime]]></category>
            <category><![CDATA[aspnetcore]]></category>
            <category><![CDATA[backend]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Fri, 24 Oct 2025 07:54:41 GMT</pubDate>
            <atom:updated>2025-10-24T07:54:41.026Z</atom:updated>
            <content:encoded><![CDATA[<h3>⚡ Real-Time Progress Updates for Long-Running API Tasks with Server-Sent Events (SSE) in ASP.NET Core</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MSWaPAjJsY6gpAvXfmn1GA.png" /></figure><p>When you trigger a <strong>long-running operation</strong> through an API — like generating a document, compiling a report, or uploading a processed file to cloud storage, users hate waiting in silence.<br>Wouldn’t it be great if your backend could <strong>continuously stream progress updates</strong> back to the browser in <strong>real time</strong>, without needing WebSockets or polling?</p><p>That’s exactly what this post is about.</p><p>We’ll explore how to build a <strong>robust, async-safe SSE architecture</strong> in ASP.NET Core to stream progress updates for any background task, and how to avoid common pitfalls like interleaved writes and lost events.</p><h3>🎯 The Use Case</h3><p>Let‘s take a typical example:</p><p>A user triggers a document generation request via your API.<br>The server generates the document, uploads it to a storage service,<br>and streams progress updates back to the client during each phase.</p><p>At the end, the client receives either a success event with the storage path,<br>or an error event if something failed.</p><h3>🧩 Core Components</h3><p>We‘ll build our system around three reliable helpers:</p><p>SseResponseWriter handles <strong>safe, thread-protected writes</strong> to the SSE response stream.</p><p>ProgressThrottler ensures we only <strong>send updates every few milliseconds</strong> or after <strong>meaningful percentage changes</strong>.</p><p>Channel&lt;EventMessageBase&gt;queues events to <strong>decouple your long-running task</strong> from the network I/O layer.</p><p>These three pieces work together to ensure your streaming response is smooth, structured, and cancellation-safe.</p><h3>💡 Why We Use a Channel</h3><p>When you generate a document and upload it to a storage service, multiple async operations run in parallel — sometimes in background threads.</p><p>If all of them tried to write directly to the HTTP response, you would quickly hit:</p><ul><li>race conditions</li><li>out-of-order JSON messages</li><li>broken connections when the controller finishes before the last event is sent</li></ul><p>That‘s why we use a <strong>Channel</strong> — a thread-safe in-memory queue from System.Threading.Channels .</p><p>✅ Your background tasks write progress, success, and error messages into the channel.<br>✅ A single background consumer reads and sends them through the SseResponseWriter.<br>✅ The controller only returns once all messages are sent and the connection is cleanly closed.</p><h3>⏱️ Progress Throttling Done Right</h3><p>If your API processes thousands of steps or bytes, you don’t want to send every single one to the browser.<br>Our ProgressThrottler handles this elegantly using:</p><ul><li><strong>time-based throttling</strong> (for example, every 200 milliseconds)</li><li><strong>percentage-based throttling</strong> (for example, on at least 1 percent change)</li></ul><p>It uses a monotonic clock (Stopwatch.GetTimestamp) to avoid bugs caused by system time changes. This means your throttling logic will remain stable even on long-running tasks or VMs.</p><h3>🧭 The ProgressThrottler</h3><p>Here’s the full implementation you can directly reuse:</p><pre>/// &lt;summary&gt;<br>/// Helper class to throttle progress updates based on time intervals and percentage steps.<br>/// Uses a monotonic clock (Stopwatch) to avoid issues when system time changes.<br>/// Thread-safe.<br>/// &lt;/summary&gt;<br>public sealed class ProgressThrottler<br>{<br>    private readonly TimeSpan _minInterval;<br>    private readonly double _minPercentageStep;<br>    private long _lastSentTimestamp;<br>    private double _lastPercentage = -1;<br>    private readonly object _sync = new();<br><br>    public ProgressThrottler(TimeSpan minInterval, double minPercentageStep = 0)<br>    {<br>        if (minInterval &lt; TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(minInterval));<br>        if (minPercentageStep &lt; 0) throw new ArgumentOutOfRangeException(nameof(minPercentageStep));<br><br>        _minInterval = minInterval;<br>        _minPercentageStep = minPercentageStep;<br>    }<br><br>    public bool ShouldSend(double currentPercentage)<br>    {<br>        lock (_sync)<br>        {<br>            var nowTs = Stopwatch.GetTimestamp();<br><br>            if (_lastSentTimestamp == 0)<br>            {<br>                _lastSentTimestamp = nowTs;<br>                _lastPercentage = currentPercentage;<br>                return true;<br>            }<br><br>            if (currentPercentage &lt; _lastPercentage)<br>            {<br>                return false;<br>            }<br><br>            var elapsed = Stopwatch.GetElapsedTime(_lastSentTimestamp, nowTs);<br>            var percentageDiff = Math.Abs(currentPercentage - _lastPercentage);<br><br>            bool timeThresholdMet = elapsed &gt;= _minInterval;<br>            bool percentageThresholdMet = _minPercentageStep &gt; 0 &amp;&amp; percentageDiff &gt;= _minPercentageStep;<br><br>            if (timeThresholdMet || percentageThresholdMet || currentPercentage &gt;= 100.0)<br>            {<br>                _lastSentTimestamp = nowTs;<br>                _lastPercentage = currentPercentage;<br>                return true;<br>            }<br><br>            return false;<br>        }<br>    }<br>}</pre><p>You can tune the timing and precision to fit your workload.<br>For example, new ProgressThrottler(TimeSpan.FromMilliseconds(200), 1.0) will send at most five updates per second or whenever the percentage increases by one percent.</p><h3>⚙️ Example Controller<br>Let’s take a simplified version of our actual implementation:</h3><pre>[HttpPost(&quot;generate-document&quot;)]<br>public async Task&lt;IActionResult&gt; GenerateDocumentAsync(<br>    [FromHeader(Name = &quot;X-Sending-App&quot;)] string sender,<br>    [FromQuery][Required] string documentName,<br>    [FromQuery] string? targetFolder = null)<br>{<br>    if (!long.TryParse(Request.Headers.ContentLength?.ToString(), out var totalBytes))<br>        return BadRequest(&quot;Missing or invalid Content-Length&quot;);<br><br>    using var sseWriter = new SseResponseWriter(HttpContext.Response);<br>    var cancellation = HttpContext.RequestAborted;<br><br>    // Create an unbounded channel for event messages<br>    var channel = Channel.CreateUnbounded&lt;EventMessageBase&gt;(new UnboundedChannelOptions<br>    {<br>        SingleReader = true,<br>        SingleWriter = false<br>    });<br><br>    // Background task to process messages and send them via SSE<br>    var sseTask = Task.Run(async () =&gt;<br>    {<br>        await foreach (var msg in channel.Reader.ReadAllAsync(cancellation))<br>        {<br>            await sseWriter.WriteEventAsync(msg, cancellation);<br>        }<br>    }, cancellation);<br><br>    // Throttle progress updates<br>    var throttler = new ProgressThrottler(TimeSpan.FromMilliseconds(200), minPercentageStep: 1.0);<br>    var progress = new Progress&lt;ReportProgress&gt;(p =&gt;<br>    {<br>        if (!throttler.ShouldSend(p.Percentage)) return;<br><br>        var message = new ProgressEventMessage(p with { Percentage = Math.Floor(p.Percentage) });<br>        channel.Writer.TryWrite(new EventMessageBase(EventMessageType.Progress, JsonSerializer.Serialize(message)));<br>    });<br><br>    // Generate document (simulate long-running process)<br>    var generatedDocument = await documentService.GenerateAsync(documentName, progress, cancellation);<br><br>    // Upload generated file to external storage<br>    var uploadProgress = new Progress&lt;ReportProgress&gt;(p =&gt;<br>    {<br>        var adjusted = p with { Percentage = 50 + p.Percentage * 0.5 };<br>        progress.Report(adjusted);<br>    });<br><br>    var uploadResult = await storageService.UploadFileAsync(sender, targetFolder, generatedDocument, uploadProgress, cancellation);<br><br>    // Send final success message<br>    channel.Writer.TryWrite(new EventMessageBase(EventMessageType.Success,<br>        JsonSerializer.Serialize(new SuccessEventMessage(uploadResult.RelativeFilePath))));<br><br>    channel.Writer.Complete();<br>    await sseTask;<br>    await HttpContext.Response.CompleteAsync();<br><br>    return Empty;<br>}</pre><h3>🧠 Key Takeaways</h3><h4>🧱 1. Use Channels to Decouple Event Flow</h4><p>Channels let you queue up progress and result messages safely and keep network I/O off your business logic thread.</p><h4>🧩 2. Protect SSE Writes with a Semaphore</h4><p>ASP.NET Core’s HttpResponse isn’t thread-safe for concurrent writes.<br> Our SseResponseWriter ensures only one event is written at a time.</p><h4>⚙️ 3. Always Throttle Updates</h4><p>Throttle by time and by percentage step to keep updates smooth without flooding the network.</p><h4>🕒 4. Use Stopwatch Instead of DateTime.UtcNow</h4><p>It’s a monotonic clock that never jumps backward or forward, essential for stable throttling.</p><h4>🧵 5. Wait for All SSE Writes Before Closing</h4><p>The controller should only finish once the channel is drained and all messages are flushed.<br>Otherwise, the client might miss the final event.</p><h3>🧭 The SseResponseWriter</h3><pre>public sealed class SseResponseWriter : IDisposable<br>{<br>    private readonly HttpResponse _response;<br>    private readonly SemaphoreSlim _writeLock = new(1, 1);<br><br>    public SseResponseWriter(HttpResponse response)<br>    {<br>        _response = response ?? throw new ArgumentNullException(nameof(response));<br>        _response.Headers.CacheControl = &quot;no-cache&quot;;<br>        _response.Headers.Connection = &quot;keep-alive&quot;;<br>        _response.Headers[&quot;X-Accel-Buffering&quot;] = &quot;no&quot;;<br>        _response.ContentType = &quot;text/event-stream&quot;;<br>    }<br><br>    public async Task WriteEventAsync(EventMessageBase message, CancellationToken token)<br>    {<br>        await _writeLock.WaitAsync(token);<br>        try<br>        {<br>            await _response.WriteAsync($&quot;event: {message.Type.ToString().ToLowerInvariant()}\n&quot;, token);<br>            await _response.WriteAsync($&quot;data: {message.Body}\n\n&quot;, token);<br>            await _response.Body.FlushAsync(token);<br>        }<br>        finally<br>        {<br>            _writeLock.Release();<br>        }<br>    }<br><br>    public void Dispose() =&gt; _writeLock.Dispose();<br>}</pre><h3>💬 Client-Side Example</h3><p>Here’s how to consume those events from your frontend:</p><pre>const evtSource = new EventSource(&quot;/api/v1/documents/generate-document?documentName=report.pdf&quot;);<br><br>evtSource.addEventListener(&quot;progress&quot;, e =&gt; {<br>  const data = JSON.parse(e.data);<br>  console.log(`Progress: ${data.progress.percentage}%`);<br>});<br><br>evtSource.addEventListener(&quot;success&quot;, e =&gt; {<br>  const data = JSON.parse(e.data);<br>  console.log(&quot;Document ready at:&quot;, data.relativeFilePath);<br>  evtSource.close();<br>});<br><br>evtSource.addEventListener(&quot;error&quot;, e =&gt; {<br>  console.error(&quot;Error:&quot;, e.data);<br>  evtSource.close();<br>});</pre><h3>✅ Final Thoughts</h3><p>This approach is not just about document generation, it’s a general pattern for any long-running server process:</p><ul><li>Generating PDFs or reports</li><li>AI model inference</li><li>Video or audio processing</li><li>Background imports or exports</li></ul><p>By combining <strong>Channels</strong>, <strong>Throttling</strong>, and <strong>SSE</strong>, you get real-time progress reporting that’s reliable, scalable, and surprisingly simple.</p><p>You don’t need WebSockets.<br>You don’t need SignalR.<br>Just smart use of built-in .NET features.</p><p><em>Happy coding, and may your uploads always reach 100% — smoothly.</em></p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*tC95ijFrcyHBuSOW.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1c5fdbac6065" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/real-time-progress-updates-for-long-running-api-tasks-with-server-sent-events-sse-in-asp-net-1c5fdbac6065">⚡ Real-Time Progress Updates for Long-Running API Tasks with Server-Sent Events (SSE) in ASP.NET</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Easily Connect to Amazon S3 with .NET and Dependency Injection: A Complete Guide]]></title>
            <link>https://medium.com/medialesson/how-to-easily-connect-to-amazon-s3-with-net-and-dependency-injection-a-complete-guide-5325dfa6065b?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/5325dfa6065b</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[cloud]]></category>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[dependency-injection]]></category>
            <category><![CDATA[storage]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Fri, 19 Sep 2025 12:32:06 GMT</pubDate>
            <atom:updated>2025-09-19T12:32:06.014Z</atom:updated>
            <content:encoded><![CDATA[<h3>How to Easily Connect to Amazon S3 with .NET and Dependency Injection: A Complete Guide</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EhFJL2s_3g0GiAMSD8gBqg.png" /></figure><p>Amazon S3 (Simple Storage Service) is one of the most popular cloud storage solutions for modern applications. If you’re a .NET developer looking to integrate S3 into your application using clean architecture principles and dependency injection, this comprehensive guide will walk you through the entire process step by step.</p><h3>Why Use Amazon S3 with .NET?</h3><p>Amazon S3 offers numerous benefits for .NET applications:</p><ul><li><strong>Scalability</strong>: Handle virtually unlimited storage needs</li><li><strong>Durability</strong>: 99.999999999% (11 9’s) data durability</li><li><strong>Cost-effective</strong>: Pay only for what you use</li><li><strong>Global accessibility</strong>: Access files from anywhere in the world</li><li><strong>Security</strong>: Enterprise-grade security features</li><li><strong>Integration</strong>: Seamless integration with other AWS services</li></ul><h3>Prerequisites</h3><p>Before we begin, ensure you have:</p><ul><li>.NET 9.0 or later installed</li><li>An active AWS account</li><li>Basic knowledge of C# and dependency injection</li><li>Visual Studio, VS Code, or JetBrains Rider</li></ul><h3>Setting Up AWS S3 and IAM</h3><h4>Step 1: Create an S3 Bucket</h4><ol><li>Log into the AWS Management Console</li><li>Navigate to the S3 service</li><li>Click “Create bucket”</li><li>Choose a unique bucket name (e.g., your-app-storage-bucket)</li><li>Select your preferred AWS region</li><li>Configure bucket settings (keep defaults for this tutorial)</li><li>Click “Create bucket”</li></ol><h4>Step 2: Create an IAM User</h4><ol><li>Navigate to the IAM service in AWS Console</li><li>Click “Users” → “Create user”</li><li>Enter a username (e.g., s3-dotnet-user)</li><li>Select “Programmatic access”</li><li>Click “Next: Permissions” and select your corresponding permissions like Amazon S3 FullAccess</li></ol><h4>Step 3: Generate Access Keys</h4><ol><li>Complete the user creation process</li><li>Save the Access Key ID and Secret Access Key securely</li><li><strong>Important</strong>: Never commit these keys to version control</li></ol><h3>Project Setup and Dependencies</h3><p>Create a new .NET console application and install the required NuGet packages:</p><pre>dotnet new console -n AmazonS3Sample<br>cd AmazonS3Sample<br>dotnet add package AWSSDK.S3<br>dotnet add package Microsoft.Extensions.Hosting<br>dotnet add package Microsoft.Extensions.Configuration<br>dotnet add package Microsoft.Extensions.Configuration.Json<br>dotnet add package Microsoft.Extensions.DependencyInjection</pre><h3>Implementation Overview</h3><p>Our implementation follows clean architecture principles with:</p><ul><li><strong>Configuration model</strong> for AWS settings</li><li><strong>Service interface</strong> for abstraction</li><li><strong>Service implementation</strong> with proper error handling</li><li><strong>Dependency injection setup</strong> for clean IoC</li><li><strong>Logging integration</strong> for monitoring</li></ul><h3>Creating the Configuration Model</h3><p>First, let’s create a configuration model to handle AWS settings:</p><pre>namespace AmazonS3Sample.Configuration;<br><br>public sealed class AwsStorageConfiguration<br>{<br>    public static string SectionName =&gt; &quot;Aws:Storage&quot;;<br>    <br>    public string AccessKeyId { get; set; } = null!;<br>    public string SecretAccessKey { get; set; } = null!;<br>    public string Region { get; set; } = null!;<br>    public string Bucket { get; set; } = null!;<br>}</pre><h3>Implementing the Storage Service</h3><h3>Service Interface</h3><p>Create a clean interface that defines our S3 operations:</p><pre>using System.Net.Mime;<br>using Amazon.S3.Model;<br><br>namespace AmazonS3Sample.Services.Interfaces;<br><br>public interface IAwsStorageClientService<br>{<br>    ValueTask&lt;IEnumerable&lt;string&gt;&gt; GetFileNamesAsync();<br>    ValueTask&lt;GetObjectResponse&gt; GetFileAsync(string fileName);<br>    ValueTask UploadFileAsync(string fileName, byte[] data, ContentType contentType);<br>    ValueTask DeleteFileAsync(string fileName);<br>}</pre><h3>Service Implementation</h3><p>Now, implement the service with comprehensive error handling and logging:</p><pre><br>using System.Net.Mime;<br>using Amazon;<br>using Amazon.S3;<br>using Amazon.S3.Model;<br>using AmazonS3Sample.Configuration;<br>using AmazonS3Sample.Services.Interfaces;<br>using Microsoft.Extensions.Logging;<br>using Microsoft.Extensions.Options;<br><br>namespace AmazonS3Sample.Services;<br>public class AwsStorageClientService : IAwsStorageClientService<br>{<br>    private readonly IAmazonS3 _client;<br>    private readonly IOptions&lt;AwsStorageConfiguration&gt; _options;<br>    private readonly ILogger&lt;AwsStorageClientService&gt; _logger;<br>    public AwsStorageClientService(IOptions&lt;AwsStorageConfiguration&gt; options, ILogger&lt;AwsStorageClientService&gt; logger)<br>    {<br>        _options = options;<br>        _logger = logger;<br>        _client = CreateAmazonS3Client();<br>    }<br>    <br>    public async ValueTask&lt;IEnumerable&lt;string&gt;&gt; GetFileNamesAsync()<br>    {<br>        _logger.LogInformation(&quot;Getting file names from bucket {Bucket}&quot;, _options.Value.Bucket);<br>        <br>        try<br>        {<br>            var result = await _client.ListObjectsV2Async(new ListObjectsV2Request<br>            {<br>                BucketName = _options.Value.Bucket,<br>            });<br>            <br>            if (result.S3Objects == null || !result.S3Objects.Any())<br>            {<br>                _logger.LogInformation(&quot;No files found in bucket {Bucket}&quot;, _options.Value.Bucket);<br>                return Array.Empty&lt;string&gt;();<br>            }<br>            return result.S3Objects.Select(x =&gt; x.Key);<br>        }<br>        catch (Exception e)<br>        {<br>            _logger.LogError(e, &quot;Error getting file names from bucket {Bucket}&quot;, _options.Value.Bucket);<br>            throw;<br>        }<br>    }<br>    public async ValueTask&lt;GetObjectResponse&gt; GetFileAsync(string fileName)<br>    {<br>        _logger.LogInformation(&quot;Getting file &#39;{FileName}&#39; from bucket {Bucket}&quot;, fileName, _options.Value.Bucket);<br>        try<br>        {<br>            var response = await _client.GetObjectAsync(_options.Value.Bucket, fileName);<br>            return response;<br>        }<br>        catch (Exception e)<br>        {<br>            _logger.LogError(e, &quot;Error getting file &#39;{FileName}&#39; from bucket {Bucket}&quot;, fileName, _options.Value.Bucket);<br>            throw;<br>        }<br>    }<br>    public async ValueTask UploadFileAsync(string fileName, byte[] data, ContentType contentType)<br>    {<br>        _logger.LogInformation(&quot;Uploading file &#39;{FileName}&#39; to bucket {Bucket}&quot;, fileName, _options.Value.Bucket);<br>        try<br>        {<br>            using var stream = new MemoryStream(data);<br>            var request = new PutObjectRequest<br>            {<br>                BucketName = _options.Value.Bucket,<br>                Key = fileName,<br>                InputStream = stream,<br>                ContentType = contentType.ToString()<br>            };<br>            <br>            var response = await _client.PutObjectAsync(request);<br>            _logger.LogInformation(&quot;File &#39;{FileName}&#39; uploaded successfully with status {StatusCode}&quot;, <br>                fileName, response.HttpStatusCode);<br>        }<br>        catch (Exception e)<br>        {<br>            _logger.LogError(e, &quot;Error uploading file &#39;{FileName}&#39; to bucket {Bucket}&quot;, fileName, _options.Value.Bucket);<br>            throw;<br>        }<br>    }<br>    public async ValueTask DeleteFileAsync(string fileName)<br>    {<br>        _logger.LogInformation(&quot;Deleting file &#39;{FileName}&#39; from bucket {Bucket}&quot;, fileName, _options.Value.Bucket);<br>        try<br>        {<br>            await _client.DeleteObjectAsync(_options.Value.Bucket, fileName);<br>            _logger.LogInformation(&quot;File &#39;{FileName}&#39; deleted successfully&quot;, fileName);<br>        }<br>        catch (Exception e)<br>        {<br>            _logger.LogError(e, &quot;Error deleting file &#39;{FileName}&#39; from bucket {Bucket}&quot;, fileName, _options.Value.Bucket);<br>            throw;<br>        }<br>    }<br>    <br>    private AmazonS3Client CreateAmazonS3Client()<br>    {<br>        var options = _options.Value;<br>        return new AmazonS3Client(options.AccessKeyId, options.SecretAccessKey, <br>            RegionEndpoint.GetBySystemName(options.Region));<br>    }<br>}</pre><h3>Setting Up Dependency Injection</h3><p>Create an extension method to register our services cleanly:</p><pre>using AmazonS3Sample.Configuration;<br>using AmazonS3Sample.Services;<br>using AmazonS3Sample.Services.Interfaces;<br>using Microsoft.Extensions.Configuration;<br>using Microsoft.Extensions.DependencyInjection;<br><br>namespace AmazonS3Sample.DependencyInjection;<br>public static class AwsStorageDependencyInjection<br>{<br>    public static IServiceCollection AddAwsStorageClientService(this IServiceCollection services, IConfiguration configuration)<br>    {<br>        services.Configure&lt;AwsStorageConfiguration&gt;(configuration.GetSection(AwsStorageConfiguration.SectionName));<br>        services.AddTransient&lt;IAwsStorageClientService, AwsStorageClientService&gt;();<br>        <br>        return services;<br>    }<br>}</pre><h3>Configuration and Environment Variables</h3><h3>appsettings.json Setup</h3><p>Create your configuration file (never commit real credentials):</p><pre>{<br>    &quot;Logging&quot;: {<br>        &quot;LogLevel&quot;: {<br>            &quot;Default&quot;: &quot;Information&quot;,<br>            &quot;System&quot;: &quot;Warning&quot;,<br>            &quot;Microsoft&quot;: &quot;Warning&quot;<br>        }<br>    },<br>    &quot;Aws&quot;: {<br>        &quot;Storage&quot;: {<br>            &quot;AccessKeyId&quot;: &quot;YOUR_ACCESS_KEY_ID&quot;,<br>            &quot;SecretAccessKey&quot;: &quot;YOUR_SECRET_ACCESS_KEY&quot;,<br>            &quot;Region&quot;: &quot;us-east-1&quot;,<br>            &quot;Bucket&quot;: &quot;your-bucket-name&quot;<br>        }<br>    }<br>}</pre><p><strong>Important</strong>: Use UserSecrets to store sensetive data during development.</p><h3>Using the Service in Your Application</h3><p>Here’s how to set up and use the service in your Program.cs:</p><pre>using AmazonS3Sample.DependencyInjection;<br>using AmazonS3Sample.Services.Interfaces;<br>using Microsoft.Extensions.Configuration;<br>using Microsoft.Extensions.DependencyInjection;<br>using Microsoft.Extensions.Hosting;<br><br>var builder = Host.CreateApplicationBuilder(args);<br><br>builder.Configuration<br>    .AddJsonFile(&quot;appsettings.json&quot;, optional: true, reloadOnChange: true)<br>    .AddEnvironmentVariables();<br>builder.Services.AddAwsStorageClientService(builder.Configuration);<br><br>using var host = builder.Build();<br><br>// Use the service<br>var storageService = host.Services.GetRequiredService&lt;IAwsStorageClientService&gt;();<br><br>// Example operations<br>try<br>{<br>    // List files<br>    var fileNames = await storageService.GetFileNamesAsync();<br>    Console.WriteLine(&quot;Files in S3 bucket:&quot;);<br>    foreach (var fileName in fileNames)<br>    {<br>        Console.WriteLine($&quot;- {fileName}&quot;);<br>    }<br><br>    // Upload a file<br>    var testContent = &quot;Hello, S3 from .NET!&quot;u8.ToArray();<br>    await storageService.UploadFileAsync(&quot;test-file.txt&quot;, testContent,<br>        new System.Net.Mime.ContentType(&quot;text/plain&quot;));<br>    Console.WriteLine(&quot;File uploaded successfully!&quot;);<br><br>    // Download the file<br>    var downloadedFile = await storageService.GetFileAsync(&quot;test-file.txt&quot;);<br>    using var reader = new StreamReader(downloadedFile.ResponseStream);<br>    var content = await reader.ReadToEndAsync();<br>    Console.WriteLine($&quot;Downloaded content: {content}&quot;);<br><br>    // Clean up<br>    await storageService.DeleteFileAsync(&quot;test-file.txt&quot;);<br>    Console.WriteLine(&quot;File deleted successfully!&quot;);<br>}<br>catch (Exception ex)<br>{<br>    Console.WriteLine($&quot;Error: {ex.Message}&quot;);<br>}<br><br>await host.RunAsync();</pre><h3>Complete Example Repository</h3><p>You can find the complete working example on GitHub: <a href="https://github.com/schroedermarius/amazon-s3-dotnet-sample">Amazon S3 .NET Sample Repository</a></p><h3>Conclusion</h3><p>Integrating Amazon S3 with .NET using dependency injection provides a clean, maintainable, and testable solution for cloud storage needs. By following the patterns and best practices outlined in this guide, you’ll have a robust foundation for handling file operations in your .NET applications.</p><p>The combination of proper dependency injection, configuration management, and error handling ensures your S3 integration is production-ready and follows modern .NET development practices.</p><p>Remember to always follow security best practices, never commit credentials to version control, and implement proper monitoring and logging for production environments.</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*iwwN4l6GTbMc-ErY.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5325dfa6065b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/how-to-easily-connect-to-amazon-s3-with-net-and-dependency-injection-a-complete-guide-5325dfa6065b">How to Easily Connect to Amazon S3 with .NET and Dependency Injection: A Complete Guide</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ Fast Excel Template Replacement in .NET — OpenXmlSdk vs ClosedXML]]></title>
            <link>https://medium.com/medialesson/fast-excel-template-replacement-in-net-openxmlsdk-vs-closedxml-088de1079714?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/088de1079714</guid>
            <category><![CDATA[openxml]]></category>
            <category><![CDATA[benchmark]]></category>
            <category><![CDATA[excel-automation]]></category>
            <category><![CDATA[excel]]></category>
            <category><![CDATA[dotnet]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Mon, 08 Sep 2025 08:22:45 GMT</pubDate>
            <atom:updated>2026-07-07T10:01:00.569Z</atom:updated>
            <content:encoded><![CDATA[<h3>📊 Fast Excel Template Replacement in .NET — OpenXmlSdk vs ClosedXML</h3><h4><em>Quick wins, real benchmarks, and when to trade developer joy for raw speed.</em></h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7ktAAotfq_J5EciQuwJuyA.png" /></figure><blockquote>Sponsored Content Disclosure</blockquote><blockquote>This article contains a paid sponsored section that was added as part of a commercial collaboration with Iron Software. The sponsored content is clearly separated from the original editorial content and does not form part of the original article.</blockquote><blockquote>Original article starts below.</blockquote><p>In a recent project we needed to take form data and inject it into an existing Excel template — replacing placeholder variables and producing filled spreadsheets automatically. We evaluated the available .NET libraries (looking at maintenance, maturity and community support) and found only two relevant contenders for this scenario:</p><pre>&lt;PackageReference Include=&quot;ClosedXML&quot; Version=&quot;0.105.0&quot; /&gt;<br>&lt;PackageReference Include=&quot;DocumentFormat.OpenXml&quot; Version=&quot;3.3.0&quot; /&gt;</pre><p>We compared both ergonomics (how easy is it to open and manipulate an Excel file?) and performance (measured with <a href="https://github.com/dotnet/BenchmarkDotNet">BenchmarkDotNet</a>). Below is a pragmatic walkthrough of what we did, the demo code we used, the benchmark results, and a recommendation you can reuse.</p><h3>TL;DR</h3><ul><li><strong>OpenXmlSdk</strong> = lower-level, faster, less memory allocation → best for high-throughput, server-side processing.</li><li><strong>ClosedXML</strong> = higher-level, developer-friendly, faster to implement → great for prototyping and templates with formatting.</li><li>Benchmarks show OpenXmlSdk is ~2.9–3.5× faster and uses ~1/3 of the allocations compared to ClosedXML in our tests.</li></ul><h3>A quick note about my background</h3><p>I already had some experience with <strong>OpenXmlSdk</strong> — it’s powerful, but you end up “sliding through rows and columns” more compared to ClosedXML’s niceties. That background helped when we needed to optimize for performance, but it also showed why ClosedXML is so popular for day-to-day work.</p><h3>What the demo does</h3><ol><li>Ensures a template exists (Assets/Template.xlsx).</li><li>Prompts the user for a set of variables (e.g. vehicleRegistration, revenueQ1, status_A, budget_A).</li><li>Copies the template into two files and performs token replacement using:</li></ol><ul><li>a low-level OpenXmlSdk approach (replacing text inside the shared string table), and</li><li>a high-level ClosedXML approach (iterating used cells and replacing values).</li></ul><p>Both demo methods replace tokens like ##Revenue_Q1## etc. with provided values (or defaults).</p><h3>Demo entry point (RunDemo)</h3><pre>public static void RunDemo()<br>{<br>    // Ensure template exists<br>    ExcelTemplateGenerator.EnsureTemplateExists();<br>    Console.WriteLine(&quot;=== Excel Replacement Demo ===&quot;);<br>    Console.WriteLine();<br><br>    // Collect all variables<br>    Console.WriteLine(&quot;Please enter value for &#39;Vehicle Registration&#39;:&quot;);<br>    var vehicleRegistration = Console.ReadLine();<br>    Console.WriteLine(&quot;Please enter value for &#39;Dashboard&#39;:&quot;);<br>    var dashboard = Console.ReadLine();<br>    Console.WriteLine(&quot;Please enter value for &#39;Defect Description&#39;:&quot;);<br>    var defectDescription = Console.ReadLine();<br>    Console.WriteLine(&quot;Please enter value for &#39;Revenue Q1&#39;:&quot;);<br>    var revenueQ1 = Console.ReadLine();<br>    Console.WriteLine(&quot;Please enter value for &#39;Profit Q1&#39;:&quot;);<br>    var profitQ1 = Console.ReadLine();<br>    Console.WriteLine(&quot;Please enter value for &#39;Status A&#39;:&quot;);<br>    var statusA = Console.ReadLine();<br>    Console.WriteLine(&quot;Please enter value for &#39;Budget A&#39;:&quot;);<br>    var budgetA = Console.ReadLine();<br><br>    var fileId = Guid.NewGuid();<br>    // Demo OpenXML SDK<br>    DemoOpenXmlSdk(fileId, vehicleRegistration, dashboard, defectDescription, revenueQ1, profitQ1, statusA, budgetA);<br>    // Demo ClosedXML<br>    DemoClosedXml(fileId, vehicleRegistration, dashboard, defectDescription, revenueQ1, profitQ1, statusA, budgetA);<br>    Console.WriteLine();<br>    Console.WriteLine(&quot;Demo completed!&quot;);<br>    Console.WriteLine($&quot;Files generated:&quot;);<br>    Console.WriteLine($&quot;- {fileId}_OpenXML.xlsx&quot;);<br>    Console.WriteLine($&quot;- {fileId}_ClosedXML.xlsx&quot;);<br>}</pre><h3>Open XML SDK approach (low-level, precise)</h3><p>This approach opens the XLSX package, iterates the shared string table and replaces placeholders directly in SharedStringItem s:</p><pre>private static void DemoOpenXmlSdk(Guid fileId, string? vehicleRegistration, string? dashboard, string? defectDescription, string? revenueQ1, string? profitQ1, string? statusA, string? budgetA)<br>{<br>    var fileName = $&quot;{fileId}_OpenXML.xlsx&quot;;<br>    File.Copy(&quot;Assets/Template.xlsx&quot;, fileName, true);<br>    Console.WriteLine($&quot;Created a new file with ID: {fileName}&quot;);<br>    Console.WriteLine(&quot;Using Open XML SDK to replace variables in the Excel file...&quot;);<br>    Stopwatch stopwatch = Stopwatch.StartNew();<br>    using (FileStream fileStream = new(fileName, FileMode.Open, FileAccess.ReadWrite))<br>    using (SpreadsheetDocument document = SpreadsheetDocument.Open(fileStream, true))<br>    {<br>        WorkbookPart? workbookPart = document.WorkbookPart;<br>        WorksheetPart? worksheetPart = workbookPart?.WorksheetParts.First();<br>        SheetData? sheetData = worksheetPart?.Worksheet.GetFirstChild&lt;SheetData&gt;();<br>        if (sheetData == null)<br>        {<br>            Console.WriteLine(&quot;No sheet data found in the workbook.&quot;);<br>            return;<br>        }<br>        foreach (Row row in sheetData.Elements&lt;Row&gt;())<br>        {<br>            foreach (Cell cell in row.Elements&lt;Cell&gt;())<br>            {<br>                if (cell.CellValue is not null &amp;&amp; cell.DataType != null &amp;&amp; cell.DataType.Value == CellValues.SharedString)<br>                {<br>                    int id = int.Parse(cell.CellValue.InnerText);<br>                    SharedStringItem cellTextItem = workbookPart!.SharedStringTablePart!.SharedStringTable.Elements&lt;SharedStringItem&gt;().ElementAt(id);<br>                    string text = cellTextItem.InnerText;<br>                    text = text.Replace(&quot;##VehicleRegistration##&quot;, vehicleRegistration ?? &quot;DefaultVehicle&quot;)<br>                        .Replace(&quot;##Dashboard##&quot;, dashboard ?? &quot;DefaultDashboard&quot;)<br>                        .Replace(&quot;##DefectDescription##&quot;, defectDescription ?? &quot;DefaultDefect&quot;)<br>                        .Replace(&quot;##Date##&quot;, DateTime.Now.ToString(&quot;MM/dd/yyyy&quot;))<br>                        .Replace(&quot;##Revenue_Q1##&quot;, revenueQ1 ?? &quot;450,000&quot;)<br>                        .Replace(&quot;##Profit_Q1##&quot;, profitQ1 ?? &quot;85,000&quot;)<br>                        .Replace(&quot;##Costs_Q1##&quot;, &quot;365,000&quot;)<br>                        .Replace(&quot;##Margin_Q1##&quot;, &quot;18.9&quot;)<br>                        .Replace(&quot;##Revenue_Q2##&quot;, &quot;520,000&quot;)<br>                        .Replace(&quot;##Profit_Q2##&quot;, &quot;95,000&quot;)<br>                        .Replace(&quot;##Costs_Q2##&quot;, &quot;425,000&quot;)<br>                        .Replace(&quot;##Margin_Q2##&quot;, &quot;18.3&quot;)<br>                        .Replace(&quot;##Revenue_Q3##&quot;, &quot;580,000&quot;)<br>                        .Replace(&quot;##Profit_Q3##&quot;, &quot;110,000&quot;)<br>                        .Replace(&quot;##Costs_Q3##&quot;, &quot;470,000&quot;)<br>                        .Replace(&quot;##Margin_Q3##&quot;, &quot;19.0&quot;)<br>                        .Replace(&quot;##Revenue_Q4##&quot;, &quot;620,000&quot;)<br>                        .Replace(&quot;##Profit_Q4##&quot;, &quot;125,000&quot;)<br>                        .Replace(&quot;##Costs_Q4##&quot;, &quot;495,000&quot;)<br>                        .Replace(&quot;##Margin_Q4##&quot;, &quot;20.2&quot;)<br>                        .Replace(&quot;##Status_A##&quot;, statusA ?? &quot;Completed&quot;)<br>                        .Replace(&quot;##Budget_A##&quot;, budgetA ?? &quot;75,000&quot;)<br>                        .Replace(&quot;##Status_B##&quot;, &quot;In Progress&quot;)<br>                        .Replace(&quot;##Budget_B##&quot;, &quot;120,000&quot;)<br>                        .Replace(&quot;##Status_C##&quot;, &quot;Planned&quot;)<br>                        .Replace(&quot;##Budget_C##&quot;, &quot;200,000&quot;);<br>                    cellTextItem.GetFirstChild&lt;Text&gt;()!.Text = text;<br>                }<br>            }<br>        }<br>        worksheetPart?.Worksheet.Save();<br>    }<br>    stopwatch.Stop();<br>    Console.WriteLine($&quot;Replacement completed in {stopwatch.ElapsedMilliseconds} ms using Open XML SDK!&quot;);<br>}</pre><p><strong>Why choose it:</strong> efficient, minimal allocations, fine-grained control.<br><strong>Downside:</strong> more verbose and more code to maintain.</p><h3>ClosedXML approach (high-level, ergonomic)</h3><p>This approach opens the workbook with ClosedXML, iterates used cells and replaces values:</p><pre>private static void DemoClosedXml(Guid fileId, string? vehicleRegistration, string? dashboard, string? defectDescription, string? revenueQ1, string? profitQ1, string? statusA, string? budgetA)<br>{<br>    var fileName = $&quot;{fileId}_ClosedXML.xlsx&quot;;<br>    File.Copy(&quot;Assets/Template.xlsx&quot;, fileName, true);<br>    Console.WriteLine($&quot;Created a new file with ID: {fileName}&quot;);<br>    Console.WriteLine(&quot;Using ClosedXML to replace variables in the Excel file...&quot;);<br>    Stopwatch stopwatch = Stopwatch.StartNew();<br>    using (var workbook = new XLWorkbook(fileName))<br>    {<br>        var worksheet = workbook.Worksheet(1);<br>        foreach (var cell in worksheet.CellsUsed())<br>        {<br>            if (cell.HasFormula) continue;<br>            var value = cell.GetString();<br>            value = value.Replace(&quot;##VehicleRegistration##&quot;, vehicleRegistration ?? &quot;DefaultVehicle&quot;)<br>                         .Replace(&quot;##Dashboard##&quot;, dashboard ?? &quot;DefaultDashboard&quot;)<br>                         .Replace(&quot;##DefectDescription##&quot;, defectDescription ?? &quot;DefaultDefect&quot;)<br>                         .Replace(&quot;##Date##&quot;, DateTime.Now.ToString(&quot;MM/dd/yyyy&quot;))<br>                         .Replace(&quot;##Revenue_Q1##&quot;, revenueQ1 ?? &quot;450,000&quot;)<br>                         .Replace(&quot;##Profit_Q1##&quot;, profitQ1 ?? &quot;85,000&quot;)<br>                         .Replace(&quot;##Costs_Q1##&quot;, &quot;365,000&quot;)<br>                         .Replace(&quot;##Margin_Q1##&quot;, &quot;18.9&quot;)<br>                         .Replace(&quot;##Revenue_Q2##&quot;, &quot;520,000&quot;)<br>                         .Replace(&quot;##Profit_Q2##&quot;, &quot;95,000&quot;)<br>                         .Replace(&quot;##Costs_Q2##&quot;, &quot;425,000&quot;)<br>                         .Replace(&quot;##Margin_Q2##&quot;, &quot;18.3&quot;)<br>                         .Replace(&quot;##Revenue_Q3##&quot;, &quot;580,000&quot;)<br>                         .Replace(&quot;##Profit_Q3##&quot;, &quot;110,000&quot;)<br>                         .Replace(&quot;##Costs_Q3##&quot;, &quot;470,000&quot;)<br>                         .Replace(&quot;##Margin_Q3##&quot;, &quot;19.0&quot;)<br>                         .Replace(&quot;##Revenue_Q4##&quot;, &quot;620,000&quot;)<br>                         .Replace(&quot;##Profit_Q4##&quot;, &quot;125,000&quot;)<br>                         .Replace(&quot;##Costs_Q4##&quot;, &quot;495,000&quot;)<br>                         .Replace(&quot;##Margin_Q4##&quot;, &quot;20.2&quot;)<br>                         .Replace(&quot;##Status_A##&quot;, statusA ?? &quot;Completed&quot;)<br>                         .Replace(&quot;##Budget_A##&quot;, budgetA ?? &quot;75,000&quot;)<br>                         .Replace(&quot;##Status_B##&quot;, &quot;In Progress&quot;)<br>                         .Replace(&quot;##Budget_B##&quot;, &quot;120,000&quot;)<br>                         .Replace(&quot;##Status_C##&quot;, &quot;Planned&quot;)<br>                         .Replace(&quot;##Budget_C##&quot;, &quot;200,000&quot;);<br>            cell.Value = value;<br>        }<br>        workbook.Save();<br>    }<br>    stopwatch.Stop();<br>    Console.WriteLine($&quot;Replacement completed in {stopwatch.ElapsedMilliseconds} ms using ClosedXML!&quot;);<br>}</pre><p><strong>Why choose it:</strong> readable code, fast to implement, great for formatting and formulas.<br><strong>Downside:</strong> higher memory allocations and lower throughput for bulk processing.</p><h3>Benchmarks (real runs)</h3><p>We ran BenchmarkDotNet for both methods. Here are the trimmed results relevant to decision-making:</p><pre><br>+------------+-------------+----------+----------+------------+-------------+<br>|   Method   | Environment |   Mean   |   Gen0   | Allocated  | Alloc Ratio |<br>+------------+-------------+----------+----------+------------+-------------+<br>| OpenXmlSdk | Job-YETQRT  | 3.101 ms | 46.8750  | 317.09 KB  | 1.00        |<br>| ClosedXml  | Job-YETQRT  | 4.733 ms | 171.8750 | 1098.7 KB  | 3.46        |<br>| OpenXmlSdk | .NET 9.0    | 1.057 ms | 50.7813  | 316.62 KB  | 1.00        |<br>| ClosedXml  | .NET 9.0    | 3.264 ms | 171.8750 | 1099.18 KB | 3.47        |<br>+------------+-------------+----------+----------+------------+-------------+</pre><p><strong>Conclusion from benchmarks:</strong> OpenXmlSdk is notably faster and uses far fewer allocations. For many-file generation scenarios, that difference matters.</p><h3>✅ Recommendation (practical)</h3><p><strong>Choose Open XML SDK when</strong>:</p><ul><li>You need the best performance and minimal allocations.</li><li>You generate many files concurrently or work at scale.</li><li>You don’t mind more detailed code and extra boilerplate.</li></ul><p><strong>Choose ClosedXML when</strong>:</p><ul><li>Developer speed matters and you want readable, maintainable code.</li><li>Templates include complicated formatting / ranges you want to manipulate easily.</li><li>Throughput demands are moderate.</li></ul><h4>Sponsored Section — A Third Option for Excel Processing with IronXL</h4><p>The following content was provided as part of a paid collaboration with Iron Software and is published separately from the original article.</p><h3>A Third Option for Broader Pipelines: IronXL</h3><p>Worth adding a footnote to the two-contender comparison: there’s a third library that wasn’t in scope for a pure performance benchmark but fits the same developer-friendly tier as ClosedXML — with a wider output surface if template filling isn’t the end of the pipeline.</p><p><strong>IronXL</strong> is a commercial .NET library from Iron Software. The honest performance positioning up front: it sits in the same tier as ClosedXML, not OpenXmlSdk. If raw throughput and minimal allocations are the deciding criteria, OpenXmlSdk remains the recommendation from the benchmarks above. IronXL earns its place when the project needs more than template filling from the same workbook object.</p><h4>The Same Token Replacement, with IronXL</h4><pre>using IronXL;<br><br>WorkBook workbook = WorkBook.Load(&quot;Assets/Template.xlsx&quot;);<br>WorkSheet sheet = workbook.DefaultWorkSheet;<br><br>foreach (var row in sheet.Rows)<br>{<br>    foreach (var cell in row)<br>    {<br>        if (cell.HasFormula) continue;<br><br>        string value = cell.StringValue;<br>        value = value<br>            .Replace(&quot;##VehicleRegistration##&quot;, vehicleRegistration ?? &quot;DefaultVehicle&quot;)<br>            .Replace(&quot;##Dashboard##&quot;,           dashboard           ?? &quot;DefaultDashboard&quot;)<br>            .Replace(&quot;##DefectDescription##&quot;,   defectDescription   ?? &quot;DefaultDefect&quot;)<br>            .Replace(&quot;##Date##&quot;,                DateTime.Now.ToString(&quot;MM/dd/yyyy&quot;))<br>            .Replace(&quot;##Revenue_Q1##&quot;,          revenueQ1           ?? &quot;450,000&quot;)<br>            .Replace(&quot;##Profit_Q1##&quot;,           profitQ1            ?? &quot;85,000&quot;)<br>            .Replace(&quot;##Status_A##&quot;,            statusA             ?? &quot;Completed&quot;)<br>            .Replace(&quot;##Budget_A##&quot;,            budgetA             ?? &quot;75,000&quot;);<br><br>        cell.Value = value;<br>    }<br>}<br><br>workbook.SaveAs($&quot;{fileId}_IronXL.xlsx&quot;);</pre><p>The structure is almost identical to the ClosedXML version — same iteration pattern, same .Replace() chain, same .SaveAs(). The code volume is comparable.</p><h4>Where the Scope Widens</h4><p>The case for IronXL over ClosedXML isn’t template replacement — they’re equivalent there. It’s what happens after the template is filled. The same WorkBook object covers:</p><pre>// After token replacement — export the same workbook in multiple formats<br>workbook.SaveAsCsv($&quot;{fileId}.csv&quot;);      // downstream systems that expect CSV<br><br>// Aggregate the revenue cells that were just filled<br>decimal totalRevenue = sheet[&quot;C5:C8&quot;].Sum();   // LINQ range aggregates<br><br>// Hand off to a DataTable for a SQL insert or report<br>DataTable dt = sheet.ToDataTable(true);<br><br>// Evaluate formulas that reference the replaced values<br>sheet[&quot;D9&quot;].Formula = &quot;=SUM(D5:D8)&quot;;</pre><p>None of that needs a second library. For a pipeline where the filled template also needs to be exported, aggregated, or fed into another system — which is common in form-data-to-spreadsheet workflows like the one this article describes — that consolidated surface area reduces dependencies.</p><h4>Updated Recommendation Table</h4><p>Folding IronXL into the recommendation from the benchmarks:</p><p><strong>Choose OpenXmlSdk when:</strong> maximum throughput, minimum allocations, high-concurrency file generation, you’re comfortable with more verbose code.</p><p><strong>Choose ClosedXML when:</strong> developer speed matters, readable code is the priority, moderate throughput is fine, free/open-source is a requirement.</p><p><strong>Consider IronXL when:</strong> the project also needs multi-format output (CSV, JSON, DataTable) from the same workbook, formula evaluation, or LINQ range aggregates — and a commercial license is acceptable.</p><h4>Honest Trade-Off</h4><p>Both libraries compared in the article are <strong>free</strong>. IronXL is commercial — that’s the primary reason it wasn’t in scope for a “two relevant contenders” evaluation and the primary reason to not shortlist it when open-source is a constraint.<a href="https://ironsoftware.com/csharp/excel/licensing/"> Free 30-day trial</a> available if the broader feature set justifies evaluating it.</p><p>👉<a href="https://ironsoftware.com/csharp/excel/"> IronXL for .NET</a> ·<a href="https://ironsoftware.com/csharp/excel/how-to/csharp-excel-template/"> Excel template filling</a> ·<a href="https://ironsoftware.com/csharp/excel/licensing/"> Free 30-day trial</a></p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oV8boWWoErRvTb33.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=088de1079714" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/fast-excel-template-replacement-in-net-openxmlsdk-vs-closedxml-088de1079714">📊 Fast Excel Template Replacement in .NET — OpenXmlSdk vs ClosedXML</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[️ Installing .NET on macOS with dotnet-install.sh (and a Few Lessons Learned)]]></title>
            <link>https://medium.com/medialesson/https-www-marius-schroeder-de-installing-net-on-macos-with-dotnet-install-sh-and-a-few-lessons-learned-5ffcf07e393e?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/5ffcf07e393e</guid>
            <category><![CDATA[mac]]></category>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[preview]]></category>
            <category><![CDATA[bash]]></category>
            <category><![CDATA[apple]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Fri, 29 Aug 2025 06:46:52 GMT</pubDate>
            <atom:updated>2025-08-29T06:46:52.559Z</atom:updated>
            <content:encoded><![CDATA[<h3>🖥️ Installing .NET on macOS with dotnet-install.sh (and a Few Lessons Learned)</h3><h4>Recently, I tried installing .NET on my Mac using the official script provided by Microsoft. While it works, I ran into a couple of small quirks that I’d like to share — so you don’t have to stumble over the same things.</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mobMOUmKsjQcFiEXo6H1iw.png" /></figure><h3>Step 1: Download the Script</h3><p>Microsoft provides a handy install script: <a href="https://learn.microsoft.com/en-us/dotnet/core/install/macos#install-net-with-a-script">dotnet-install.sh</a>.</p><p>You can either:</p><ul><li>Use curl to grab it, or</li><li>Download it directly in the browser.</li></ul><p>⚠️ Important: Make sure the file isn’t empty — you’ll want a <em>real</em> dotnet-install.sh script, not a zero-byte placeholder. 😉</p><h3>Step 2: Put It Somewhere Safe</h3><p>I moved the script into my <strong>home directory</strong>, so it won’t get deleted accidentally when cleaning up my Downloads folder (you know how that goes…).</p><h3>Step 3: Run the Script</h3><p>Running the script directly will install (depending on the configuration of the tool) the current LTS (Long Term Support), which (at the time of writing) is <strong>.NET 8</strong>.</p><h3>Step 4: Update Your Terminal Path</h3><p>To make .NET globally available, you’ll need to update your shell config.</p><p>I’m using <strong>zsh</strong>, so I edited my .zshrc and added the following lines at the bottom:</p><pre>export PATH=&quot;$HOME/.dotnet/tools:$PATH&quot;<br>export PATH=&quot;$HOME/.dotnet:$PATH&quot;</pre><p>Then reload the file:</p><pre>source ~/.zshrc</pre><p>Now you can check if .NET is installed:</p><pre>dotnet --list-sdks</pre><h3>Step 5: Installing .NET 9 and .NET 10 Preview</h3><p>For me, it was important to have not only .NET 8, but also <strong>.NET 9</strong> and <strong>.NET 10 Preview</strong>.</p><p><strong>.NET 9</strong> is easy:</p><pre>./dotnet-install.sh -c 9.0</pre><p><strong>.NET 10 Preview</strong> requires one more trick: you need to specify the <strong>quality</strong> parameter.</p><p>⚠️ Heads-up: the short form -q didn’t work for me. You also need to put --quality <strong>before</strong> the -c parameter.</p><pre>./dotnet-install.sh --quality preview -c 10.0</pre><h3>Bonus: Script Documentation 📖</h3><p>If you’re curious what else the install script can do, check out the official documentation:<br> 👉 <a href="https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script">dotnet-install script reference</a></p><h3>Takeaway 🎯</h3><p>If you’re installing .NET on macOS:</p><ul><li>Download the script,</li><li>Place it somewhere safe,</li><li>Update your shell config, and</li><li>Use the right parameters for .NET 9 and 10.</li></ul><p>And just like that, you’re running multiple .NET versions side by side on your Mac. 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5ffcf07e393e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/https-www-marius-schroeder-de-installing-net-on-macos-with-dotnet-install-sh-and-a-few-lessons-learned-5ffcf07e393e">🖥️ Installing .NET on macOS with dotnet-install.sh (and a Few Lessons Learned)</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ Taming Generics, Interfaces & JSON in .NET — Meet JsonPolymorphic]]></title>
            <link>https://medium.com/medialesson/taming-generics-interfaces-json-in-net-meet-jsonpolymorphic-579820451bb4?source=rss-4bc650e7a5a6------2</link>
            <guid isPermaLink="false">https://medium.com/p/579820451bb4</guid>
            <category><![CDATA[generics]]></category>
            <category><![CDATA[polymorphism]]></category>
            <category><![CDATA[json]]></category>
            <category><![CDATA[inheritance]]></category>
            <category><![CDATA[dotnet]]></category>
            <dc:creator><![CDATA[Marius Schröder]]></dc:creator>
            <pubDate>Fri, 22 Aug 2025 12:59:03 GMT</pubDate>
            <atom:updated>2025-08-22T12:59:03.782Z</atom:updated>
            <content:encoded><![CDATA[<h3>🧩 Taming Generics, Interfaces &amp; JSON in .NET — Meet JsonPolymorphic</h3><h4>Goodbye custom converters, hello clean polymorphic JSON in .NET 7+</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gH3o_brCxnrMFitKj_L6Gg.png" /></figure><p>Ever tried to split lots of different actions into <strong>inheritance-friendly classes</strong> and <strong>interfaces</strong>, then make them configurable through a connected frontend?</p><p>Sounds easy, right?<br> Yeah… until you try to <strong>serialize and deserialize</strong> them with all those <strong>different generic parameters</strong> floating around. Then things get ugly. 😅</p><h3>The First Attempts (a.k.a. “How to Make Your Life Harder”)</h3><p>Our first approach was to just use dynamic.<br> .NET: <em>&quot;Sure, I can do that… wait, no, I can’t make you a nice mixed list of completely unrelated types without breaking everything.&quot;</em></p><p>Then we tried building <strong>custom converters</strong>.<br>They worked, though the approach was both <strong>time-consuming</strong> and <strong>prone to errors</strong>.</p><h3>The Hero Arrives: JsonPolymorphic ✨</h3><p>Starting from <strong>.NET 7</strong>, System.Text.Json got a new superpower — <strong>polymorphic serialization and deserialization</strong> without having to roll your own converter factory from scratch.</p><p>Here’s the magic:</p><pre>[JsonPolymorphic(TypeDiscriminatorPropertyName = &quot;$type&quot;)]<br>[JsonDerivedType(typeof(Configuration), &quot;config&quot;)]<br>[JsonDerivedType(typeof(Configuration2), &quot;config2&quot;)]<br>public interface IConfiguration { }<br><br>public class Configuration : IConfiguration<br>{<br>    public int Number { get; set; }<br>    public string Name { get; set; }<br>    public bool IsActive { get; set; }<br>}<br><br>public class Configuration2 : IConfiguration<br>{<br>    public string Configuration { get; set; }<br>}</pre><h3>Serializing</h3><pre>var obj = new List&lt;IConfiguration&gt;<br>{<br>    new Configuration { Number = 42, Name = &quot;MainConfig&quot;, IsActive = true },<br>    new Configuration2 { Configuration = &quot;Special settings&quot; }<br>};<br><br>var options = new JsonSerializerOptions<br>{<br>    WriteIndented = true<br>};<br><br>var json = JsonSerializer.Serialize(obj, options);<br>Console.WriteLine(json);</pre><p>The output now includes a $type discriminator:</p><pre>[<br>  {<br>    &quot;$type&quot;: &quot;config&quot;,<br>    &quot;Number&quot;: 42,<br>    &quot;Name&quot;: &quot;MainConfig&quot;,<br>    &quot;IsActive&quot;: true<br>  },<br>  {<br>    &quot;$type&quot;: &quot;config2&quot;,<br>    &quot;Configuration&quot;: &quot;Special settings&quot;<br>  }<br>]</pre><h3>Why This Is Awesome 🚀</h3><ul><li><strong>No more hand-crafted converters</strong> (and their inevitable bugs).</li><li><strong>Works out-of-the-box</strong> with System.Text.Json.</li><li><strong>Keeps your interfaces intact</strong>, so your architecture stays clean.</li></ul><p>When deserializing, .NET will look at the $type and <strong>automatically pick the right implementation</strong> for you.</p><h3>TL;DR 📝</h3><p>If you’ve been fighting with serializing/deserializing <strong>interfaces or abstract classes</strong> in .NET and ended up in <strong>dynamic hell</strong> or <strong>converter purgatory</strong>,<br> ➡️ try JsonPolymorphic and JsonDerivedType.<br> Your future self will thank you. 🙌</p><figure><a href="https://rebrand.ly/t5rftjb"><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LWNo9_mtn5ZwM6KS.png" /></a></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=579820451bb4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/medialesson/taming-generics-interfaces-json-in-net-meet-jsonpolymorphic-579820451bb4">🧩 Taming Generics, Interfaces &amp; JSON in .NET — Meet JsonPolymorphic</a> was originally published in <a href="https://medium.com/medialesson">Medialesson</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>