<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.0.1">Jekyll</generator><link href="https://blog.burgyn.online/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.burgyn.online/" rel="alternate" type="text/html" /><updated>2026-03-19T21:06:04+00:00</updated><id>https://blog.burgyn.online/feed.xml</id><title type="html">Burgyn’s blog</title><subtitle>A blog about everything possible and impossible that dotnet developer face every day.</subtitle><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><entry><title type="html">TeaPie: Testing real-world API scenarios</title><link href="https://blog.burgyn.online/2026/03/18/teapie-real-world-api-testing" rel="alternate" type="text/html" title="TeaPie: Testing real-world API scenarios" /><published>2026-03-18T17:00:00+00:00</published><updated>2026-03-18T17:00:00+00:00</updated><id>https://blog.burgyn.online/2026/03/18/teapie-real-world-api-testing</id><content type="html" xml:base="https://blog.burgyn.online/2026/03/18/teapie-real-world-api-testing"><![CDATA[<p>In the <a href="/2026/03/12/teapie-getting-started">first article</a>,
I showed how to get from zero to passing tests in a few minutes - directives,
a <code class="highlighter-rouge">POST</code> with a <code class="highlighter-rouge">.csx</code> script, and chaining IDs across test cases.</p>

<p>This post skips repeating that path. I won’t walk through extra CRUD steps or
another “invalid body returns 400” example - you already have the pattern from
Part 1. Instead: <strong>environments</strong>, how this demo API expects an <strong>API key</strong>,
<strong>pre-request scripts</strong> that feed variables into <code class="highlighter-rouge">.http</code> files, <strong>built-in
functions</strong> for inline values, and <strong>retries</strong> - both for flaky HTTP responses
and for work that finishes asynchronously in the background.</p>

<p>I’m still using
<a href="https://github.com/Burgyn/MMLib.DummyApi">MMLib.DummyApi</a> as the demo backend.
If you haven’t read
<a href="/2026/03/12/teapie-getting-started">Part 1</a>, start there; spin up
the API with
<code class="highlighter-rouge">docker run -p 8080:8080 ghcr.io/burgyn/mmlib-dummyapi</code>.</p>

<h2 id="environments">Environments</h2>

<p>Until now, the API base URL was hardcoded in every <code class="highlighter-rouge">.http</code> file. That breaks
the moment you want to run the same tests against staging. Let’s fix that.</p>

<p>Create <code class="highlighter-rouge">.teapie/env.json</code> in your project root:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"$shared"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ApiBaseUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApiKey"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test-api-key-123"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"staging"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ApiBaseUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://my-staging-api.example.com"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="highlighter-rouge">$shared</code> is the default environment - its variables are always available.
<code class="highlighter-rouge">staging</code> overrides <code class="highlighter-rouge">ApiBaseUrl</code> for that specific environment.</p>

<p>Now update your <code class="highlighter-rouge">.http</code> files to use the variable:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
GET {{ApiBaseUrl}}/products
</span></code></pre></div></div>

<p>Run against local (default):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie <span class="nb">test </span>Tests/
</code></pre></div></div>

<p>Run against staging:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie <span class="nb">test </span>Tests/ <span class="nt">-e</span> staging
</code></pre></div></div>

<p>One test collection, multiple environments.</p>

<blockquote>
  <p>Of course, tests won’t pass against <code class="highlighter-rouge">staging</code> right now - that URL doesn’t
exist. Replace it with your real staging endpoint when you have one.</p>
</blockquote>

<h2 id="api-keys-in-this-demo-and-what-comes-next">API keys in this demo (and what comes next)</h2>

<p>The DummyApi <code class="highlighter-rouge">orders</code> endpoints expect an <code class="highlighter-rouge">X-Api-Key</code> header. Without it, you get
<code class="highlighter-rouge">401</code>. For this sample, put the key in <strong><code class="highlighter-rouge">.teapie/env.json</code></strong> (the same file as
in <a href="#environments">Environments</a>). The <code class="highlighter-rouge">ApiKey</code> property becomes variable
{{ApiKey}} in <code class="highlighter-rouge">.http</code> files. That is handy for workshops
and Docker demos, but not how every real API handles identity.</p>

<p>A follow-up post will cover other authentication styles and a <strong>custom auth
provider</strong> in TeaPie so you don’t have to paste secrets into every request by
hand.</p>

<p>Positive path: use the env value in a header the same way you use
{{ApiBaseUrl}}. Create
<code class="highlighter-rouge">Tests/002-Orders/001-List-Orders-Authorized-req.http</code>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
GET {{ApiBaseUrl}}/orders
X-Api-Key: {{ApiKey}}
</span></code></pre></div></div>

<p><code class="highlighter-rouge">ApiKey</code> is resolved from <code class="highlighter-rouge">.teapie/env.json</code> when you run <code class="highlighter-rouge">teapie test</code>.</p>

<p>Quick negative check - no header, expect rejection. Create
<code class="highlighter-rouge">Tests/002-Orders/004-Unauthorized-Access-req.http</code>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">## TEST-EXPECT-STATUS: [401]
GET {{ApiBaseUrl}}/orders
</span></code></pre></div></div>

<h2 id="pre-request-scripts-set-variables-keep-http-readable">Pre-request scripts: set variables, keep <code class="highlighter-rouge">.http</code> readable</h2>

<p>Sometimes you need to prepare values <strong>before</strong> TeaPie sends the request. Add a
<strong>pre-request script</strong> next to the test case: same base name, suffix
<code class="highlighter-rouge">-init.csx</code>. It runs first; use <code class="highlighter-rouge">tp.SetVariable</code> for anything the <code class="highlighter-rouge">.http</code> file
should reference by name.</p>

<p>Example: <code class="highlighter-rouge">Tests/002-Orders/002-Create-Order-init.csx</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tp</span><span class="p">.</span><span class="nf">SetVariable</span><span class="p">(</span><span class="s">"OrderCustomerId"</span><span class="p">,</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">().</span><span class="nf">ToString</span><span class="p">());</span>
</code></pre></div></div>

<p>Http file: <code class="highlighter-rouge">Tests/002-Orders/002-Create-Order-init.http</code>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># @name CreateOrderRequest
## TEST-EXPECT-STATUS: [201]
POST {{ApiBaseUrl}}/orders
Content-Type: application/json
X-Api-Key: {{ApiKey}}

{
  "customerId": "{{OrderCustomerId}}",
  "customerName": "TeaPie Demo Customer",
  "customerEmail": "teapie.demo@example.com",
  "totalAmount": 29.98,
  "status": "pending",
  "shippingAddress": "123 TeaPie Lane",
  "shippingCity": "Demo City",
  "shippingCountry": "SK"
}
</span></code></pre></div></div>

<p><code class="highlighter-rouge">002-Create-Order-test.csx</code> (shortened):</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="nf">Test</span><span class="p">(</span><span class="s">"Created order should have a valid ID."</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">dynamic</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">GetBodyAsExpandoAsync</span><span class="p">();</span>
    <span class="nf">True</span><span class="p">(</span><span class="n">body</span><span class="p">.</span><span class="n">id</span> <span class="p">!=</span> <span class="k">null</span><span class="p">);</span>
    <span class="n">tp</span><span class="p">.</span><span class="nf">SetVariable</span><span class="p">(</span><span class="s">"NewOrderId"</span><span class="p">,</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
<span class="p">});</span>

<span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="nf">Test</span><span class="p">(</span><span class="s">"Order status should start as 'pending'."</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">dynamic</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">GetBodyAsExpandoAsync</span><span class="p">();</span>
    <span class="nf">Equal</span><span class="p">(</span><span class="s">"pending"</span><span class="p">,</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">status</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="built-in-functions-in-http-files">Built-in functions in <code class="highlighter-rouge">.http</code> files</h2>

<p>TeaPie can inject dynamic values <strong>directly</strong> in the request file, without an
<code class="highlighter-rouge">init</code> script. You can use build-in functions; names start with
<code class="highlighter-rouge">$</code> and use space-separated arguments (no commas), for example
<code class="highlighter-rouge">{{$randomInt 1 100}}</code>.</p>

<p>The defaults today are:</p>

<table>
  <thead>
    <tr>
      <th>Function</th>
      <th>Role</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="highlighter-rouge">$guid</code></td>
      <td>New GUID</td>
    </tr>
    <tr>
      <td><code class="highlighter-rouge">$now</code></td>
      <td>Current local time, optional format string</td>
    </tr>
    <tr>
      <td><code class="highlighter-rouge">$rand</code></td>
      <td>Random double in [0, 1)</td>
    </tr>
    <tr>
      <td><code class="highlighter-rouge">$randomInt</code></td>
      <td>Random int in [min, max)</td>
    </tr>
  </tbody>
</table>

<p>You can rewrite preview <code class="highlighter-rouge">.http</code> file without <code class="highlighter-rouge">-init.csx</code> file.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># @name CreateOrderRequest
## TEST-EXPECT-STATUS: [201]
POST {{ApiBaseUrl}}/orders
Content-Type: application/json
X-Api-Key: {{ApiKey}}

{
  "customerId": "{{$guid}}",
  "customerName": "TeaPie Demo Customer",
  "customerEmail": "teapie.demo@example.com",
  "totalAmount": 29.98,
  "status": "pending",
  "shippingAddress": "123 TeaPie Lane",
  "shippingCity": "Demo City",
  "shippingCountry": "SK"
}
</span></code></pre></div></div>

<p>You can also define <strong>your own functions</strong> and use them from <code class="highlighter-rouge">.http</code>
files like the built-ins. How that works is worth its own walkthrough - I’ll
cover it in the next article.</p>

<h2 id="retrying-when-the-server-answers-not-yet">Retrying when the server answers “not yet”</h2>

<p>Sometimes the API is up, but the <strong>response is not ready</strong> - cold starts, short
outages, overloaded instances, or an honest <code class="highlighter-rouge">500</code>. Reasonable clients <strong>repeat
the request</strong> with backoff until they get a successful status (or give up).</p>

<p>DummyApi can force that shape without a real outage. You flip behavior with
simulation headers (full list is in the
<a href="https://github.com/Burgyn/MMLib.DummyApi">DummyApi README</a>):</p>

<table>
  <thead>
    <tr>
      <th>Header</th>
      <th>Effect</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="highlighter-rouge">X-Simulate-Delay: 500</code></td>
      <td>Add 500 ms delay before responding</td>
    </tr>
    <tr>
      <td><code class="highlighter-rouge">X-Simulate-Error: true</code></td>
      <td>Return <code class="highlighter-rouge">500</code></td>
    </tr>
    <tr>
      <td><code class="highlighter-rouge">X-Simulate-Retry: 3</code></td>
      <td>Two failing responses, then success on the 3rd</td>
    </tr>
    <tr>
      <td><code class="highlighter-rouge">X-Request-Id</code></td>
      <td>Correlation id; required when using <code class="highlighter-rouge">X-Simulate-Retry</code></td>
    </tr>
  </tbody>
</table>

<p>For a <strong>worked example</strong>, ask for two failures then success, and tell TeaPie to
retry until it finally sees <code class="highlighter-rouge">200</code>:</p>

<p><code class="highlighter-rouge">Tests/003-Retry/001-Simulate-Flaky-Get-req.http</code>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
## RETRY-MAX-ATTEMPTS: 5
## RETRY-UNTIL-STATUS: [200]
GET {{ApiBaseUrl}}/products
X-Simulate-Retry: 3
X-Request-Id: {{$guid}}
</span></code></pre></div></div>

<p>Without retries, the first responses would be errors. With <code class="highlighter-rouge">RETRY-UNTIL-STATUS</code>
and enough attempts, the run lines up with the 3rd successful response.</p>

<h2 id="retrying-until-the-body-matches-what-you-expect">Retrying until the body matches what you expect</h2>

<p>In production you often see this: the API returns <strong><code class="highlighter-rouge">200</code></strong> right away because the
request itself was accepted, but the <strong>real outcome</strong> is produced later - a
message on a queue, a background job, a workflow engine, whatever runs
<strong>asynchronously</strong>. Until that work finishes, the resource still looks “in
progress”. Your test needs to prove that the job <strong>eventually</strong> reached the
expected state - not that the first HTTP response was OK.</p>

<p>DummyApi does the same with orders: <code class="highlighter-rouge">status</code> advances in the background
(<code class="highlighter-rouge">pending</code> → <code class="highlighter-rouge">processing</code> → <code class="highlighter-rouge">completed</code>). Your <code class="highlighter-rouge">GET /orders/{id}</code> can keep
returning <strong><code class="highlighter-rouge">200</code></strong> while the JSON still says <code class="highlighter-rouge">pending</code>, so
<code class="highlighter-rouge">## RETRY-UNTIL-STATUS: [200]</code> tells you nothing new - you
already had <code class="highlighter-rouge">200</code> on the first try.</p>

<p>What you need is <strong>retry until a post-response test passes</strong>. Put the real
condition in <code class="highlighter-rouge">.csx</code> (here: <code class="highlighter-rouge">status</code> is <code class="highlighter-rouge">completed</code>), then point the directive at
that test’s <strong>exact name</strong>:</p>

<p><code class="highlighter-rouge">Tests/002-Orders/003-Check-Order-Status-req.http</code>:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># @name CheckOrderStatusRequest
## RETRY-UNTIL-TEST-PASS: Order should eventually reach 'completed' status.
## RETRY-MAX-ATTEMPTS: 15
## RETRY-BACKOFF-TYPE: Linear
GET {{ApiBaseUrl}}/orders/{{NewOrderId}}
X-Api-Key: {{ApiKey}}
</span></code></pre></div></div>

<p><code class="highlighter-rouge">003-Check-Order-Status-test.csx</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="nf">Test</span><span class="p">(</span><span class="s">"Order should eventually reach 'completed' status."</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">dynamic</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">GetBodyAsExpandoAsync</span><span class="p">();</span>
    <span class="nf">Equal</span><span class="p">(</span><span class="s">"completed"</span><span class="p">,</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">status</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The string in <code class="highlighter-rouge">RETRY-UNTIL-TEST-PASS</code> must match the first argument of
<code class="highlighter-rouge">tp.Test</code> character for character.</p>

<h2 id="where-you-are-now">Where you are now</h2>

<p>You have:</p>

<ul>
  <li><strong>Environments</strong> via <code class="highlighter-rouge">.teapie/env.json</code> and <code class="highlighter-rouge">teapie test … -e &lt;name&gt;</code></li>
  <li><strong>Pre-request scripts</strong> that set variables consumed from <code class="highlighter-rouge">.http</code> files</li>
  <li><strong>Built-in <code class="highlighter-rouge">$…</code> functions</strong> for inline dynamic values</li>
  <li><strong>Retries</strong> for flaky status codes and for
async state using <code class="highlighter-rouge">RETRY-UNTIL-TEST-PASS</code>, with the
<a href="https://www.teapie.fun/docs/retrying.html">retrying documentation</a> for deeper
configuration</li>
</ul>

<p>The <a href="/">next article</a> goes deeper: custom test directives, custom auth
providers, named retry strategies, reporting, and TeaPie’s AI-assisted workflows.</p>

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://www.teapie.fun/docs/introduction.html">TeaPie documentation</a></li>
  <li><a href="https://www.teapie.fun/docs/environments.html">Environments</a></li>
  <li><a href="https://www.teapie.fun/docs/retrying.html">Retrying</a></li>
  <li><a href="https://www.teapie.fun/docs/functions.html">Functions</a></li>
  <li><a href="https://github.com/Burgyn/MMLib.DummyApi">MMLib.DummyApi on GitHub</a></li>
  <li><a href="/2026/03/12/teapie-getting-started">Part 1 of this series</a></li>
</ul>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="dotnet" /><category term="testing" /><category term="api" /><category term="tools" /><summary type="html"><![CDATA[Environments, API keys on DummyApi, pre-request variables, built-in .http functions, and resilient retries - with links to TeaPie docs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/teapie-real-world-api-testing-cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/teapie-real-world-api-testing-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">TeaPie: Test your REST API in 5 minutes</title><link href="https://blog.burgyn.online/2026/03/12/teapie-getting-started" rel="alternate" type="text/html" title="TeaPie: Test your REST API in 5 minutes" /><published>2026-03-12T17:00:00+00:00</published><updated>2026-03-12T17:00:00+00:00</updated><id>https://blog.burgyn.online/2026/03/12/teapie-getting-started</id><content type="html" xml:base="https://blog.burgyn.online/2026/03/12/teapie-getting-started"><![CDATA[<p>You have an API. You want to test it. You don’t want to set up a full test project, install a heavy framework, or write a test class just to send a <code class="highlighter-rouge">GET</code> request and check the status code.</p>

<p>That’s exactly the use case <a href="https://www.teapie.fun">TeaPie</a> was built for. It’s a CLI tool for API testing that uses plain <code class="highlighter-rouge">.http</code> files — the same format you might already know from VS Code’s REST Client extension or Visual Studio’s HTTP Files support. You write the request, optionally add a C# script for assertions, and run it.</p>

<p>Let me show you how to go from zero to running tests in a few minutes.</p>

<h2 id="spin-up-a-demo-api">Spin up a demo API</h2>

<p>I’ll use my own open-source project <a href="https://github.com/Burgyn/MMLib.DummyApi">MMLib.DummyApi</a> as the demo backend. It’s a configurable mock REST API that ships with three ready-to-use collections: <code class="highlighter-rouge">products</code>, <code class="highlighter-rouge">orders</code>, and <code class="highlighter-rouge">customers</code>. You can <a href="/2026/03/04/mmlib-dummyapi">read more about it here</a>.</p>

<p>Start it with Docker:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull ghcr.io/burgyn/mmlib-dummyapi
docker run <span class="nt">-p</span> 8080:8080 ghcr.io/burgyn/mmlib-dummyapi
</code></pre></div></div>

<p>That’s it. The API is running at <code class="highlighter-rouge">http://localhost:8080</code> with 50 seeded products, 20 orders, and 30 customers. Try it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8080/products
</code></pre></div></div>

<h2 id="install-teapie">Install TeaPie</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet tool <span class="nb">install</span> <span class="nt">-g</span> TeaPie.Tool
</code></pre></div></div>

<p>Verify it works:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie <span class="nt">--version</span>
</code></pre></div></div>

<h2 id="create-the-test-project">Create the test project</h2>

<p>Navigate to the folder where you want to keep your tests and run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie init
</code></pre></div></div>

<p>This creates a <code class="highlighter-rouge">.teapie</code> folder with a default configuration. Now scaffold your first test case:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie generate <span class="s2">"001-List-Products"</span> Tests/001-Products
</code></pre></div></div>

<p>This generates a <code class="highlighter-rouge">001-List-Products-req.http</code> file. Open the <code class="highlighter-rouge">.http</code> file — it’s where the request lives.</p>

<h2 id="step-1-your-first-test--no-c-needed">Step 1: Your first test — no C# needed</h2>

<p>Open <code class="highlighter-rouge">Tests/001-Products/001-List-Products-req.http</code> and write:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
GET http://localhost:8080/products
</span></code></pre></div></div>

<p>That’s the whole test. The <code class="highlighter-rouge">## TEST-*</code> lines are <strong>directives</strong> — TeaPie processes them and schedules the assertions automatically. No <code class="highlighter-rouge">.csx</code> script, no C# code, no test class.</p>

<p>Run it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie <span class="nb">test </span>Tests/001-Products/001-List-Products-req.http
</code></pre></div></div>

<p>or</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie <span class="nb">test </span>Tests
</code></pre></div></div>

<p>You’ll see the result in the console immediately. Two tests pass: status is 200, response has a body.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  _____                 ___   _           _       ___       ___
 |_   _|  ___   __ _   | _ \ (_)  ___    / |     | __|     |_  )
   | |   / -_) / _` |  |  _/ | | / -_)   | |  _  |__ \  _   / /
   |_|   \___| \__,_|  |_|   |_| \___|   |_| (_) |___/ (_) /___|

[21:03:22 INF] Exploration of the collection started at path: '/Users/martiniak/Developer/GitHub/Burgyn/MMLib.TeapieExample/Tests'.
[21:03:22 INF] Collection explored in 4 ms, found 1 test cases.
[21:03:22 WRN] No environment file found. Running without environment.
[21:03:22 INF] Test case '001-List-Products' is going to be executed. (1/1)
[21:03:22 INF] Test Passed: '[1] Status code should match one of these: [200]' in 5 ms
[21:03:22 INF] Test Passed: '[2] Response should have body.' in 1 ms
[21:03:22 INF] Execution of test case '001-List-Products' has finished. (1/1)
╭────────────────────────────────────────────────────────────────────────────────╮
│ Test Results: SUCCESS                                                          │
├────────────────────────────────────────────────────────────────────────────────┤
│                                                                                │
│ ┌─ Summary ──────────────────────────────────────────────────────────────────┐ │
│ │                                                                            │ │
│ │  ████████████████████████████████████████████████████████████████████████  │ │
│ │                                                                            │ │
│ │  ■ Passed Tests [100.00%]: 2          ■ Skipped Tests [0.00%]: 0           │ │
│ │  ■ Failed Tests [0.00%]: 0                                                 │ │
│ └────────────────────────────────────────────────────────────────────────────┘ │
│                                                                                │
╰────────────────────────────────────────────────────────────────────────────────╯
</code></pre></div></div>

<blockquote>
  <p>The directive approach is great for quick sanity checks. You don’t need to write any C# until you actually need something more specific.</p>
</blockquote>

<h2 id="step-2-add-a-csx-script-for-meaningful-assertions">Step 2: Add a .csx script for meaningful assertions</h2>

<p>Directives cover the basics, but they can’t express things like “the response body is an array with at least one item” or “the newly created resource has a positive ID”. For that, you add a post-response script.</p>

<p>Scaffold the create test:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie generate <span class="s2">"002-Create-Product"</span> Tests/001-Products <span class="nt">-t</span>
</code></pre></div></div>

<p>The <code class="highlighter-rouge">-t</code> or <code class="highlighter-rouge">--test</code> create test csx file. Two files are created:</p>

<ul>
  <li><code class="highlighter-rouge">002-Create-Product-req.http</code> — the request</li>
  <li><code class="highlighter-rouge">002-Create-Product-test.csx</code> — the post-response assertions</li>
</ul>

<h3 id="the-request-file">The request file</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># @name CreateProductRequest
## TEST-EXPECT-STATUS: [201]
POST http://localhost:8080/products
Content-Type: application/json

{
  "name": "TeaPie Mug",
  "price": 12.99,
  "category": "merchandise",
  "sku": "MUG-001"
}
</span></code></pre></div></div>

<h3 id="the-post-response-script">The post-response script</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="nf">Test</span><span class="p">(</span><span class="s">"Created product should have a positive ID."</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="c1">// 👇 GetBodyAsExpandoAsync gives you case-insensitive dynamic access to the JSON body</span>
    <span class="kt">dynamic</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">GetBodyAsExpandoAsync</span><span class="p">();</span>
    <span class="nf">NotNull</span><span class="p">(</span><span class="n">body</span><span class="p">.</span><span class="n">id</span><span class="p">);</span>
    <span class="n">tp</span><span class="p">.</span><span class="nf">SetVariable</span><span class="p">(</span><span class="s">"NewProductId"</span><span class="p">,</span> <span class="n">body</span><span class="p">.</span><span class="n">id</span><span class="p">);</span>
<span class="p">});</span>

<span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="nf">Test</span><span class="p">(</span><span class="s">"Created product should have the correct name."</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">dynamic</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">GetBodyAsExpandoAsync</span><span class="p">();</span>
    <span class="nf">Equal</span><span class="p">(</span><span class="s">"TeaPie Mug"</span><span class="p">,</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">name</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p><code class="highlighter-rouge">tp.Test()</code> is the test runner. Assertions use XUnit’s <code class="highlighter-rouge">Assert</code> (the <code class="highlighter-rouge">Assert.</code> prefix is optional). The variable <code class="highlighter-rouge">NewProductId</code> is stored at collection level — other test cases in the same run can read it.</p>

<blockquote>
  <p>You can use your favorite assertion framework.</p>
</blockquote>

<h3 id="chain-to-get--using-request-variables">Chain to Get — using request variables</h3>

<p>Now scaffold the get test:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie generate <span class="s2">"003-Get-Product"</span> Tests/001-Products <span class="nt">-t</span>
</code></pre></div></div>

<p>The request file uses a <strong>request variable</strong> to read the ID from the create response:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err"># @name GetProductRequest
## TEST-EXPECT-STATUS: [200]
## TEST-HAS-BODY
GET http://localhost:8080/products/{{NewProductId}
</span></code></pre></div></div>

<p>The post-response script verifies the right product came back:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="nf">Test</span><span class="p">(</span><span class="s">"Retrieved product name should match."</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">dynamic</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">tp</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">GetBodyAsExpandoAsync</span><span class="p">();</span>
    <span class="nf">Equal</span><span class="p">(</span><span class="s">"TeaPie Mug"</span><span class="p">,</span> <span class="p">(</span><span class="kt">string</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">name</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="run-the-full-collection">Run the full collection</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>teapie <span class="nb">test </span>Tests/001-Products
</code></pre></div></div>

<p>TeaPie runs all test cases in order, resolves variables across them, and prints a summary:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>╭────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Test Results: SUCCESS                                                                          │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                │
│ ┌─ Summary ──────────────────────────────────────────────────────────────────────────────────┐ │
│ │                                                                                            │ │
│ │  ████████████████████████████████████████████████████████████████████████████████████████  │ │
│ │                                                                                            │ │
│ │  ■ Passed Tests [100.00%]: 8    ■ Skipped Tests [0.00%]: 0    ■ Failed Tests [0.00%]: 0    │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│                                                                                                │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
</code></pre></div></div>

<p>Exit code <code class="highlighter-rouge">0</code> means all tests passed — which makes it CI-friendly out of the box.</p>

<h2 id="where-you-are-now">Where you are now</h2>

<p>In a few minutes you have:</p>

<ul>
  <li>A <code class="highlighter-rouge">GET</code> test that uses only directives — no C# at all.</li>
  <li>A <code class="highlighter-rouge">POST</code> test with a post-response script that validates the response body and stores a variable.</li>
  <li>A <code class="highlighter-rouge">GET</code> test that chains from the create response using request variables.</li>
</ul>

<p>The pattern scales. Start with directives for quick checks, add <code class="highlighter-rouge">.csx</code> scripts
when you need real assertions.</p>

<p><strong>Part 2:</strong>
<a href="/2026/03/18/teapie-real-world-api-testing">TeaPie: Testing real-world API scenarios</a>
covers environments, API keys on DummyApi, pre-request variables,
built-in <code class="highlighter-rouge">.http</code> functions, and retries (flaky responses vs async background work).</p>

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://www.teapie.fun/docs/introduction.html">TeaPie documentation</a></li>
  <li><a href="/2026/03/18/teapie-real-world-api-testing">Part 2 of this series - real-world API scenarios</a></li>
  <li><a href="https://github.com/Burgyn/MMLib.DummyApi">MMLib.DummyApi on GitHub</a></li>
  <li><a href="/2026/03/04/mmlib-dummyapi">MMLib.DummyApi blog post</a></li>
</ul>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="dotnet" /><category term="testing" /><category term="api" /><category term="tools" /><summary type="html"><![CDATA[TeaPie is a CLI tool for API testing using .http files. No heavy frameworks — install it, write a request, run it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/teapie-getting-started-cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/teapie-getting-started-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">MMLib.DummyApi - Configurable mock API for prototypes and testing</title><link href="https://blog.burgyn.online/2026/03/04/mmlib-dummyapi" rel="alternate" type="text/html" title="MMLib.DummyApi - Configurable mock API for prototypes and testing" /><published>2026-03-04T17:00:00+00:00</published><updated>2026-03-04T17:00:00+00:00</updated><id>https://blog.burgyn.online/2026/03/04/mmlib-dummyapi</id><content type="html" xml:base="https://blog.burgyn.online/2026/03/04/mmlib-dummyapi"><![CDATA[<p>This is probably one of those projects that helps mostly me. Still, I wanted to share it in case you run into the same problems.</p>

<p>I often need a working API for quick UI prototypes, UI framework testing, integration tool testing, load-testing tools, and demo projects for talks <em>(conference, school, meetup)</em>. I do not want to build a new backend every single time just to have realistic endpoints and data.</p>

<p>So I built <a href="https://github.com/Burgyn/MMLib.DummyApi"><code class="highlighter-rouge">MMLib.DummyApi</code></a>: a configurable mock REST API that starts fast and gives me fully functional CRUD endpoints, generated seed data, validation, and edge-case simulation.</p>

<h2 id="why-this-project-exists">Why this project exists</h2>

<p>The core idea is simple:</p>

<ul>
  <li>I have different frontends and tools to test.</li>
  <li>I need realistic data and behavior.</li>
  <li>I need to control “bad” scenarios too.</li>
</ul>

<p>Instead of creating another one-off API <em>(or asking an agent to generate one again)</em>, I provide configuration and the API is ready.</p>

<p>Out of the box, it is not read-only mock data. The API works with:</p>

<ul>
  <li><code class="highlighter-rouge">GET /{collection}</code></li>
  <li><code class="highlighter-rouge">GET /{collection}/{id}</code></li>
  <li><code class="highlighter-rouge">POST /{collection}</code></li>
  <li><code class="highlighter-rouge">PUT /{collection}/{id}</code></li>
  <li><code class="highlighter-rouge">DELETE /{collection}/{id}</code></li>
</ul>

<p>POST/PUT payloads are validated by your JSON schema, and collections can be seeded with dummy data on startup.</p>

<h2 id="quick-start">Quick start</h2>

<p>Run it with Docker:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker pull ghcr.io/burgyn/mmlib-dummyapi
docker run <span class="nt">-p</span> 8080:8080 ghcr.io/burgyn/mmlib-dummyapi
</code></pre></div></div>

<p>Try a default collection:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8080/products
</code></pre></div></div>

<p>API docs are available at <code class="highlighter-rouge">http://localhost:8080/scalar/v1</code>.</p>

<h2 id="create-your-own-records-collection">Create your own records collection</h2>

<p>In my case, I often need some custom “evidence” or “records” collection. Here is a minimal example:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"collections"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"records"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Records"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"seedCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
      </span><span class="nl">"schema"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"object"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"required"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"title"</span><span class="p">,</span><span class="w"> </span><span class="s2">"status"</span><span class="p">],</span><span class="w">
        </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w"> </span><span class="nl">"minLength"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="p">},</span><span class="w">
          </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w"> </span><span class="nl">"enum"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"new"</span><span class="p">,</span><span class="w"> </span><span class="s2">"processing"</span><span class="p">,</span><span class="w"> </span><span class="s2">"done"</span><span class="p">]</span><span class="w"> </span><span class="p">},</span><span class="w">
          </span><span class="nl">"ownerEmail"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w"> </span><span class="nl">"format"</span><span class="p">:</span><span class="w"> </span><span class="s2">"email"</span><span class="w"> </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Save it as <code class="highlighter-rouge">my-collections.json</code> and start the container with config mount:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-p</span> 8080:8080 <span class="se">\</span>
  <span class="nt">-v</span> ./my-collections.json:/config/collections.json <span class="se">\</span>
  <span class="nt">-e</span> <span class="nv">DUMMYAPI__COLLECTIONSFILE</span><span class="o">=</span>/config/collections.json <span class="se">\</span>
  ghcr.io/burgyn/mmlib-dummyapi
</code></pre></div></div>

<p>Now verify round-trip CRUD quickly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:8080/records <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"title":"Load test scenario","status":"new","ownerEmail":"john@example.com"}'</span>

curl http://localhost:8080/records
</code></pre></div></div>

<h2 id="simulate-edge-cases-without-extra-coding">Simulate edge cases without extra coding</h2>

<p>This is the part I use a lot in demos and testing:</p>

<ul>
  <li>Delays: <code class="highlighter-rouge">X-Simulate-Delay: 500</code></li>
  <li>Forced error: <code class="highlighter-rouge">X-Simulate-Error: true</code></li>
  <li>Retry scenario: <code class="highlighter-rouge">X-Simulate-Retry: 3</code> with <code class="highlighter-rouge">X-Request-Id</code></li>
  <li>Random chaos latency/failures with chaos headers</li>
  <li>Background updates with <code class="highlighter-rouge">backgroundJob</code> <em>(for example status transitions)</em></li>
</ul>

<p>A tiny retry simulation call can look like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:8080/records <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Simulate-Retry: 3"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"X-Request-Id: demo-retry-1"</span>
</code></pre></div></div>

<p>This lets me test how UI and integration clients behave when things are not perfect, without implementing custom fault endpoints.</p>

<h2 id="wrap-up">Wrap-up</h2>

<p>I built this as a practical utility for my own daily workflows, but maybe it helps you too if you prototype often or need controllable API behavior for tests and demos.</p>

<p>I kept this article intentionally simple. Full details, all configuration options, and more advanced scenarios are in the project README.</p>

<h2 id="links">Links</h2>

<ul>
  <li>GitHub repository: <a href="https://github.com/Burgyn/MMLib.DummyApi">https://github.com/Burgyn/MMLib.DummyApi</a></li>
</ul>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="dotnet" /><category term="api" /><category term="testing" /><category term="tools" /><summary type="html"><![CDATA[I built a configurable mock API with CRUD and edge-case simulation for UI prototypes, demos, and testing tools.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/mmlib-dummyapi-cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/mmlib-dummyapi-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Built-in validation for Minimal APIs in .NET 10</title><link href="https://blog.burgyn.online/2026/02/11/minimal-api-validation" rel="alternate" type="text/html" title="Built-in validation for Minimal APIs in .NET 10" /><published>2026-02-11T17:00:00+00:00</published><updated>2026-02-11T17:00:00+00:00</updated><id>https://blog.burgyn.online/2026/02/11/minimal-api-validation</id><content type="html" xml:base="https://blog.burgyn.online/2026/02/11/minimal-api-validation"><![CDATA[<p>Minimal APIs have been around since .NET 6 and have evolved a lot, but one thing was missing: first-class validation support. With .NET 10 <em>(since November 2025)</em>, that gap is finally closed thanks to revived <code class="highlighter-rouge">System.ComponentModel.DataAnnotations</code> support for Minimal APIs.</p>

<h2 id="how-it-works">How it works</h2>

<p>You need three things:</p>

<ol>
  <li><strong>Register validation in DI</strong> - call <code class="highlighter-rouge">builder.Services.AddValidation();</code> in <code class="highlighter-rouge">Program.cs</code>.</li>
  <li><strong>Decorate your request types</strong> with DataAnnotation attributes: <code class="highlighter-rouge">[Required]</code>, <code class="highlighter-rouge">[MinLength]</code>, <code class="highlighter-rouge">[EmailAddress]</code>, <code class="highlighter-rouge">[Range]</code>, <code class="highlighter-rouge">[Compare]</code>, and so on.</li>
  <li><strong>Do nothing else</strong> - a source generator emits the validation logic; your endpoint signature stays the same.</li>
</ol>

<p>If validation fails, the framework returns 400 Bad Request with a <code class="highlighter-rouge">ProblemDetails</code> - style payload before your handler runs.</p>

<h2 id="where-you-can-use-it">Where you can use it</h2>

<p>This validation works for:</p>

<ul>
  <li><strong>Request body</strong> - e.g. JSON bound to a record or class.</li>
  <li><strong>Query parameters</strong> - e.g. <code class="highlighter-rouge">[FromQuery]</code> with attributes.</li>
  <li><strong>Headers</strong> - when bound from headers with the right attributes.</li>
</ul>

<p>So you get the same declarative style you know from MVC, but without the controller layer.</p>

<h2 id="important-the-request-type-must-be-public">Important: the request type must be public</h2>

<p>If your request class or record is not <strong>public</strong>, validation will not run. The generated code only sees public types. Keep that in mind when you define DTOs in the same file as your endpoints or in a separate assembly.</p>

<h2 id="example">Example</h2>

<p>Let’s put it together. First, add the validation services and define a minimal endpoint:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.ComponentModel.DataAnnotations</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="c1">// 👇 Add validation support</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddValidation</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// 👇 Nothing changed here in your endpoint</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/users"</span><span class="p">,</span> <span class="p">(</span><span class="n">CreateUserRequest</span> <span class="n">request</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="k">new</span> <span class="p">{</span> <span class="n">id</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Email</span> <span class="p">});</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Your handler still receives a <code class="highlighter-rouge">CreateUserRequest</code>; the framework validates it before the lambda runs. The request type is a <strong>public</strong> record with DataAnnotations:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 👇 Your model class with validation attributes</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">CreateUserRequest</span><span class="p">(</span>
    <span class="p">[</span><span class="n">Required</span><span class="p">,</span> <span class="nf">MinLength</span><span class="p">(</span><span class="m">2</span><span class="p">),</span> <span class="nf">MaxLength</span><span class="p">(</span><span class="m">100</span><span class="p">)]</span>
    <span class="kt">string</span> <span class="n">Name</span><span class="p">,</span>
    <span class="p">[</span><span class="n">Required</span><span class="p">,</span> <span class="n">EmailAddress</span><span class="p">]</span>
    <span class="kt">string</span> <span class="n">Email</span><span class="p">,</span>
    <span class="p">[</span><span class="n">Required</span><span class="p">,</span> <span class="nf">MinLength</span><span class="p">(</span><span class="m">8</span><span class="p">),</span> <span class="nf">MaxLength</span><span class="p">(</span><span class="m">100</span><span class="p">)]</span>
    <span class="kt">string</span> <span class="n">Password</span><span class="p">,</span>
    <span class="p">[</span><span class="n">property</span><span class="p">:</span> <span class="n">Required</span><span class="p">,</span> <span class="nf">Compare</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">CreateUserRequest</span><span class="p">.</span><span class="n">Password</span><span class="p">))]</span>
    <span class="kt">string</span> <span class="n">ConfirmPassword</span><span class="p">,</span>
    <span class="p">[</span><span class="nf">Range</span><span class="p">(</span><span class="m">18</span><span class="p">,</span> <span class="m">120</span><span class="p">)]</span> <span class="kt">int</span> <span class="n">Age</span><span class="p">);</span>
</code></pre></div></div>

<p>For records, note the <code class="highlighter-rouge">property:</code> prefix on <code class="highlighter-rouge">[Compare]</code> so the attribute is applied to the generated property. Once that’s in place, invalid requests (e.g. short name, bad email, mismatched passwords, or age out of range) get a 400 response automatically.</p>

<h2 id="when-you-need-more-than-dataannotations">When you need more than DataAnnotations</h2>

<p>DataAnnotations are great for straightforward rules. If you need more complex, FluentValidation-style logic <em>(cross-field rules, async validators, or heavy reuse)</em>, you can still use custom endpoint filters - I wrote about that approach in an earlier post <em>(in Slovak)</em>: <a href="/2023/01/16/asp-net-core-minimal-api-filters">ASP.NET Core Minimal API – Filters &amp; Validation</a>. The built-in validation in .NET 10 simply gives you a zero-friction option when attributes are enough.</p>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="C#" /><category term=".NET" /><category term="ASP.NET CORE" /><category term="Minimal API" /><summary type="html"><![CDATA[Built-in validation for Minimal APIs in .NET 10: AddValidation(), DataAnnotations, and what to watch for.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/blog-post-base-cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/blog-post-base-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">My Reading and Study Workflow for Tech Articles</title><link href="https://blog.burgyn.online/2026/02/05/my-reading-and-study-workflow-for-tech-articles" rel="alternate" type="text/html" title="My Reading and Study Workflow for Tech Articles" /><published>2026-02-05T17:00:00+00:00</published><updated>2026-02-05T17:00:00+00:00</updated><id>https://blog.burgyn.online/2026/02/05/my-reading-and-study-workflow-for-tech-articles</id><content type="html" xml:base="https://blog.burgyn.online/2026/02/05/my-reading-and-study-workflow-for-tech-articles"><![CDATA[<p>I thought I’d share my flow for collecting and reading articles. Maybe you’ll find it useful, or maybe you’ll share yours in the comments.</p>

<p>🔍 Where I find articles
I usually discover interesting content in 4 places:</p>

<p>📬 <a href="https://vlko.substack.com/">Marián Vlčák aka vlko’s monthly newsletter</a>
Consistently high-quality tech content.</p>

<p>📰 Medium newsletter - Though lately it’s been flooded with “5 things every .NET developer must know” type articles. You have to dig deeper to find the gems.</p>

<p>💼 LinkedIn - Still a solid source for technical articles. After Twitter’s decline, it became even more valuable.</p>

<p>☁️ BlueSky - For me, the best Twitter replacement. I’ve created a custom feed built as a filter over my list of specific people I want to follow. BlueSky’s killer feature is the ability to create your own feeds and have full control over what you see and how much.
<a href="https://bsky.app/profile/did:plc:gdx2f7prj422nixph6laigs7/lists/3mc5jltbaks2n">My Developers list.</a></p>

<p>🎙️ Podcasts - Another source where I often discover interesting topics to explore further.</p>

<p>📚 My reading workflow
When I find an interesting article, I don’t read it immediately. Instead, I bookmark it using raindrop.io - my go-to bookmarking system for everything.
Articles I want to read go into a “Blogs” folder, which I’ve set up as a public RSS feed. This RSS feed is connected to Instapaper - a reader that creates a clean copy of the article, optimizes it for mobile reading, and most importantly: preserves scroll position. Whenever I return to an article, I’m exactly where I left off.</p>

<p>🤔 Why this seemingly complex setup?
I separate two activities:
 ⚡ Discovery - scrolling feeds and finding new content
 📖 Deep reading - actually consuming the content</p>

<p>My BlueSky feed is limited to max 20 posts per day. I quickly scan what interests me and save it for later. When I’m in the mood for reading, I open Instapaper. Raindrop keeps all my bookmarks organized in one place.
What’s your workflow for managing technical articles? Do you read immediately or save for later?</p>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="productivity" /><category term="developerworkflow" /><category term="continuouslearning" /><summary type="html"><![CDATA[I thought I’d share my flow for collecting and reading articles. Maybe you’ll find it useful, or maybe you’ll share yours in the comments.]]></summary></entry><entry><title type="html">ASP.NET CORE Junior Developer Roadmap</title><link href="https://blog.burgyn.online/2024/10/29/asp-net-core-junior-developer-roadmap" rel="alternate" type="text/html" title="ASP.NET CORE Junior Developer Roadmap" /><published>2024-10-29T17:00:00+00:00</published><updated>2024-10-29T17:00:00+00:00</updated><id>https://blog.burgyn.online/2024/10/29/asp-net-core-junior-developer-roadmap</id><content type="html" xml:base="https://blog.burgyn.online/2024/10/29/asp-net-core-junior-developer-roadmap"><![CDATA[<h2 id="úvod">Úvod</h2>

<p>Občas sa ma ľudia pýtajú: „Učím sa programovať a chcel by som sa stať backend vývojárom. Ako začať?” alebo „Som junior/medior vývojár a chcem sa posunúť ďalej. Na čo sa mám zamerať?” Ak ide o niekoho z môjho okolia, vieme si sadnúť a prebrať to. Postupne sa dopracujeme k vhodnej ceste. Keď sa ma však opýta niekto na LinkedIn, odpoveď býva: „Je to špecifické, záleží na tom, čo presne chceš alebo potrebuješ…” A tam to väčšinou končí. Prípadne mu ešte odporúčim veľmi dobre spracovaný zoznam oblastí a zdrojov pre vývoj v prostredí .NET od <a href="https://medium.com/@techworldwithmilan/net-developer-roadmap-2023-c1a9a102748e">Milana Milanoviča (čistá zhoda mien 😊)</a>.</p>

<p>Preto som sa rozhodol napísať túto mini sériu článkov, kde popíšem, ako môže vyzerať cesta od junior ASP.NET CORE backend vývojára po seniora. Nejde o návod na programovanie, ale skôr o opis cesty, ktorou sa vývojár môže vydať. Vysvetlím, v ktorej fáze je dobré ovládať určité zručnosti a aké vedomosti by mal mať.</p>

<blockquote>
  <p>💭 Toto nie je žiadna rigorózna práca. Každý človek je iný, každá firma má odlišné očakávania a potreby. Môžeš to brať ako jednu z možností, ktorou sa môžeš inšpirovať.</p>
</blockquote>

<h2 id="mojich-5-odporúčaní-pre-budúcich-vývojárov">Mojich 5 odporúčaní pre budúcich vývojárov</h2>

<p>Tu je mojich 5 všeobecných odporúčaní, ktoré podľa mňa môžu pomôcť na ceste k tomu, aby sa z teba stal lepší vývojár.</p>

<h3 id="1️⃣-práca-ťa-musí-baviť">1️⃣ Práca ťa musí baviť</h3>

<p>Nezáleží na tom, či práve začínaš a vymýšľaš si projekt, na ktorom sa budeš učiť, či máš nápad, ktorý chceš zrealizovať, alebo hľadáš novú prácu. Vyberaj si tak, aby ťa to bavilo, aby si v tom videl zmysel a aby si videl výsledky. Tieto tri faktory (niekedy potrebuješ všetky tri, inokedy stačí jeden z nich) ťa môžu dostatočne motivovať, aby si sa zlepšoval, rástol a zotrval.</p>

<h3 id="2️⃣-chuť-a-vytrvalosť">2️⃣ Chuť a vytrvalosť</h3>

<p>Z vlastnej skúsenosti so začínajúcimi vývojármi viem, že chuť učiť sa, tvoriť a vytrvalosť sú dôležitejšie než momentálne vedomosti a schopnosti.</p>

<h3 id="3️⃣-zvedavosť">3️⃣ Zvedavosť</h3>

<p>Zvedavosť a záujem o to, ako veci fungujú, vedomé vnímanie toho, s čím pracuješ, môžu výrazne prispieť k tvorbe lepších riešení. Keď si všímam aplikácie, ktoré používam (čo dokážu, čo na nich oceňujem a čo mi nevyhovuje), môžem zákazníkom priniesť lepší produkt. Ak si uvedomujem, ako fungujú knižnice a frameworky, ktoré využívam, môžem navrhnúť efektívnejší kód.</p>

<h3 id="4️⃣-lenivosť">4️⃣ Lenivosť</h3>

<p>Tento bod možno niekoho prekvapí a na prvý pohľad si môže odporovať s predchádzajúcimi. Vysvetlím. Ak mi po tretíkrát príde rovnaká požiadavka, mal by sa prejaviť môj zmysel pre „lenivosť” a mal by som si danú úlohu zautomatizovať. Ak niekto opakovane potrebuje rovnaký report, vytvorím na to skript. Ak často zakladám nový projekt, pripravím si scaffold. Lenivý vývojár = efektívny vývojár 😊. Lenivosť je často motorom inovácií.</p>

<h3 id="5️⃣-vystúp-zo-svojej-komfortnej-zóny">5️⃣ Vystúp zo svojej komfortnej zóny</h3>

<p>Opustenie komfortnej zóny nás dokáže posunúť výrazne dopredu. Komunikuj s kolegami, nauč sa používať netechnický jazyk, vysvetľuj ľuďom, čo robíš, ich vlastnými slovami. Neboj sa komunikovať so zákazníkmi ani zdieľať svoje vedomosti v komunite. Uč sa soft skills. Nemusíš všetko zvládnuť hneď, ale zamysli sa, kde chceš byť v budúcnosti a vyber si to, čo ťa tam posunie. Práve toto sú veci, ktoré sú v súčasnosti tie rozdielové.</p>

<h2 id="junior--medior--senior">Junior / Medior / Senior</h2>

<p>V IT firmách sa zaužívalo rozdelenie na junior, medior a senior vývojárov z pohľadu kariérneho rastu alebo zručností. Neexistuje jednotná definícia toho, kto je junior, medior a senior, pretože každá firma to vníma po svojom. Všeobecne však platí, že tieto úrovne kombinujú odpracované roky, technické schopnosti, samostatnosť, zodpovednosť a soft skills.</p>

<h2 id="junior-vývojár">Junior vývojár</h2>

<p>Poďme sa pozrieť na jednu z možných ciest, ako sa môže junior programátor postupne dostať do vývoja ASP.NET CORE REST API.</p>

<h3 id="prvý-projekt">Prvý projekt</h3>

<p>🚩 Existuje mnoho roadmap a odporúčaní, čo všetko by mal takýto vývojár vedieť. Zvyčajne začínajú tým, že by mal ovládať základy C# a .NET. Ja by som sa však pridržiaval môjho prvého odporúčania: práca ťa musí baviť a musíš vidieť výsledky. Študovať C# je dôležité, ale výsledky prídu až časom.</p>

<p>Preto by som rovno začal s ASP.NET CORE Minimal API a vyskúšal si prvé endpointy.</p>

<p>Takto veľmi rýchlo uvidíš výsledok svojej práce. Máš prvé endpointy, ktoré si môžeš otestovať a postupne si začneš tvoriť obraz o tom, čo daná technológia umožňuje a na čo je dobrá.</p>

<p>Navrhni si svoj prvý jednoduchý projekt a postupne skúšaj, čo do neho dokážeš zapracovať (napr. zoznam kontaktov, nákupný zoznam, receptár atď.).</p>

<p>V tejto fáze si to zjednoduš a dáta nikam neukladaj, maximálne ich drž v pamäti. Na prácu s databázou príde čas.</p>

<p>🚩 Keď už máš prvé endpointy za sebou, vráť sa k C# a preštuduj si základy. Premenné, základné dátové typy, funkcie, metódy a jednoduché triedy ti umožnia postupne riešiť komplexnejšie úlohy.</p>

<p>🚩 Prvé requesty si pravdepodobne vykonal priamo z prehliadača pomocou API dokumentácie. Aby si mohol plnohodnotne pracovať s API, budeš si musieť osvojiť prácu s niektorým z HTTP klientov. Najpoužívanejší je Postman, ale môžeš sa stretnúť aj s Insomnia či inými nástrojmi.</p>

<p>🚩 Dáta sú základom väčšiny projektov. Potrebuješ ich ukladať, manipulovať s nimi a poskytovať klientom. Možnosti na ukladanie dát sú rôzne, ale v prípade štruktúrovaných dát sa väčšinou jedná o SQL alebo NoSQL databázy. V nových projektoch je podiel SQL a NoSQL databáz pomerne vyrovnaný, v starších projektoch však častejšie narazíš na SQL databázy. Pre začiatok odporúčam SQL, keďže pravdepodobnosť, že s ním budeš pracovať, je vysoká.</p>

<p>Na úvod to nemusí byť nič zložité. Skús začať s Entity Frameworkom, pripojiť sa na existujúcu databázu, vytvoriť prvú migráciu a naučiť sa, ako načítať dáta.</p>

<p>🚩 Aby si sa mohol ďalej posúvať, potrebuješ ísť hlbšie do ASP.NET Core. Postupne si rozšír svoje zadanie. Pridaj nové sady endpointov, aby si pochopil, ako funguje routing. Preštuduj si model binding, aby si vedel, akými spôsobmi môžeš posielať dáta na backend (cez <code class="highlighter-rouge">path</code>, <code class="highlighter-rouge">query params</code>, <code class="highlighter-rouge">headers</code>, <code class="highlighter-rouge">body</code>).</p>

<p>Dáta, ktoré ti prichádzajú, potrebuješ validovať, aby si mal istotu, že dostávaš to, čo očakávaš. Odporúčam sa rovno pozrieť na FluentValidation, keďže model validation princíp, ktorý sa používal v minulosti, už v MinimalAPI nie je možný.</p>

<p>Aby si mohol lepšie štruktúrovať kód, budeš potrebovať pochopiť základy dependency injection (DI). Skús presunúť prácu s dátami zo samotných endpointov do samostatnej triedy (to, či ju nazveš repository, store, manager, je v tejto fáze jedno). Túto triedu teraz nakonfiguruj do DI kontajnera a nechaj si ju injektnúť do tvojich endpointov. Snaž sa pochopiť, na čo je to dobré a aspoň rámcovo pochop, ako to funguje.</p>

<p>🫡 Ak zvládneš tieto veci, budeš pripravený pridať sa k tímu a začať pracovať na menších úlohách v reálnom projekte.</p>

<blockquote>
  <p>💁‍♂️ Odporúčam začať s MinimalAPI. Ak však vieš, že firma, v ktorej budeš pracovať (alebo pracuješ), používa klasický prístup cez controllery, prečítaj si o tom tiež.</p>

</blockquote>

<p>Toto je napríklad u nás nutné minimum, ktoré musí junior ovládať, aby mohol prísť k nám do firmy. Po pohovore dostane zadanie, ktoré, ak vypracuje, tak by mal mať tieto zručnosti zvládnuté.</p>

<p>✅ Checklist:</p>

<p><img src="/assets/images/junior-developer/r-1.png" alt="Checklist prvý projekt" /></p>

<p><strong>🔗 Zdroje</strong></p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/aspnet/core/tutorials/min-web-api?view=aspnetcore-8.0&amp;tabs=visual-studio">Create a web API with ASP.NET Core</a></li>
  <li><a href="https://www.codecademy.com/learn/learn-c-sharp">Learn C#</a></li>
  <li><a href="https://learn.microsoft.com/en-us/shows/c-fundamentals-for-absolute-beginners/">C# Fundamentals for Absolute Beginners</a></li>
  <li><a href="https://www.postman.com/product/what-is-postman/">What is Postman?</a></li>
  <li><a href="https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli">First EF Core App</a></li>
  <li><a href="https://docs.fluentvalidation.net/en/latest/">FluentValidation Documentation</a></li>
  <li><a href="https://blog.burgyn.online/2023/01/16/asp-net-core-minimal-api-filters">ASP.NET Core Minimal API Filters</a></li>
  <li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0">Dependency injection in ASP.NET Core</a></li>
</ul>

<h3 id="práca-v-tíme">Práca v tíme</h3>

<p>Pokiaľ máš ako junior zvládnuté vyššie spomínané veci <em>(v rôznej úrovni. Niečo viac, niečo trochu menej)</em> môže začať robiť v tíme.</p>

<p>🚩 V tíme nie si sám. Na to aby tími dokázali rozumne pracovať nad spoločným code baseom využívajú rôzne verzionovacie systémy. V súčasnej dobe to je najčastejšie GIT. Je potrebné aby si zvládol základnú prácu s GIT-om. Na začiatok stačí vedieť získať aktuálnu verziu, vytvoriť si novú vetvu. Vedieť commitovať zmeny a pushnúť zmeny na server.</p>

<p>🚩 Nasleduje Pull request proces, ktorý má daná firma nastavený. <em>(Budeš musieť spoznať základy s nástrojom, ktorý používajú. GitHub / GitLab / AZURE DevOps / …)</em>  Tam získaš feadback na svoj kód. Je to výborné miesto na učenie sa. Pretože dostaneš konštruktívne pripomienky od skúsených ľudí. Naučíš sa veľa o projekte, na ktorom pracuješ, ale aj o samotnom programovaní.</p>

<p>🚩 Firma, v ktorej budeš pracovať, má určite nastavený systém práce, využívajú jednu z viacerých metód riadenia softvérového vývoja. V súčasnosti to bude pravdpodobne nejaká z agilných techník. Skús pochopiť základy agilného riadenia, pozri si čo je scrum.</p>

<p>🚩 Ďalej sa vzdelávaj v C# / .NET. Mal by si zvládnuť základy OOP v .NET (classes, objects, interfaces, inheritance, composition), základy generík, údajové štruktúry, error handling, … Je potrebné naučiť sa pracovať so štruktúrou projektov, závislosťami, balíčkovacím systémom (Nuget).</p>

<p>Na projekte už nepracuješ sám, potrebuješ pochopiť a dodržiavať dohodnuté tímové štandardy, čítaš a orientuješ sa v cudzom kóde.</p>

<p>✅ Checklist:</p>

<p><img src="/assets/images/junior-developer/r-2.png" alt="Praca v time.png" /></p>

<p><strong>🔗 Zdroje</strong></p>

<ul>
  <li><a href="https://medium.com/@dilkushi.j/scrum-for-beginners-ab1177f86f95">Scrum for Beginners</a></li>
  <li><a href="https://einsmanntech.medium.com/soft-skills-help-developers-level-up-here-are-the-10-id-tell-my-junior-self-e8daaa26ad8e">Soft Skills Help Developers Level Up: 10 Tips</a></li>
  <li><a href="https://dev.to/kanani_nirav/getting-started-with-github-actions-a-beginners-guide-og7">Getting Started with GitHub Actions: A Beginner’s Guide</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/create-first-pipeline?view=azure-devops&amp;tabs=java,browser">Create Your First Pipeline with Azure DevOps</a></li>
  <li><a href="https://github.blog/developer-skills/github/beginners-guide-to-github-creating-a-pull-request/">Beginner’s Guide to GitHub: Creating a Pull Request</a></li>
  <li><a href="https://www.w3schools.com/git/default.asp">Git Tutorial - W3Schools</a></li>
  <li><a href="https://dotnet.microsoft.com/en-us/learn/csharp">Learn C# with .NET</a></li>
</ul>

<h3 id="príprava-na-mediora">Príprava na Mediora</h3>

<p>Keď už zvládneš prácu v tíme a pracuješ na reálnych projektoch, začína sa obdobie, kedy sa pripravuješ na úroveň mediora. Medior vývojár by mal mať zvládnuté nielen technické zručnosti, ale aj schopnosť samostatne riešiť problémy, robiť rozhodnutia a prichádzať s návrhmi zlepšení.</p>

<p>🚩 Postupne budeš čoraz samostatnejší a budeš dostávať komplexnejšie úlohy. Tvoj kód musí byť nielen funkčný, ale aj prehľadný a udržiavateľný. V tejto fáze je dôležité osvojiť si princípy ako DRY (Don’t Repeat Yourself), KISS (Keep It Simple, Stupid) a YAGNI (You Aren’t Gonna Need It). Tieto základné princípi ti pomôžu ľahšie pristupovať k hľadaniu riešení.</p>

<p>🚩 V rámci ASP.NET Core budeš musieť hlbšie pochopiť základy <code class="highlighter-rouge">HTTP</code> protokolu, parameter bindingu, autentifikácie a autorizácie. Hoci sa neočakáva, že navrhneš celý proces autorizácie, mal by si rozumieť jej významu a základnému fungovaniu v projekte. Pre efektívnu prácu s dátami je dôležité pochopiť koncept DTO a data mappingu, kde ti môžu pomôcť knižnice ako AutoMapper alebo Mapster. Rovnako je dôležité oboznámiť sa s tým, ako sa v ASP.NET Core používa Options pattern na konfiguráciu služby.</p>

<blockquote>
  <p>📖 Nauč sa čítať dokumentáciu. Prejdi si dokumentáciu frameworku, s ktorým pracuješ.</p>
</blockquote>

<p>🚩 Práca s databázami je kľúčová. Mal by si zvládnuť nielen základné DML príkazy (<code class="highlighter-rouge">SELECT</code>, <code class="highlighter-rouge">INSERT</code>, <code class="highlighter-rouge">UPDATE</code>, <code class="highlighter-rouge">DELETE</code>), ale aj prácu s migráciami a vedieť, prečo a ako používať indexy na optimalizáciu dotazov. Technické fungovanie indexov môžeš zatiaľ považovať za mágiu 🔮.</p>

<p>🚩 Základné vývojárske zručnosti zahŕňajú efektívne používanie tvojho IDE (Visual Studio, VS Code, Rider), vrátane skratiek, debugovania a analýzy chýb. Investuj čas do spoznávania možností tvojho IDE, až kým niektoré skratky nebudeš mať v rukách namiesto v hlave. Tým získaš viac priestoru na premýšľanie o samotnom riešení a tvoje ruky budú len pretavovať tvoje myšlienky do kódu.</p>

<p>Rovnako by si mal byť zbehlý v práci s príkazovým riadkom, najmä s nástrojmi ako dotnet CLI a ef CLI, ktoré ti umožnia jednoducho spravovať projekty a databázy. UI sa neustále mení, ale CLI príkazy sú trvácne. Keď sa naučíš pracovať s CLI, tvoja produktivita výrazne vzrastie.</p>

<p>🚩 Základy DevOps ti pomôžu pochopiť, kde a ako je projekt nasadený a osvojíš si proces CI/CD (Continuous Integration / Continuous Deployment), aby si vedel lepšie spolupracovať s rôznymi časťami tímu.</p>

<p>🚩 Nezabúdaj na testovanie. Aj keď budete mať testerov, vývojár je spoluzodpovedný za kvalitu svojho kódu. Mal by si rozumieť tomu, čo vyvíjaš a vedieť otestovať základné scenáre.</p>

<p>Postupne si osvoj aj automatické testovanie. Začni s písaním unit testov a zdokonaľuj sa v nich. Ak sa na projekte používajú aj iné typy testov, ako napríklad E2E, integračné alebo load testy, na začiatku ich budeš vedieť upravovať (keď tvoja zmena spôsobí, že test zlyhá) a neskôr ich budeš sám vytvárať.</p>

<blockquote>
  <p>💁 Pri všetkej svojej práci nezabúdaj, že máš okolo seba šikovných kolegov, ktorí ti radi pomôžu, poradia a nasmerujú ťa. Využi ich skúsenosti a vedomosti. Na druhej strane, pamätaj, že práve tí najšikovnejší bývajú často veľmi vyťažení. Preto si vyberaj správny prístup, správne slová, a maj pokoru, no aj sebadôveru. Buď trpezlivý, keď odpoveď nepríde hneď.</p>
</blockquote>

<p>✅ Checklist</p>

<p><img src="/assets/images/junior-developer/r-3.png" alt="Príprava na mediora - Develoepr roadmap.png" /></p>

<p><strong>🔗 Zdroje</strong></p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-8.0">Introduction to ASP.NET Core</a></li>
  <li><a href="https://workat.tech/machine-coding/tutorial/software-design-principles-dry-yagni-eytrxfhz1fla">Software Design Principles: DRY, YAGNI</a></li>
  <li><a href="https://www.boldare.com/blog/kiss-yagni-dry-principles/">KISS, YAGNI, and DRY Principles</a></li>
  <li><a href="https://learn.microsoft.com/en-us/ef/">Entity Framework Documentation</a></li>
  <li><a href="https://toxigon.com/microsoft-visual-studio-tips-and-tricks">Microsoft Visual Studio Tips and Tricks</a></li>
  <li><a href="https://learnwithinderjeet.com/blog/mastering-visual-studio-2022-shortcuts-tips-and-tricks/">Mastering Visual Studio 2022: Shortcuts, Tips, and Tricks</a></li>
  <li><a href="https://www.meziantou.net/quick-introduction-to-xunitdotnet.htm">Quick Introduction to xUnit</a></li>
  <li><a href="https://fluentassertions.com/">FluentAssertions Documentation</a></li>
  <li><a href="https://github.com/AutoMapper/AutoMapper">AutoMapper GitHub Repository</a></li>
</ul>

<h2 id="záver">Záver</h2>

<p>Na záver by som chcel zdôrazniť, že cesta vývojára je vždy individuálna a plná výziev. Každý z nás má iné tempo učenia sa, rôzne záujmy a preferencie. Tento článok ti mal poskytnúť návod, ktorým smerom sa môžeš vydať, no to, ako sa rozhodneš napredovať, je úplne na tebe. Nezabúdaj, že programovanie je o neustálom zlepšovaní a prispôsobovaní sa novým technológiám. Neboj sa urobiť chybu, uč sa z nej a hľadaj spôsoby, ako sa posunúť ďalej. Dôležité je neostať stáť na mieste a stále sa snažiť zlepšovať. Takže držím palce na tvojej ceste za lepším kódom a hlavne – užívaj si to!</p>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="ASP.NET CORE" /><category term="long read" /><summary type="html"><![CDATA[Úvod]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/junior-developer/1.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/junior-developer/1.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Creating UML / Graphs with Mermaid</title><link href="https://blog.burgyn.online/2024/08/02/creating-uml-graphs-with-mermaid" rel="alternate" type="text/html" title="Creating UML / Graphs with Mermaid" /><published>2024-08-02T17:00:00+00:00</published><updated>2024-08-02T17:00:00+00:00</updated><id>https://blog.burgyn.online/2024/08/02/creating-uml-graphs-with-mermaid</id><content type="html" xml:base="https://blog.burgyn.online/2024/08/02/creating-uml-graphs-with-mermaid"><![CDATA[<p>How do you draw UML diagrams or charts? Do you draw? Not really, but sometimes it comes in handy and it’s nice to have the some part of the system described with a sequence diagram, for example. 
In the past I used <a href="https://draw.io">draw.io</a>. Cool tool, but there was a standard problem with it. How to store the diagrams? How to add them to the documentation? How to share them? And the biggest problem how to edit them afterwards 🤔? 
I used <a href="https://plantuml.com/">PlantUML</a> a couple of times, which solved exactly this. I simply create the diagram using text directly in markdown, and the tool renders the outline for me at that point. For me it’s a great solution.</p>

<p>However, I discovered <a href="https://mermaid.js.org/">Mermaid</a>. Same idea, but given where and how it’s integrated everywhere, it’s a significantly better choice for me.</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sequenceDiagram
    participant Author
    participant Notion
    participant Blog1 as blog.burgyn.online
    participant Blog2 as blog.vyvojari.dev
    participant Social as Social media

    Author-&gt;&gt;Notion: Record ideas for articles
    Author-&gt;&gt;Notion: Select an idea to write about
    Author-&gt;&gt;Author: Write the basic structure of the article
    Note right of Author: Drink lots of coffee!
    Author-&gt;&gt;Author: Insert images and diagrams
    Author-&gt;&gt;Author: Check grammar and style
    Author-&gt;&gt;Blog1: Publish the article
    Author-&gt;&gt;Blog2: Publish the article
    Author-&gt;&gt;Author: Create graphic material
    Author-&gt;&gt;Social: Share the article from blog.burgyn.online
    Note right of Author: Wait for feedback!
</code></pre></div></div>

<script src=""></script>
<div class="mermaid">
sequenceDiagram
    participant Author
    participant Notion
    participant Blog1 as blog.burgyn.online
    participant Blog2 as blog.vyvojari.dev
    participant Social as Social media

    Author-&gt;&gt;Notion: Record ideas for articles
    Author-&gt;&gt;Notion: Select an idea to write about
    Author-&gt;&gt;Author: Write the basic structure of the article
    Note right of Author: Drink lots of coffee!
    Author-&gt;&gt;Author: Insert images and diagrams
    Author-&gt;&gt;Author: Check grammar and style
    Author-&gt;&gt;Blog1: Publish the article
    Author-&gt;&gt;Blog2: Publish the article
    Author-&gt;&gt;Author: Create graphic material
    Author-&gt;&gt;Social: Share the article from blog.burgyn.online
    Note right of Author: Wait for feedback!
</div>

<p>Benefits for me:</p>
<ul>
  <li>easy to use directly in markdown
    <ul>
      <li>so it’s part of the documentation</li>
      <li>native git support (versioning, sharing, …)</li>
      <li>post-editing</li>
    </ul>
  </li>
  <li>possibility to create different types of diagrams (flowchart, sequence, class, state, …)</li>
  <li>various integrations
    <ul>
      <li>Notion</li>
      <li>GitHub</li>
      <li>Jekyll (I used <a href="https://github.com/jasonbellamy/jekyll-mermaid">jekyll-mermaid</a>)</li>
      <li>VsCode</li>
      <li>AZURE DevOps (Wiki only for now)</li>
      <li>ChatGPT 🤣</li>
      <li>…</li>
    </ul>
  </li>
</ul>

<p><a href="https://docs.mermaidchart.com/plugins/mermaid-chart-gpt">Plugin for ChatGPT</a>. I created the diagram above with a simple prompt to ChatGPT:</p>

<blockquote>
  <p>I’m going to post an article just about Mermaid. Create me some interesting sequence diagram on how to write such a blog post. I’m writing the ideas in Notion, posting the post on blog.burgyn.online and blog.vyvojari.dev. I then do a social media post about it.</p>
</blockquote>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="tools" /><category term="architecture" /><summary type="html"><![CDATA[The blog outlines how to leverage tools like PlantUML, Mermaid for diagram creation in markdown, storage, and sharing.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/code_images/creating-uml-graphs-with-mermaid/cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/code_images/creating-uml-graphs-with-mermaid/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Application Insights - Log Level</title><link href="https://blog.burgyn.online/2024/07/01/application-insights-log-level" rel="alternate" type="text/html" title="Application Insights - Log Level" /><published>2024-07-01T17:00:00+00:00</published><updated>2024-07-01T17:00:00+00:00</updated><id>https://blog.burgyn.online/2024/07/01/application-insights-log-level</id><content type="html" xml:base="https://blog.burgyn.online/2024/07/01/application-insights-log-level"><![CDATA[<p>🤦 I’ve been using ApplicationInsights for a few years now, but only now have I had a real need to look for my own custom logs there. It took me a while to figure out why I can’t find them there.</p>

<p>🤔 It’s not enough to set the generic “LogLeve:Default” to “Information” (Debug, Trace), because by default the ApplicationInsights provider only pushes logs with severity Warning and higher to AppInsights anyway.</p>

<p>💡 In order to push them there you need to explicitly set “ApplicationInsights:LogLevel:Default:” to “Information” (Debug, Trace).</p>

<p>🤞 Maybe this will help you sometime too.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Logging"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"LogLevel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Default"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Information"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="err">//</span><span class="w"> </span><span class="err">👇</span><span class="w"> </span><span class="err">This</span><span class="w"> </span><span class="err">section</span><span class="w"> </span><span class="err">is</span><span class="w"> </span><span class="err">important</span><span class="w">
    </span><span class="nl">"ApplicationInsights"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"LogLevel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"Default"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Information"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ApplicationInsights"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ConnectionString"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="AZURE" /><category term="csharp" /><category term="asp.net core" /><category term="logs" /><summary type="html"><![CDATA[Discover how to locate custom logs in Azure's ApplicationInsights and adjust settings so they're visible by default.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/blog-post-base-cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/blog-post-base-cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Arch tests - Check project references</title><link href="https://blog.burgyn.online/2024/06/23/arch-tests-check-project-references" rel="alternate" type="text/html" title="Arch tests - Check project references" /><published>2024-06-23T17:00:00+00:00</published><updated>2024-06-23T17:00:00+00:00</updated><id>https://blog.burgyn.online/2024/06/23/arch-tests-check-project-references</id><content type="html" xml:base="https://blog.burgyn.online/2024/06/23/arch-tests-check-project-references"><![CDATA[<p>In our company we have a large solution which contains more than 100 projects (not including test projects). Within that solution are WebAPI projects, libraries and AZURE functions. 
A couple of times it happened to us that our WebAPI projects referenced each other. Which is fundamentally wrong. They should be independent of each other and only reference other libraries.
<em>(we missed it during code review)</em></p>

<blockquote>
  <p>It is bad not only in principle, but also because MSBuild still has a problem and if you reference WebAPI projects in this way, it can non-deterministically give you <code class="highlighter-rouge">json</code> files in the output of one of the projects that are from another project. We have had this happen to us quite often.</p>
</blockquote>

<p>We decided to do a test on it. I don’t know if this is really the type of an architectural test, but let’s say it is 😊.</p>

<p>I originally tried to use the <code class="highlighter-rouge">Microsoft.Build</code> and <code class="highlighter-rouge">Microsoft.Build.Locator</code> libraries. These libraries contain the <code class="highlighter-rouge">Project</code> and <code class="highlighter-rouge">ProjectCollection</code> classes, which can retrieve project properties and references. <em>(Microsoft uses this for MSBuild)</em> But the problem with this was that it was very slow and had its flies.</p>

<p>Fortunately we didn’t need anything complicated from <code class="highlighter-rouge">csproj</code>, to be able to find out if it is a host project and its references.
So we read the necessary information from the <code class="highlighter-rouge">csproj</code> files directly via <code class="highlighter-rouge">XDocument</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">class</span> <span class="nc">ProjectFile</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="n">HashSet</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">_projectReferences</span> <span class="p">=</span> <span class="p">[];</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">DirectoryPath</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">FullPath</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">Sdk</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">OutputType</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">AzureFunctionsVersion</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsWebProject</span>
        <span class="p">=&gt;</span> <span class="n">OutputType</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Exe"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">)</span>
        <span class="p">||</span> <span class="n">Sdk</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Microsoft.NET.Sdk.Web"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">)</span>
        <span class="p">||</span> <span class="n">AzureFunctionsVersion</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="s">"v"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">);</span>

    <span class="k">public</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">ProjectsReferences</span> <span class="p">=&gt;</span> <span class="n">_projectReferences</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProjectFile</span><span class="p">&gt;</span> <span class="nf">LoadAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">projectFilePath</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">projectFile</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ProjectFile</span><span class="p">();</span>

        <span class="n">projectFile</span><span class="p">.</span><span class="n">Name</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetFileNameWithoutExtension</span><span class="p">(</span><span class="n">projectFilePath</span><span class="p">);</span>
        <span class="n">projectFile</span><span class="p">.</span><span class="n">FullPath</span> <span class="p">=</span> <span class="n">projectFilePath</span><span class="p">;</span>
        <span class="n">projectFile</span><span class="p">.</span><span class="n">DirectoryPath</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetDirectoryName</span><span class="p">(</span><span class="n">projectFilePath</span><span class="p">)</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

        <span class="k">using</span> <span class="nn">var</span> <span class="n">fileStream</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FileStream</span><span class="p">(</span><span class="n">projectFilePath</span><span class="p">,</span> <span class="n">FileMode</span><span class="p">.</span><span class="n">Open</span><span class="p">,</span> <span class="n">FileAccess</span><span class="p">.</span><span class="n">Read</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="k">await</span> <span class="n">XDocument</span><span class="p">.</span><span class="nf">LoadAsync</span><span class="p">(</span><span class="n">fileStream</span><span class="p">,</span> <span class="n">LoadOptions</span><span class="p">.</span><span class="n">None</span><span class="p">,</span> <span class="k">default</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">projectElement</span> <span class="p">=</span> <span class="n">doc</span><span class="p">.</span><span class="nf">Element</span><span class="p">(</span><span class="s">"Project"</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">projectElement</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">projectFile</span><span class="p">.</span><span class="n">Sdk</span> <span class="p">=</span> <span class="n">projectElement</span><span class="p">.</span><span class="nf">Attribute</span><span class="p">(</span><span class="s">"Sdk"</span><span class="p">)?.</span><span class="n">Value</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

            <span class="kt">var</span> <span class="n">propertyGroup</span> <span class="p">=</span> <span class="n">projectElement</span><span class="p">.</span><span class="nf">Element</span><span class="p">(</span><span class="s">"PropertyGroup"</span><span class="p">);</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">propertyGroup</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">projectFile</span><span class="p">.</span><span class="n">OutputType</span> <span class="p">=</span> <span class="n">propertyGroup</span><span class="p">.</span><span class="nf">Element</span><span class="p">(</span><span class="s">"OutputType"</span><span class="p">)?.</span><span class="n">Value</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
                <span class="n">projectFile</span><span class="p">.</span><span class="n">AzureFunctionsVersion</span> <span class="p">=</span> <span class="n">propertyGroup</span><span class="p">.</span><span class="nf">Element</span><span class="p">(</span><span class="s">"AzureFunctionsVersion"</span><span class="p">)?.</span><span class="n">Value</span> <span class="p">??</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="n">projectFile</span><span class="p">.</span><span class="n">_projectReferences</span> <span class="p">=</span> <span class="n">projectElement</span>
                <span class="p">.</span><span class="nf">Elements</span><span class="p">(</span><span class="s">"ItemGroup"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">Elements</span><span class="p">(</span><span class="s">"ProjectReference"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">Attributes</span><span class="p">(</span><span class="s">"Include"</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">attr</span> <span class="p">=&gt;</span> <span class="n">attr</span><span class="p">.</span><span class="n">Value</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ToHashSet</span><span class="p">();</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">projectFile</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>be careful when using <code class="highlighter-rouge">Directory.Build.props</code>, then not all properties are directly in the <code class="highlighter-rouge">csproj</code> file.
We didn’t mind, though, because the necessary ones were there.</p>
</blockquote>

<p>We used the SDK property for ASP.NET Core WebAPI projects to determine if it is a host project and the <code class="highlighter-rouge">AzureFunctionsVersion</code> property for Azure Functions.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="kt">bool</span> <span class="n">IsWebProject</span>
    <span class="p">=&gt;</span> <span class="n">OutputType</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Exe"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">)</span>
    <span class="p">||</span> <span class="n">Sdk</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Microsoft.NET.Sdk.Web"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">)</span>
    <span class="p">||</span> <span class="n">AzureFunctionsVersion</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="s">"v"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">);</span>
</code></pre></div></div>

<p>A <code class="highlighter-rouge">ProjectFileCollection</code> class that retrieves all the projects in the subdirectories and their references.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">class</span> <span class="nc">ProjectFileCollection</span> <span class="p">:</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">ProjectFile</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">ProjectFile</span><span class="p">&gt;</span> <span class="n">_projects</span> <span class="p">=</span> <span class="k">new</span><span class="p">();</span>

    <span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProjectFileCollection</span><span class="p">&gt;</span> <span class="nf">LoadSolution</span><span class="p">(</span><span class="kt">string</span> <span class="n">solutionDirectory</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">projectFileCollection</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ProjectFileCollection</span><span class="p">();</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">projectFile</span> <span class="k">in</span> <span class="n">Directory</span><span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="n">solutionDirectory</span><span class="p">,</span> <span class="s">"*.csproj"</span><span class="p">,</span> <span class="n">SearchOption</span><span class="p">.</span><span class="n">AllDirectories</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">project</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ProjectFile</span><span class="p">.</span><span class="nf">LoadAsync</span><span class="p">(</span><span class="n">projectFile</span><span class="p">);</span>
            <span class="n">projectFileCollection</span><span class="p">.</span><span class="n">_projects</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">project</span><span class="p">.</span><span class="n">FullPath</span><span class="p">,</span> <span class="n">project</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">projectFileCollection</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">ProjectFile</span> <span class="nf">GetProject</span><span class="p">(</span><span class="kt">string</span> <span class="n">projectPath</span><span class="p">)</span>
        <span class="p">=&gt;</span> <span class="n">_projects</span><span class="p">[</span><span class="n">projectPath</span><span class="p">];</span>

    <span class="k">public</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">ProjectFile</span><span class="p">&gt;</span> <span class="nf">GetProjectReferences</span><span class="p">(</span><span class="n">ProjectFile</span> <span class="n">project</span><span class="p">)</span>
        <span class="p">=&gt;</span> <span class="n">project</span><span class="p">.</span><span class="n">ProjectsReferences</span>
            <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="nf">GetProject</span><span class="p">(</span><span class="nf">GetReferenceFullPath</span><span class="p">(</span><span class="n">project</span><span class="p">.</span><span class="n">DirectoryPath</span><span class="p">,</span> <span class="n">p</span><span class="p">)));</span>

    <span class="k">private</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">GetReferenceFullPath</span><span class="p">(</span><span class="kt">string</span> <span class="n">projectDir</span><span class="p">,</span> <span class="kt">string</span> <span class="n">referencePath</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">IsPathRooted</span><span class="p">(</span><span class="n">referencePath</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="n">referencePath</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetFullPath</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="n">projectDir</span><span class="p">,</span> <span class="n">referencePath</span><span class="p">));</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">IEnumerator</span><span class="p">&lt;</span><span class="n">ProjectFile</span><span class="p">&gt;</span> <span class="nf">GetEnumerator</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="n">_projects</span><span class="p">.</span><span class="n">Values</span><span class="p">.</span><span class="nf">GetEnumerator</span><span class="p">();</span>

    <span class="n">IEnumerator</span> <span class="n">IEnumerable</span><span class="p">.</span><span class="nf">GetEnumerator</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="nf">GetEnumerator</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The test itself can then look like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">WebProjects_ShouldNotReferenceOtherWebProjects</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">solutionDirectory</span> <span class="p">=</span> <span class="nf">FindSolutionDirectory</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">projects</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ProjectFileCollection</span><span class="p">.</span><span class="nf">LoadSolution</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="n">solutionDirectory</span><span class="p">,</span> <span class="s">"src"</span><span class="p">));</span>

    <span class="kt">var</span> <span class="n">webProjects</span> <span class="p">=</span> <span class="n">projects</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">IsWebProject</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">errorMessage</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringBuilder</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">wrongRecerencesCount</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">webProject</span> <span class="k">in</span> <span class="n">webProjects</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">projectReferences</span> <span class="p">=</span> <span class="n">projects</span><span class="p">.</span><span class="nf">GetProjectReferences</span><span class="p">(</span><span class="n">webProject</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">webProjectReferences</span> <span class="p">=</span> <span class="n">projectReferences</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">IsWebProject</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">webProjectReferences</span><span class="p">.</span><span class="nf">Any</span><span class="p">())</span>
        <span class="p">{</span>
            <span class="n">errorMessage</span><span class="p">.</span><span class="nf">AppendFormat</span><span class="p">(</span><span class="s">"&gt; {0}:"</span><span class="p">,</span> <span class="n">webProject</span><span class="p">.</span><span class="n">Name</span><span class="p">).</span><span class="nf">AppendLine</span><span class="p">();</span>
            <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">reference</span> <span class="k">in</span> <span class="n">webProjectReferences</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">errorMessage</span><span class="p">.</span><span class="nf">AppendFormat</span><span class="p">(</span><span class="s">"\t- {0}"</span><span class="p">,</span> <span class="n">reference</span><span class="p">.</span><span class="n">Name</span><span class="p">).</span><span class="nf">AppendLine</span><span class="p">();</span>
            <span class="p">}</span>
            <span class="n">wrongRecerencesCount</span><span class="p">++;</span>
            <span class="n">errorMessage</span><span class="p">.</span><span class="nf">AppendLine</span><span class="p">(</span><span class="s">"----------------------------------------"</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">wrongRecerencesCount</span><span class="p">.</span><span class="nf">Should</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">Be</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="s">"Web project should not reference other web project:\n"</span> <span class="p">+</span> <span class="n">errorMessage</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The result may look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Expected wrongRecerencesCount to be 0 because Web project should not reference other web project:
&gt; Kros.Esw.ApiProjectA:
	- Kros.Esw.ApiProjectB
	- Kros.Esw.ApiProjectC
	- Kros.Esw.ApiProjectD
----------------------------------------
, but found 1 (difference of 1).
</code></pre></div></div>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="csharp" /><category term="architecture" /><category term="unit tests" /><category term="AZURE" /><summary type="html"><![CDATA[The blog post discusses managing project references in WebAPI projects featuring C# codes to perform architectural tests and maintain project independence.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/code_images/arch-tests-check-project-references/cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/code_images/arch-tests-check-project-references/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Own mediator</title><link href="https://blog.burgyn.online/2024/06/09/own-mediator" rel="alternate" type="text/html" title="Own mediator" /><published>2024-06-09T17:00:00+00:00</published><updated>2024-06-09T17:00:00+00:00</updated><id>https://blog.burgyn.online/2024/06/09/own-mediator</id><content type="html" xml:base="https://blog.burgyn.online/2024/06/09/own-mediator"><![CDATA[<p>Currently, the Mediator design pattern is often used. Its purpose is to separate the communication between objects from each other. Which will reduce dependencies between classes, make the code more transparent and simplify testing. 
In a .NET environment, a developer often reaches for the cool library <a href="https://github.com/jbogard/MediatR">MediatR</a> by Jimmy Bogard. This library provides an easy way to implement the Mediator pattern into an application.</p>

<p>However, there are situations where we can’t/don’t want to use a third party library. For example, you are writing some custom library/framework and you don’t want to introduce such dependencies into it.</p>

<p>Something similar happened to us. Fortunately, we didn’t need the complete functionality that MediatR provides, but just a basic event dispatch and processing was enough.</p>

<p>Therefore, I will now show how to easily create such an implementation of your own Mediator.</p>

<p>Let’s start with defining the message interface. In our case it was a domain event, hence the name.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IDomainEvent</span>
<span class="p">{</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>It’s just a pure markup interface. We could do without it, but it’s a good way to indicate that it’s an event.</p>
</blockquote>

<p>Then the event processing interface.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;</span> 
    <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="nf">HandleAsync</span><span class="p">(</span><span class="n">TEvent</span> <span class="n">domainEvent</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span> <span class="p">=</span> <span class="k">default</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We need an interface for sending events.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IEventPublisher</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="n">PublishAsync</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span><span class="n">TEvent</span> <span class="n">domainEvent</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
        <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We could stop with the abstractions here and start implementing. However, we decided to add an abstraction for the processing strategy in there. For now, the sequential processing of events was enough, but we might need other strategies in the future.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IEventPublisherStrategy</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="n">PublishAsync</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span>
        <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;&gt;</span> <span class="n">handlers</span><span class="p">,</span>
        <span class="n">TEvent</span> <span class="n">domainEvent</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
        <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="implementation">Implementation</h2>

<p>Publisher can look like this:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">class</span> <span class="nc">EventPublisher</span> <span class="p">:</span> <span class="n">IEventPublisher</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IServiceProvider</span> <span class="n">_serviceProvider</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IEventPublisherStrategy</span> <span class="n">_publisherStrategy</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IHttpContextAccessor</span> <span class="n">_httpContextAccessor</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">EventPublisher</span><span class="p">(</span>
        <span class="n">IServiceProvider</span> <span class="n">serviceProvider</span><span class="p">,</span>
        <span class="n">IHttpContextAccessor</span> <span class="n">httpContextAccessor</span><span class="p">,</span>
        <span class="n">IEventPublisherStrategy</span> <span class="n">publisherStrategy</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_serviceProvider</span> <span class="p">=</span> <span class="n">serviceProvider</span><span class="p">;</span>
        <span class="n">_publisherStrategy</span> <span class="p">=</span> <span class="n">publisherStrategy</span><span class="p">;</span>
        <span class="n">_httpContextAccessor</span> <span class="p">=</span> <span class="n">httpContextAccessor</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="n">PublishAsync</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span><span class="n">TEvent</span> <span class="n">domainEvent</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
        <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span>
    <span class="p">{</span>
        <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;&gt;</span> <span class="n">handlers</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="c1">// 👇 if available, then find handlers in HttpContext services</span>
            <span class="n">handlers</span> <span class="p">=</span> <span class="n">GetHandlers</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span><span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">.</span><span class="n">RequestServices</span><span class="p">);</span>
            <span class="c1">// 👇 publish event to all handlers</span>
            <span class="k">await</span> <span class="n">_publisherStrategy</span><span class="p">.</span><span class="nf">PublishAsync</span><span class="p">(</span><span class="n">handlers</span><span class="p">,</span> <span class="n">domainEvent</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">else</span>
        <span class="p">{</span>
            <span class="c1">// 👇 if not, then create new scope</span>
            <span class="k">using</span> <span class="nn">var</span> <span class="n">scope</span> <span class="p">=</span> <span class="n">_serviceProvider</span><span class="p">.</span><span class="nf">CreateScope</span><span class="p">();</span>
            <span class="n">handlers</span> <span class="p">=</span> <span class="n">GetHandlers</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span><span class="n">scope</span><span class="p">.</span><span class="n">ServiceProvider</span><span class="p">);</span>
            <span class="c1">// 👇 publish event to all handlers</span>
            <span class="k">await</span> <span class="n">_publisherStrategy</span><span class="p">.</span><span class="nf">PublishAsync</span><span class="p">(</span><span class="n">handlers</span><span class="p">,</span> <span class="n">domainEvent</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">static</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;&gt;</span> <span class="n">GetHandlers</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span><span class="n">IServiceProvider</span> <span class="n">serviceProvider</span><span class="p">)</span>
        <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span>
        <span class="p">=&gt;</span> <span class="n">serviceProvider</span><span class="p">.</span><span class="n">GetServices</span><span class="p">&lt;</span><span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;&gt;();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Publisher takes care of getting the handlers from the container and then forwards them to publisher strategy, which decides how they will be processed.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">ForeachAwaitPublisherStrategy</span> <span class="p">:</span> <span class="n">IEventPublisherStrategy</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="n">PublishAsync</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;(</span>
        <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;&gt;</span> <span class="n">handlers</span><span class="p">,</span>
        <span class="n">TEvent</span> <span class="n">domainEvent</span><span class="p">,</span>
        <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
        <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span>
    <span class="p">{</span>
        <span class="c1">// 👇 publish event to all handlers</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">handler</span> <span class="k">in</span> <span class="n">handlers</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">await</span> <span class="n">handler</span><span class="p">.</span><span class="nf">HandleAsync</span><span class="p">(</span><span class="n">domainEvent</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Our <code class="highlighter-rouge">ForeachAwaitPublisherStrategy</code> handles events sequentially. It forwards the message to all handlers and waits for them to process it.</p>

<h2 id="registration">Registration</h2>

<p>In order to use the abstractions we create, we need to register them in the DI container.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="n">IServiceCollection</span> <span class="nf">AddEventPublisher</span><span class="p">(</span><span class="k">this</span> <span class="n">IServiceCollection</span> <span class="n">services</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">services</span><span class="p">.</span><span class="nf">AddHttpContextAccessor</span><span class="p">();</span>
    <span class="n">services</span><span class="p">.</span><span class="n">TryAddSingleton</span><span class="p">&lt;</span><span class="n">IEventPublisher</span><span class="p">,</span> <span class="n">EventPublisher</span><span class="p">&gt;();</span>
    <span class="n">services</span><span class="p">.</span><span class="n">TryAddSingleton</span><span class="p">&lt;</span><span class="n">IEventPublisherStrategy</span><span class="p">,</span> <span class="n">ForeachAwaitPublisherStrategy</span><span class="p">&gt;();</span>
    <span class="k">return</span> <span class="n">services</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can also create an extension for easy registration of handlers.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="n">IServiceCollection</span> <span class="n">AddDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">,</span> <span class="n">THandler</span><span class="p">&gt;(</span>
    <span class="k">this</span> <span class="n">IServiceCollection</span> <span class="n">services</span><span class="p">,</span>
    <span class="n">ServiceLifetime</span> <span class="n">lifetime</span> <span class="p">=</span> <span class="n">ServiceLifetime</span><span class="p">.</span><span class="n">Transient</span><span class="p">)</span>
    <span class="k">where</span> <span class="n">TEvent</span> <span class="p">:</span> <span class="n">IDomainEvent</span>
    <span class="k">where</span> <span class="n">THandler</span> <span class="p">:</span> <span class="k">class</span><span class="err">,</span> <span class="nc">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="n">services</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ServiceDescriptor</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;),</span> <span class="k">typeof</span><span class="p">(</span><span class="n">THandler</span><span class="p">),</span> <span class="n">lifetime</span><span class="p">));</span>

    <span class="k">return</span> <span class="n">services</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Using this extension looks like this <code class="highlighter-rouge">services.AddDomainEventHandler&lt;YourEvent, YourHandler&gt;();</code>. 
This is because I wanted to avoid reflection. If reflection doesn’t bother you, a simplified call to <code class="highlighter-rouge">services.AddDomainEventHandler&lt;YourHandler&gt;();</code>. 
But I’ll leave that up to you 😉.</p>
</blockquote>

<h2 id="usage">Usage</h2>

<p>Finally, we’ll show you how to use our own Mediator.</p>

<p>Event and handler:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 👇 The event that the product was created</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">ProductCreated</span><span class="p">(</span><span class="kt">int</span> <span class="n">Id</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Name</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">Price</span><span class="p">)</span> <span class="p">:</span> <span class="n">IDomainEvent</span><span class="p">;</span>

<span class="c1">// 👇 The handler that will process the event</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductCreatedHandler</span> <span class="p">:</span> <span class="n">IDomainEventHandler</span><span class="p">&lt;</span><span class="n">ProductCreated</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Task</span> <span class="nf">HandleAsync</span><span class="p">(</span><span class="n">ProductCreated</span> <span class="n">domainEvent</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// 👇 Process the event</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Product created: </span><span class="p">{</span><span class="n">domainEvent</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The registration:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddEventPublisher</span><span class="p">();</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddDomainEventHandler</span><span class="p">&lt;</span><span class="n">ProductCreated</span><span class="p">,</span> <span class="n">ProductCreatedHandler</span><span class="p">&gt;();</span>
</code></pre></div></div>

<p>Use in endpoint:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/products"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">Product</span> <span class="n">product</span><span class="p">,</span> <span class="n">IEventPublisher</span> <span class="n">publisher</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="c1">// Save the product</span>
    <span class="c1">// 👇 Publish the event</span>
    <span class="k">await</span> <span class="n">publisher</span><span class="p">.</span><span class="nf">PublishAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductCreated</span><span class="p">(</span><span class="n">product</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span> <span class="n">product</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">product</span><span class="p">.</span><span class="n">Price</span><span class="p">));</span>
    <span class="k">return</span> <span class="n">product</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="links">Links</h2>

<ul>
  <li><a href="https://github.com/Burgyn/MMLib.Sample.Mediator">Sample project</a></li>
</ul>]]></content><author><name>Milan Martiniak</name><email>mino.martiniak@gmail.com</email></author><category term="csharp" /><category term="dotnet" /><category term="unit tests" /><category term="architecture" /><summary type="html"><![CDATA[Discover how to build your own implementation of the Mediator design pattern in a .NET environment without third-party libraries.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.burgyn.online/assets/images/code_images/own-mediator/cover.png" /><media:content medium="image" url="https://blog.burgyn.online/assets/images/code_images/own-mediator/cover.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>