<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gabriel Weidmann</title>
    <description>The latest articles on DEV Community by Gabriel Weidmann (@gabrielweidmann).</description>
    <link>https://dev.to/gabrielweidmann</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2814%2Fb80fb4f7-07fe-4706-a8df-fe73f7a8542e.png</url>
      <title>DEV Community: Gabriel Weidmann</title>
      <link>https://dev.to/gabrielweidmann</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gabrielweidmann"/>
    <language>en</language>
    <item>
      <title>Display and test openapi.yaml file</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Thu, 16 Apr 2026 09:19:27 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/display-and-test-openapiyaml-file-53h9</link>
      <guid>https://dev.to/gabrielweidmann/display-and-test-openapiyaml-file-53h9</guid>
      <description>&lt;p&gt;From a partner company I got a &lt;code&gt;openapi.yaml&lt;/code&gt; file to evaluate their api for our use cases. Mostly I know those files from working with ASP.NET Core Apis + Swagger UI, but this time it was just a standalone file.&lt;/p&gt;

&lt;p&gt;I got the file and an api key for the sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did not work: Use via ASP.NET Core swagger
&lt;/h2&gt;

&lt;p&gt;The first thing I was trying was to use the already known swagger ui as it's the same format.&lt;/p&gt;

&lt;p&gt;So:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setting up a new ASP.NET Core Api project in Visual Studio&lt;/li&gt;
&lt;li&gt;Including the latest &lt;code&gt;Swashbuckle.AspNetCore&lt;/code&gt; nuget&lt;/li&gt;
&lt;li&gt;Putting the &lt;code&gt;openapi.yaml&lt;/code&gt; file in the &lt;code&gt;wwwroot&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Setting up the Program.cs
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseHttpsRedirection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Allow to serve the unknown-by-default file type .yaml&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileExtensionContentTypeProvider&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mappings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;".yaml"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/yaml"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseStaticFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;StaticFileOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ContentTypeProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSwaggerUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwaggerEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/openapi.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Imported API"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoutePrefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"swagger"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Accessing the api via &lt;a href="https://localhost:7298/swagger" rel="noopener noreferrer"&gt;https://localhost:7298/swagger&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This worked almost, but then CORS came around :-(&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Access to fetch at '&lt;a href="https://sandbox.***.com/api/.." rel="noopener noreferrer"&gt;https://sandbox.***.com/api/..&lt;/a&gt;.' from origin '&lt;a href="https://localhost:7298" rel="noopener noreferrer"&gt;https://localhost:7298&lt;/a&gt;' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem seems to be the browser, so let's look for a non-browser-solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did work: Import in Postman
&lt;/h2&gt;

&lt;p&gt;Then I noticed that Postman got the functionality to import openapi spec files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffijwq064bnakgpkrtmii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffijwq064bnakgpkrtmii.png" alt="Importing openapi spec in Postman" width="699" height="867"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here it's possible to simulate the api requests just as good (or better?) as in swagger UI.&lt;/p&gt;

</description>
      <category>api</category>
      <category>openapi</category>
      <category>test</category>
    </item>
    <item>
      <title>Multi-Highlighting in PDF.js</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Fri, 14 Mar 2025 16:58:38 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/multi-highlighting-in-pdfjs-403l</link>
      <guid>https://dev.to/gabrielweidmann/multi-highlighting-in-pdfjs-403l</guid>
      <description>&lt;p&gt;For a current project (document management), we were given a seemingly simple task:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Implement a PDF preview where I can see all full-text search results highlighted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Searching for a single value is easy: just press &lt;code&gt;Ctrl + F&lt;/code&gt;, search for the term, and it gets highlighted automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pwe1n3xvpc72p85dzj8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pwe1n3xvpc72p85dzj8.png" alt="Single value highlighting" width="800" height="789"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, highlighting multiple values is a bit trickier. After some research, we discovered that only one major PDF viewer supports this functionality out of the box: Mozilla Firefox’s PDF.js viewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the viewer
&lt;/h2&gt;

&lt;p&gt;We integrated the viewer as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"@_pdfPreviewId"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"document"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/pdf"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/web/viewer.html?file="&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pdf-preview"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loads PDF.js with an empty view. We found that the required functionality is only accessible through JavaScript. Note that the ID changes dynamically with the currently previewed document to prevent the iframe from displaying the wrong content.&lt;/p&gt;

&lt;p&gt;Whenever the displayed document changes, the previous preview immediately disappears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt; Dynamic id coupled to the preview id to prevent race condition while loading pdfs &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_pdfPreviewId&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"pdf-preview_"&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;_previewedDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever the displayed document changes the previous preview immediately disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the logic
&lt;/h2&gt;

&lt;p&gt;Next, we call JavaScript from our C# Blazor code, passing in the necessary parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;JS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeVoidAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"documentPreview.open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_pdfPreviewId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_previewedDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DownloadPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valuesToHighlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the JavaScript code we used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;documentPreview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iframeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valuesToHighlight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iframeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Wait until available&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PDFViewerApplication&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// Wait for a maximum of 10 seconds&lt;/span&gt;
            &lt;span class="c1"&gt;// This will happen if the PDF preview is getting replaced (by iframe id) before the file loading has started&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="c1"&gt;// Prevent a bug where there are multiple iframes with the same id getting created and replaced&lt;/span&gt;
            &lt;span class="c1"&gt;// This would lead to the iframe displaying the wrong file&lt;/span&gt;
            &lt;span class="nx"&gt;iframe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iframeId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Open the PDF with credentials (we're using msal; without this we get 401 Unauthorized)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PDFViewerApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;withCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to open PDF: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Show highlights&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valuesToHighlight&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;valuesToHighlight: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;valuesToHighlight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;eventBus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PDFViewerApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pdfViewer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;searchOption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;valuesToHighlight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;caseSensitive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;highlightAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;phraseSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="nx"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;find&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;searchOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;And there it is: beautiful multi-term highlighting in action.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6hjl77kkvym6awr2rb8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe6hjl77kkvym6awr2rb8.png" alt="Multi-Highlighting in all of it's glory" width="800" height="899"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing to keep in mind: using the search feature within the viewer will break the highlighting.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>javascript</category>
      <category>pdfjs</category>
      <category>blazor</category>
    </item>
    <item>
      <title>Preserve File Creation and Modification Date When Downloading in a ZIP File</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Wed, 12 Mar 2025 18:12:54 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/preserve-file-creation-and-modification-date-when-downloading-in-a-zip-file-4gb8</link>
      <guid>https://dev.to/gabrielweidmann/preserve-file-creation-and-modification-date-when-downloading-in-a-zip-file-4gb8</guid>
      <description>&lt;p&gt;As part of a recent project, it was necessary to ensure that the creation date and modification date of files were preserved when downloading them from the browser. The default behavior of browsers is to set both dates to the current time upon download (some research showed that this behavior can't be easily bypassed when downloading files directly in the browser).&lt;/p&gt;

&lt;p&gt;For example, here’s the title image: just downloaded, but definitely not just created.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzp9akc1b2s8ow82j2ab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzp9akc1b2s8ow82j2ab.png" alt="default metadata" width="267" height="96"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A workaround was to always download the files in ZIP archives and adjust the creation and modification dates there. Unfortunately, in .NET and common libraries, only the &lt;em&gt;LastModifiedAt&lt;/em&gt; can be set. It seems the standard only supports this value.&lt;/p&gt;

&lt;p&gt;Fortunately, the &lt;a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT" rel="noopener noreferrer"&gt;ZIP specification&lt;/a&gt; does support an NTFS extra field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   4.5.5 -NTFS Extra Field (0x000a):

      The following is the layout of the NTFS attributes 
      "extra" block. (Note: At this time the Mtime, Atime
      and Ctime values MAY be used on any WIN32 system.)  

      Note: all fields stored in Intel low-byte/high-byte order.

        Value      Size       Description
        -----      ----       -----------
(NTFS)  0x000a     2 bytes    Tag for this "extra" block type
        TSize      2 bytes    Size of the total "extra" block
        Reserved   4 bytes    Reserved for future use
        Tag1       2 bytes    NTFS attribute tag value #1
        Size1      2 bytes    Size of attribute #1, in bytes
        (var)      Size1      Attribute #1 data
         .
         .
         .
         TagN       2 bytes    NTFS attribute tag value #N
         SizeN      2 bytes    Size of attribute #N, in bytes
         (var)      SizeN      Attribute #N data

       For NTFS, values for Tag1 through TagN are as follows:
       (currently only one set of attributes is defined for NTFS)

         Tag        Size       Description
         -----      ----       -----------
         0x0001     2 bytes    Tag for attribute #1 
         Size1      2 bytes    Size of attribute #1, in bytes
         Mtime      8 bytes    File last modification time
         Atime      8 bytes    File last access time
         Ctime      8 bytes    File creation time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, you can provide additional values—specifically the &lt;code&gt;Creation time&lt;/code&gt; and &lt;code&gt;Last modification time&lt;/code&gt; you want.&lt;/p&gt;

&lt;p&gt;The implementation isn’t completely straightforward.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You need a ZIP library that supports writing this NTFS extra field. I came across &lt;a href="https://github.com/icsharpcode/SharpZipLib" rel="noopener noreferrer"&gt;SharpZipLib&lt;/a&gt;. It hasn’t been updated since August 2023, but it works perfectly fine for a server-side packaging solution.&lt;/li&gt;
&lt;li&gt;You need the code to write the field.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zipMemoryStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zipStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ZipOutputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipMemoryStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IsStreamOwner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// Prevent from disposing the memory stream&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zipEntry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ZipEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myFilename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;ntfsExtraData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateNtfsExtraField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;creationTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lastAccessTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedAt&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;zipEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtraData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ntfsExtraData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;zipStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PutNextEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipEntry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fileStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyToAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;fileStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;CreateNtfsExtraField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;creationTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;lastAccessTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// NTFS extra field structure:&lt;/span&gt;
    &lt;span class="c1"&gt;// [Header ID (2 bytes)][Data Size (2 bytes)][Reserved (4 bytes)][Tag (2 bytes)][Content Size (2 bytes)][Timestamps (24 bytes)]&lt;/span&gt;

    &lt;span class="c1"&gt;// Constants&lt;/span&gt;
    &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;headerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x000A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// NTFS field&lt;/span&gt;
    &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x0001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Attribute Tag - "Standard information"&lt;/span&gt;

    &lt;span class="c1"&gt;// Time conversion: DateTime -&amp;gt; Windows FILETIME (ticks since 1601-01-01)&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;creationFileTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;creationTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFileTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;accessFileTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lastAccessTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFileTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;modifiedFileTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFileTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BinaryWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Write Header ID and Size Placeholder&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Data Size: Reserved(4) + Tag(2) + Size(2) + 24 bytes&lt;/span&gt;

        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Reserved (4 bytes)&lt;/span&gt;

        &lt;span class="c1"&gt;// Attribute Tag&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Content Size (24 bytes for 3 timestamps)&lt;/span&gt;

        &lt;span class="c1"&gt;// Write the timestamps (each 8 bytes)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifiedFileTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessFileTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creationFileTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And voilà: the metadata is preserved as desired :-)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fitdfwp2fybq66ehe63mk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fitdfwp2fybq66ehe63mk.png" alt="adjusted metadata" width="284" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Note: "Last Access" changes when opening the metadata dialog; before that, it’s correct 😉)&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>zip</category>
      <category>download</category>
      <category>aspnet</category>
    </item>
    <item>
      <title>Datei-Erstellungs- und -Bearbeitungsdatum beim Download in einer ZIP-Datei beibehalten</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Wed, 12 Mar 2025 17:58:18 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/datei-erstellungs-und-bearbeitungsdatum-beim-download-in-einer-zip-datei-beibehalten-112g</link>
      <guid>https://dev.to/gabrielweidmann/datei-erstellungs-und-bearbeitungsdatum-beim-download-in-einer-zip-datei-beibehalten-112g</guid>
      <description>&lt;p&gt;Im Rahmen eines aktuellen Projektes war es notwendig, dass beim Download von Dateien aus dem Browser das Erstellungsdatum und Bearbeitungsdatum erhalten bleiben. Das Standardverhalten von Browsern ist nämlich, beim Download beides auf die aktuelle Zeit zu setzen (einiges an Recherche hat ergeben, dass sich dieses Verhalten beim Download im Browser nicht praktikabel umgehen lässt).&lt;/p&gt;

&lt;p&gt;Beispielsweise hier das Titelbild: Gerade erst heruntergeladen, aber sicherlich nicht gerade erst erstellt.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzp9akc1b2s8ow82j2ab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbzp9akc1b2s8ow82j2ab.png" alt="Standard-Metadaten" width="267" height="96"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ein Workaround war, die Dateien stets in ZIP-Dateien herunterzuladen und das Erstellungs- und Bearbeitungsdatum dort anzupassen. Unglücklicherweise ist in .NET und gängigen Bibliotheken stets nur das &lt;em&gt;LastModifiedAt&lt;/em&gt; zur Änderung verfügbar. Scheinbar unterstützt der Standard nur diesen Wert.&lt;/p&gt;

&lt;p&gt;Glücklicherweise unterstützt die &lt;a href="https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT" rel="noopener noreferrer"&gt;ZIP-Definition&lt;/a&gt; aber ein NTFS-Extra-Feld:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   4.5.5 -NTFS Extra Field (0x000a):

      The following is the layout of the NTFS attributes 
      "extra" block. (Note: At this time the Mtime, Atime
      and Ctime values MAY be used on any WIN32 system.)  

      Note: all fields stored in Intel low-byte/high-byte order.

        Value      Size       Description
        -----      ----       -----------
(NTFS)  0x000a     2 bytes    Tag for this "extra" block type
        TSize      2 bytes    Size of the total "extra" block
        Reserved   4 bytes    Reserved for future use
        Tag1       2 bytes    NTFS attribute tag value #1
        Size1      2 bytes    Size of attribute #1, in bytes
        (var)      Size1      Attribute #1 data
         .
         .
         .
         TagN       2 bytes    NTFS attribute tag value #N
         SizeN      2 bytes    Size of attribute #N, in bytes
         (var)      SizeN      Attribute #N data

       For NTFS, values for Tag1 through TagN are as follows:
       (currently only one set of attributes is defined for NTFS)

         Tag        Size       Description
         -----      ----       -----------
         0x0001     2 bytes    Tag for attribute #1 
         Size1      2 bytes    Size of attribute #1, in bytes
         Mtime      8 bytes    File last modification time
         Atime      8 bytes    File last access time
         Ctime      8 bytes    File creation time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mit diesem lassen sich weitere Werte mitgeben; eben genau die gewünschten &lt;code&gt;Creation time&lt;/code&gt; und &lt;code&gt;Last modification time&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Die Umsetzung ist nicht ganz straightforward.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;braucht es eine ZIP-Bibliothek die das Schreiben dieses &lt;code&gt;NTFS extra&lt;/code&gt; Feldes unterstützt. Hier bin ich auf &lt;a href="https://github.com/icsharpcode/SharpZipLib" rel="noopener noreferrer"&gt;SharpZipLib&lt;/a&gt; gestoßen. Wird zwar seit August 2023 nicht mehr aktualisiert, aber für eine serverseitige Pack-Logik passt es wunderbar.&lt;/li&gt;
&lt;li&gt;braucht es den Code, um das Feld zu schreiben:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zipMemoryStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zipStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ZipOutputStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipMemoryStream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;IsStreamOwner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// Prevent from disposing the memory stream&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;zipEntry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ZipEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myFilename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;ntfsExtraData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CreateNtfsExtraField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;creationTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lastAccessTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChangedAt&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;zipEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExtraData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ntfsExtraData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;zipStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PutNextEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipEntry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fileStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyToAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zipStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;fileStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;CreateNtfsExtraField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;creationTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;lastAccessTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// NTFS extra field structure:&lt;/span&gt;
    &lt;span class="c1"&gt;// [Header ID (2 bytes)][Data Size (2 bytes)][Reserved (4 bytes)][Tag (2 bytes)][Content Size (2 bytes)][Timestamps (24 bytes)]&lt;/span&gt;

    &lt;span class="c1"&gt;// Constants&lt;/span&gt;
    &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;headerId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x000A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// NTFS field&lt;/span&gt;
    &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0x0001&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Attribute Tag - "Standard information"&lt;/span&gt;

    &lt;span class="c1"&gt;// Time conversion: DateTime -&amp;gt; Windows FILETIME (ticks since 1601-01-01)&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;creationFileTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;creationTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFileTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;accessFileTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lastAccessTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFileTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;modifiedFileTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modifiedTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToFileTimeUtc&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BinaryWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Write Header ID and Size Placeholder&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="m"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Data Size: Reserved(4) + Tag(2) + Size(2) + 24 bytes&lt;/span&gt;

        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Reserved (4 bytes)&lt;/span&gt;

        &lt;span class="c1"&gt;// Attribute Tag&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Content Size (24 bytes for 3 timestamps)&lt;/span&gt;

        &lt;span class="c1"&gt;// Write the timestamps (each 8 bytes)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifiedFileTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accessFileTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creationFileTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Und voilà: Die Metadaten sind wie gewünscht :-)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fitdfwp2fybq66ehe63mk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fitdfwp2fybq66ehe63mk.png" alt="Angepasste Metadaten" width="284" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Hinweis: "Letzter Zugriff" ändert sich beim Öffnen des Metadaten-Dialogs; vorher passt es ;) )&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>zip</category>
      <category>download</category>
      <category>aspnet</category>
    </item>
    <item>
      <title>Hallo RabbitMQ - 2. Routing</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Tue, 20 Dec 2022 08:22:29 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/hallo-rabbitmq-2-routing-5gfh</link>
      <guid>https://dev.to/gabrielweidmann/hallo-rabbitmq-2-routing-5gfh</guid>
      <description>&lt;p&gt;Im nächsten Schritt geht es jetzt darum ein Gesamtkonzept für das aktuelle System zu finden. Dieses umfasst folgende Komponenten (abstrahiert):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Die UI-Applikation, über welche die Benutzer Daten anzeigen, erstellen, ändern und löschen können.&lt;/li&gt;
&lt;li&gt;Eine Dokumenten-API, über welche Dokumente angelegt, gelöscht und bearbeitet werden können. Außerdem kann jedes Dokument 1-n Versionen enthalten&lt;/li&gt;
&lt;li&gt;Ein Renderservice, der zu jeder Dokumentversion eine Vorschau rendert&lt;/li&gt;
&lt;li&gt;Ein Backupservice, der die erste Version jedes Dokumentes nochmal extra wegspeichert&lt;/li&gt;
&lt;li&gt;Ein Synchronisationsservice, der jede Dokumentänderung mit einem externen System synchronisiert&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aktuell unterteilen sich die Event-Producer und Consumer in folgende Gruppen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr19um8bauv8go6h89ox6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr19um8bauv8go6h89ox6.png" alt="Übersicht Producer und Consumer" width="800" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Um diese Struktur umzusetzen, bietet RabbitMQ einige weitere Möglichkeiten an, um unsere Queues aus dem ersten Teil flexibler zu machen:&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rabbitmq.com/tutorials/tutorial-four-dotnet.html" rel="noopener noreferrer"&gt;https://www.rabbitmq.com/tutorials/tutorial-four-dotnet.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Für das Routing bietet RabbitMQ diverse Möglichkeiten an, um unsere Queues zu befüllen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct exchange
&lt;/h3&gt;

&lt;p&gt;Aus Teil 1 kennen wir ja bereits die Bindings mit dem &lt;code&gt;RoutingKey&lt;/code&gt;. Direct exchange bedeutet also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Finde alle Queues mit dem gleichen Key wie die Nachricht und schreibe sie dort hinein.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq55cy2mqblzz7ryhhbu8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq55cy2mqblzz7ryhhbu8.png" alt="Beispiel Direct Exchange" width="408" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Es dürfen natürlich auch mehrere Queues mit demselben BindingKey vorhanden sein:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdcv4u1g4ursz898sbv77.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdcv4u1g4ursz898sbv77.png" alt="Mehrere Queues mit gleichem BindingKey" width="398" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Topic Exchange
&lt;/h3&gt;

&lt;p&gt;Topic Exchange ist ähnlich zum Direct exchange, bietet aber die Möglichkeit mehrere Kriterien zu verwenden.&lt;/p&gt;

&lt;p&gt;Ein Topic ist ein Textwert der 1 oder mehrere durch &lt;code&gt;.&lt;/code&gt; getrennte Wörter, sowie ggf. Platzhalter enthält. Ohne Platzhalter verhält sich Topic Exchange exakt wie Direct Exchange.&lt;/p&gt;

&lt;p&gt;Es gibt nur zwei Platzhalter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt; (Stern) steht für genau 1 beliebiges Wort&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#&lt;/code&gt; (Raute) steht für 0 bis n Wörter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wir könnten also z.B. die Konvention einführen, dass Binding Keys folgendes Format haben müssen:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;Kontext&amp;gt;.&amp;lt;Aktion&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Es wäre in unserem Fall also möglich z.B. die Topic &lt;code&gt;version.created&lt;/code&gt;, sowie &lt;code&gt;document.*&lt;/code&gt; zu verwenden.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjmegvt0rp2alr69ee1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvjmegvt0rp2alr69ee1l.png" alt="Beispiel Topic Binding Key" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alle Änderungen an einem Dokument gelangen immer an die UI, damit diese sich ggf. aktualisieren kann&lt;/li&gt;
&lt;li&gt;Alle neuen Versionen gehen an die Render-Queue, damit eine neue Vorschau gerendert wird&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wichtig:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Der Binding Key muss trotzdem genau gematched werden, sonst gibt es keinen Treffer. &lt;code&gt;document&lt;/code&gt; oder &lt;code&gt;document.created.today&lt;/code&gt; wären somit von obigen Filtern nicht erkannt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#&lt;/code&gt; als Filter matched alles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Umsetzungsmöglichkeit
&lt;/h2&gt;

&lt;p&gt;Fügen wir jetzt die Teile mit den neuen Möglichkeiten zusammen, könnte sich folgendes Bild ergeben.&lt;/p&gt;

&lt;p&gt;Topics&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;document

&lt;ul&gt;
&lt;li&gt;created&lt;/li&gt;
&lt;li&gt;restored&lt;/li&gt;
&lt;li&gt;deleted&lt;/li&gt;
&lt;li&gt;metadata_changed&lt;/li&gt;
&lt;li&gt;version.created&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;preview

&lt;ul&gt;
&lt;li&gt;created&lt;/li&gt;
&lt;li&gt;failed&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbcf2p9c87fgp2tv24vd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbcf2p9c87fgp2tv24vd.png" alt="Beispiel-Architektur der Queues mit Topics" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Beispielprojekt
&lt;/h3&gt;

&lt;p&gt;Ich habe mal versucht das beispielhaft mit dem minimalen Setup umzusetzen. Das vollständige Projekt findet man hier: &lt;a href="https://github.com/weidmanngabriel/rabbitmq_topics_poc" rel="noopener noreferrer"&gt;https://github.com/weidmanngabriel/rabbitmq_topics_poc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Man kann jetzt einfach auf der Kommandozeile eingeben welches Event die Dokument-API abschicken soll. Die Events verhalten sich dann so wie im obigen Bild definiert.&lt;/p&gt;




&lt;p&gt;Mit diesem Wissen kommen wir schonmal ein ganzes Stück weiter. Im nächsten Teil können wir uns dann Details wie Sende- und Empfangsbestätigung, Callbacks, etc. ansehen.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Hallo RabbitMQ - 1. Die Basics</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Fri, 16 Dec 2022 16:11:17 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/hallo-rabbitmq-1-die-basics-142l</link>
      <guid>https://dev.to/gabrielweidmann/hallo-rabbitmq-1-die-basics-142l</guid>
      <description>&lt;p&gt;Aktuell wage ich mich für ein Firmenprojekt &lt;a href="https://www.rabbitmq.com/" rel="noopener noreferrer"&gt;RabbitMQ&lt;/a&gt; heran. Einfach gesagt ist RabbitMQ eine Serveranwendung über die Nachrichten zwischen verschiedenen Sendern und Empfängern ausgetauscht werden können. In diesem Post möchte ich meine kleine Reise vom absoluten Neuling bis ich das Ganze verstanden habe etwas dokumentieren.&lt;/p&gt;

&lt;p&gt;Sehr hilfreich fand ich dabei dieses Tutorial: &lt;a href="https://www.cloudamqp.com/blog/part1-rabbitmq-for-beginners-what-is-rabbitmq.html" rel="noopener noreferrer"&gt;https://www.cloudamqp.com/blog/part1-rabbitmq-for-beginners-what-is-rabbitmq.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Warum RabbitMQ?
&lt;/h2&gt;

&lt;p&gt;Man sollte Code einfach schreiben und Programme einfach denken, denn kompliziert werden sie von alleine. Das habe ich - wie wohl jeder andere auch - in meiner Arbeit schon oft erlebt. Da auch in meinen aktuellen Projekten der Bedarf nach neuen Features hoch ist und immer mehr Teilsysteme dazukommen, haben wir uns im Team überlegt wie wir das gerne in Zukunft besser handhaben wollen. Dabei ist eine tolle Gesamtstruktur für die Projekte herausgekommen.&lt;/p&gt;

&lt;p&gt;Herzstück der neuen Struktur sind einerseits eine schmale API für die Kernfunktionen des Produkts, andererseits eine einheitliche Kommunikation zwischen den Systemen über einen MessageBus.&lt;/p&gt;

&lt;p&gt;Wir haben überlegt und evaluiert welcher MessageBus für uns in Frage kommt und RabbitMQ hat sich angeboten, da wir die Flexibilität in den Kommunikationswegen schätzen (In-Memory, TCP, Http) und vor allem da es ein etablierter Industriestandard ist und es so sehr viele Anleitungen, Tutorials und Problemlösungen zu allen gängigen Aufgabenstellungen und Problemen gibt.&lt;/p&gt;

&lt;p&gt;Grundsätzlich erhoffen wir uns, dass unsere Infrastruktur weiter wachsen kann, ohne dass die Komplexität allzu stark zunimmt, indem wir Teilbereiche entkoppeln, Domainwissen und -daten an zentralen Stellen anbieten und die einzelnen Bausteine überschaubar halten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coole Features von RabbitMQ
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Eine Queue kann von mehreren Konsumenten abgearbeitet werden, oder es können verschiedene Queues für beliebig viele Konsumenten identisch befüllt werden. Das ermöglicht z.B. ein asynchrones Abarbeiten von Renderaufträgen.&lt;/li&gt;
&lt;li&gt;Wie bereits erwähnt kann man seine RabbitMQ sogar rein In-Memory (z.B. mit Mediator) aufsetzen&lt;/li&gt;
&lt;li&gt;RabbitMQ ist &lt;strong&gt;super schnell&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Durch Plugins erweiterbar (z.B. UI-Manager, Streams)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation mit dem Windows Installer
&lt;/h2&gt;

&lt;p&gt;Hinweis: &lt;code&gt;XXX&lt;/code&gt; steht für irgendeine Versionsnummer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Erlang (Programmiersprache) installieren: &lt;a href="https://www.erlang.org/downloads" rel="noopener noreferrer"&gt;https://www.erlang.org/downloads&lt;/a&gt;&lt;br&gt;
a. Unbedingt &lt;strong&gt;als Admin installieren&lt;/strong&gt;&lt;br&gt;
b. Benötigt einen PC Neustart&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RabbitMQ herunterladen und installieren: &lt;a href="https://github.com/rabbitmq/rabbitmq-server/releases" rel="noopener noreferrer"&gt;https://github.com/rabbitmq/rabbitmq-server/releases&lt;/a&gt; (&lt;code&gt;rabbitmq-server-XXX.exe&lt;/code&gt;&lt;br&gt;
)&lt;br&gt;
a. Anleitung hier: &lt;a href="https://www.rabbitmq.com/install-windows.html" rel="noopener noreferrer"&gt;https://www.rabbitmq.com/install-windows.html&lt;/a&gt;&lt;br&gt;
b. Ebenfalls &lt;strong&gt;als Admin installieren&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;RabbitMQ UI aktivieren&lt;br&gt;
a. &lt;code&gt;RabbitMQ Command Prompt&lt;/code&gt; öffnen -&amp;gt; Ordner ist &lt;code&gt;...\RabbitMQ Server\rabbitmq_server-XXX\sbin&lt;/code&gt;&lt;br&gt;
b. Befehl ausführen: &lt;code&gt;rabbitmq-plugins enable rabbitmq_management&lt;/code&gt;&lt;br&gt;
c. Service neu starten mit &lt;code&gt;rabbitmq-service.bat stop&lt;/code&gt; und anschließend &lt;code&gt;rabbitmq-service.bat start&lt;/code&gt;&lt;br&gt;
d. Nach ca. 1 Minute ist die UI nun unter &lt;a href="http://localhost:15672" rel="noopener noreferrer"&gt;http://localhost:15672&lt;/a&gt; erreichbar. Benutzer: &lt;code&gt;guest&lt;/code&gt;, Passwort: &lt;code&gt;guest&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remotezugriff aktivieren&lt;br&gt;
a. Tab &lt;code&gt;Admin&lt;/code&gt; in der UI öffnen&lt;br&gt;
b. Einen neuen Benutzer hinzufügen. &lt;strong&gt;Wichtig&lt;/strong&gt;: Tag muss &lt;code&gt;administrator&lt;/code&gt; sein.&lt;br&gt;
c. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxcdmhee5zgwcw4w8i9u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxcdmhee5zgwcw4w8i9u.png" alt="Ansicht der Benutzereinstellungen im RabbitMQ Admin Interface" width="671" height="372"&gt;&lt;/a&gt;&lt;br&gt;
d. Jetzt muss man noch dem Admin-Benutzer die Berechtigung zum Zugriff geben. Dazu auf den Namen klicken und einfach die voreingestellten Berechtigungen hinzufügen&lt;br&gt;
e. &lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmb9uk54jm38yjmhguqu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmb9uk54jm38yjmhguqu1.png" alt="Berechtigungen hinzufügen" width="591" height="559"&gt;&lt;/a&gt;&lt;br&gt;
f. Remotezugriff mit diesem Benutzer ist jetzt möglich. Dazu &lt;code&gt;Serverurl/15627&lt;/code&gt; aufrufen, z.B. &lt;a href="http://localhost:15672/" rel="noopener noreferrer"&gt;http://localhost:15672/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Grundkonzept
&lt;/h2&gt;

&lt;p&gt;Auf &lt;a href="http://tryrabbitmq.com/" rel="noopener noreferrer"&gt;TryRabbitMQ&lt;/a&gt; kann man sehr schön sehen wie sich RabbitMQ in den Grundzügen verhält:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5u2820slgkaocq6iv6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy5u2820slgkaocq6iv6y.png" alt="Beispiel mit verschiedenen Elementen aus TryRabbitMQ.com" width="510" height="285"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Die Message
&lt;/h3&gt;

&lt;p&gt;Eine Message ist ein Bytestream der irgendwie versendet wird. Das kann natürlich ein konvertierter String, oder ein Objekt im JSON-Format sein. Die Nachricht interessiert sich nicht für das Transportmittel, sondern kapselt nur ihre Informationen.&lt;/p&gt;

&lt;p&gt;Wichtig ist, dass eine Message Metadaten wie z.B. einen Binding-Key enthalten kann, der als Routing-Information im Exchange genutzt werden kann.&lt;/p&gt;

&lt;h3&gt;
  
  
  Der Producer
&lt;/h3&gt;

&lt;p&gt;Ein Producer ist irgendein Client der eine Nachricht an den Server übermittelt. &lt;/p&gt;

&lt;h3&gt;
  
  
  Die Connection
&lt;/h3&gt;

&lt;p&gt;Es wird in den meisten Setups eine TCP-Verbindung zwischen Client und Server aufgebaut. Als Faustregel gilt: Jede Applikation sollte nur 1 Connection offen haben, da diese ressorcenintensiv sind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Der Channel
&lt;/h3&gt;

&lt;p&gt;Je Connection können mehrere Channels angelegt werden, über die der Client mit dem Server kommunizieren kann. Als Faustregel gilt hier, dass jeder Task seinen eigenen Task haben sollte, da diese üblicherweise nicht Multithreading-sicher implementiert sind.&lt;/p&gt;

&lt;p&gt;Alle offenen Channel schließen sich automatisch, wenn die Connection geschlossen wird.&lt;/p&gt;

&lt;h3&gt;
  
  
  Der Broker
&lt;/h3&gt;

&lt;p&gt;Der Broker ist der Nachrichtenserver in seiner Gesamtheit. Das kann ein richtiger Server sein, oder auch "nur" eine In-Memory Routing-Klasse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Der Exchange
&lt;/h3&gt;

&lt;p&gt;Der Exchange hat die Aufgabe die empfangenen Nachrichten den richtigen Queues zuzuordnen. Dazu hat er verschiedene Grundkonfigurationen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct&lt;/strong&gt;: Die Message wird nur an Queues weitergeleitet deren Binding-Key exakt dem der Nachricht entspricht&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fanout&lt;/strong&gt;: Die Message wird an alle verbundenen Queues weitergeleitet, ganz unabhängig vom Binding-Key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic&lt;/strong&gt;: Benutzt Wildcards, um ein Pattern-Matching durchzuführen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headers&lt;/strong&gt;: Spezialfall bei dem die Message-Header fürs Routing genutzt werden&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Die Queue
&lt;/h3&gt;

&lt;p&gt;Ein Buffer der Nachrichten zwischenspeichert. Hierbei gilt im allgemeinen &lt;a href="https://de.wikipedia.org/wiki/First_In_%E2%80%93_First_Out" rel="noopener noreferrer"&gt;FIFO&lt;/a&gt;. Es gibt aber auch z.B. die Möglichkeit einer Priorisierung.&lt;/p&gt;

&lt;p&gt;Wichtig ist zu wissen, dass jede Nachricht nur 1x aus der Queue geholt werden kann (es sei denn das Abholen läuft auf ein Timeout hinaus, dann wir Requeued). D.h. wenn mehrere Consumer auf dieselbe Queue zugreifen, wird jede Nachricht nur an genau 1 Consumer weitergereicht. Möchte man eine Nachricht an mehrere Consumer verteilen, benötigt jeder Consumer seine eigene Queue in welche die Nachricht jeweils dupliziert wird.&lt;/p&gt;

&lt;h3&gt;
  
  
  Der Consumer
&lt;/h3&gt;

&lt;p&gt;Ein oder mehrere Consumer fordern die Daten von der Queue an. Sobald eine Nachricht erhalten wurde (Standard), bzw. bestätigt wurde, wird sie aus der Queue gelöscht. Bis dahin ist sie für andere Konsumenten unsichtbar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mehr ...
&lt;/h3&gt;

&lt;p&gt;Das sind nur die absoluten Basics, es gibt diverse Einstellungen und Möglichkeiten. Es empfiehlt sich einerseits &lt;a href="https://www.rabbitmq.com/getstarted.html" rel="noopener noreferrer"&gt;die Tutorials zu machen&lt;/a&gt;, andererseits die &lt;a href="https://www.rabbitmq.com/documentation.html" rel="noopener noreferrer"&gt;Dokumentation zu studieren&lt;/a&gt;. Hier werden viele Fragen gut erklärt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Das RabbitMQ Management Interface
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr66lx71p8h4tu2b6hj5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr66lx71p8h4tu2b6hj5h.png" alt="Beispielansicht des Admin Interface" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Das Interface hilft dabei zu verstehen wie die Producer, Exchanges, Queues und Consumer zusammenarbeiten ... oder auch nicht arbeiten. Es ist im allgemeinen unter &lt;a href="http://localhost:15672/" rel="noopener noreferrer"&gt;http://localhost:15672/&lt;/a&gt;, bzw. mit dem Servernamen erreichbar.&lt;/p&gt;

&lt;p&gt;Es ist gut zu wissen, dass man über &lt;strong&gt;Queues&lt;/strong&gt; auch von hier aus Exchanges und Queues anlegen kann. Das geht auch über den Code, aber wahrscheinlich macht es am meisten Sinn alle dauerhaften Dinge hierüber einzurichten und alles weitere bei Bedarf über den Code.&lt;/p&gt;

&lt;p&gt;Das Ganze habe ich mir noch nicht in der Tiefe angeschaut. Interessant könnten aber z.B. die User und Virtual Hosts sein. Damit sollte es möglich sein abgegrenzte Bereiche zu definieren in denen Benutzern Lese- und Schreibrechte vergeben werden können. Ist aber zum jetzigen Zeitpunkt noch nicht relevant.&lt;/p&gt;

&lt;p&gt;Für mehr Infos empfehle ich den Folgepost (zu dem oben) von cloudamqp: &lt;a href="https://www.cloudamqp.com/blog/part3-rabbitmq-for-beginners_the-management-interface.html" rel="noopener noreferrer"&gt;https://www.cloudamqp.com/blog/part3-rabbitmq-for-beginners_the-management-interface.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Die Connection einrichten
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConnectionFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HostName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://localhost:15672/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;VirtualHost&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateConnection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hinweis:&lt;/strong&gt; Wie bereits erwähnt sollte es nur 1 Connection je Applikation geben. Sie kann also z.B. als Singleton deklariert werden, wenn man DI nutzt.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Den Channel öffnen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateModel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hinweis:&lt;/strong&gt; Empfehlung ist 1 Channel je Task.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Eine Queue deklarieren
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueueDeclare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"letterbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exclusive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hier gibt es viel zu erklären:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;queue&lt;/strong&gt;: Der Name der Queue. Wird er leergelassen, wird der Name vom Server generiert&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;durable&lt;/strong&gt;: Soll die Queue auf die Platte gespiegelt werden, damit im Falle eines Serverneustarts die Daten nicht verloren sind? Bei &lt;code&gt;false&lt;/code&gt; wird alles nur im RAM gehalten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exclusive&lt;/strong&gt;: Die Queue wird gelöscht, wenn die einzige Connection mit Zugriff darauf geschlossen wird&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;autoDelete&lt;/strong&gt;: Die Queue wird gelöscht, sobald es keinen aktiven Konsumenten mehr gibt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;arguments&lt;/strong&gt;: Weitere Features und Plugins nutzen diese&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wie weiter oben beschrieben halte ich es durchaus für sinnvoll, wenn man dauerhafte Queues über das Admin-Interface definiert. Es ist aber sehr cool, wenn ein Service z.B. eine exklusive Queue mit einem BindingKey anlegt und alle neuen Nachrichten dann immer auch in diese Queue geschrieben werden. Wird der Service dann gestoppt, verschwindet die Queue auch sofort wieder mitsamt der Daten.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Eine Nachricht versenden
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"This is my message number "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"letterbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Der erste Wert wäre der Name des &lt;code&gt;Exchange&lt;/code&gt; den man ansprechen will. Mit Leerstring wird der Default-Exchange verwendet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;letterbox&lt;/code&gt; ist der Name des Routings, bzw. in dem Fall der Queue&lt;/li&gt;
&lt;li&gt;Der &lt;code&gt;body&lt;/code&gt; ist ein Byte-Array oder -Stream&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Die Nachricht empfangen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EventingBasicConsumer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BasicConsume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"letterbox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;autoAck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Nachricht aus der Queue löschen, sobald sie erhalten wurde&lt;/span&gt;
    &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Received&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Nachricht erhalten: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Praktische Anwendungsszenarien
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Dokument-Rendition
&lt;/h2&gt;

&lt;p&gt;Eine Anforderung ist, dass alle eingestellten Dokumente automatisch eine Vorschau gerendert bekommen. Der Aufbau mit der Queue könnte so aussehen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhs8qp1u38e3ictranp7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhs8qp1u38e3ictranp7r.png" alt="Beispielaufbau der MessageQueue für den RenditionService" width="404" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An einer Stelle im System (in diesem Fall der &lt;code&gt;DocumentService&lt;/code&gt;) wird ein neues Dokument eingestellt&lt;/li&gt;
&lt;li&gt;Eine Nachricht mit den Dokument-Metadaten (z.B. Id, Speicherort) wird an den Broker geschickt&lt;/li&gt;
&lt;li&gt;Der Exchange leitet die Nachricht an die Queue weiter, wenn sie den Tag &lt;code&gt;rendition-task&lt;/code&gt; gesetzt hat.&lt;/li&gt;
&lt;li&gt;Ein RenditionWorker kann sich den Task aus der Queue ziehen, sobald er Kapazität hat&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  UI-Aktualisierung
&lt;/h1&gt;

&lt;p&gt;Eine weitere Anforderung ist, dass sich alle Ansichten aktualisieren müssen, in denen das Dokument zu sehen sein wird. Das könnte dann so aussehen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2ja2rg4oho7xvxdd0zy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn2ja2rg4oho7xvxdd0zy.png" alt="Beispielaufbau-Erweiterung um die UiNotificationQueues" width="513" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Der Service benachrichtigt über &lt;code&gt;document-changed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Jede Queue die den Key hinterlegt hat, bekommt die Nachricht&lt;/li&gt;
&lt;li&gt;Die &lt;code&gt;renditionQueue&lt;/code&gt; gibt die Nachricht nur an 1 ServiceWorker&lt;/li&gt;
&lt;li&gt;Jede der &lt;code&gt;uiNotifQueues&lt;/code&gt; bekommt eine eigene Nachrichtenkopie&lt;/li&gt;
&lt;li&gt;Die Nachricht wird an den jeweiligen Consumer weitergeleitet. Auf diese Weise könnten auch 100te Consumer ihre jeweils eigene (temporäre!) Queue eröffnen&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Soweit mal zu den Basics. Ich denke, dass man damit schon einiges anfangen kann. Im nächsten Teil werde ich dann mehr in die Details reinschauen.&lt;/p&gt;

</description>
      <category>rabbitmq</category>
      <category>csharp</category>
      <category>beginners</category>
      <category>german</category>
    </item>
    <item>
      <title>Show local webpage in Xamarin.Forms WebView</title>
      <dc:creator>Gabriel Weidmann</dc:creator>
      <pubDate>Wed, 01 Dec 2021 09:48:19 +0000</pubDate>
      <link>https://dev.to/gabrielweidmann/show-local-webpage-in-xamarinforms-webview-15d1</link>
      <guid>https://dev.to/gabrielweidmann/show-local-webpage-in-xamarinforms-webview-15d1</guid>
      <description>&lt;h6&gt;
  
  
  A local webpage extracted from an online webpage that runs on HTML, CSS and JavaScript.
&lt;/h6&gt;

&lt;p&gt;For my current &lt;a href="https://apps.maschinenring.de/meinacker/" rel="noopener noreferrer"&gt;mobile app project&lt;/a&gt; I needed to embed the field map from an &lt;a href="https://portal.maschinenring.de/" rel="noopener noreferrer"&gt;website &lt;/a&gt; into my field management mobile app. Here you can see how it looks at the end of the process on both platforms:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0q5g51h83i1ymqnm7fv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw0q5g51h83i1ymqnm7fv.png" alt="An image of the resulting map in my app" width="513" height="962"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Complex layouts and functions are often a problem in Xamarin.Forms … but they are great when they finally work 😇
&lt;/h6&gt;

&lt;p&gt;There were multiple problems on the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The webpage is running on HTML, CSS and JavaScript and executing custom code. We didn’t want to rewrite it in parallel in the web and the app, but to reuse the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we shrinked down the web version to a local standalone page running in the webbrowser that can be displayed by simple opening the index.html file. For displaying it we would need to open the index file with the Xamarin.Forms WebView and make it load the other files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The webview is not easy to understand, because it’s cross-plattform but uses the native controls on each. It was hard to get it working on Android, but I didn’t make it on iOS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It didn’t want to load the files from the assets, so I placed them in the shared project, set them as “Embedded Resource”, copied them at runtime to a sub folder in the cache and set this as the base path. Then I would load the index.html directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ap59it4c1q2btz27san.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ap59it4c1q2btz27san.png" alt="Code snippet of how to load embedded resources" width="700" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  When the files are copied to [CacheDirectory]/Resources/AckerMap I set this directory as the base for the webview to search the other files referenced.
&lt;/h6&gt;

&lt;p&gt;I got it working on android, but iOS was crashing and I still don’t know why.&lt;/p&gt;

&lt;p&gt;So I was trying something else: If the webview doesn’t want to load additional files correctly, why not put everything into a single file? After a short search I found the Inliner tool. It takes your webpage and tries to combine all of the assets in a single file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F061ir2rdwl9cp9jmk4qm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F061ir2rdwl9cp9jmk4qm.png" alt="Github repo of inliner" width="700" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  From &lt;a href="https://github.com/remy/inliner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;
&lt;/h6&gt;

&lt;p&gt;So I made sure my webpage runs well in my local browser and then used this tool to create the single file. It’s quite easy:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpssarld82sc5wbtt7s8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpssarld82sc5wbtt7s8i.png" alt="Install command" width="193" height="32"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Install the tool over NPM
&lt;/h6&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkt8mh1m9ecipsdz4gln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvkt8mh1m9ecipsdz4gln.png" alt="Inliner command" width="700" height="36"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  Just specify the base html file of your webpage and the output file.
&lt;/h6&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20ic7x3pak5irvmnserp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20ic7x3pak5irvmnserp.png" alt="The inliner cmd output" width="296" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h6&gt;
  
  
  And the output tells me everything is alright :-)
&lt;/h6&gt;

&lt;p&gt;At last I just needed to put the file in my project, set it as “Embedded Resource” and load it directly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft6sf9uzy9vevnaq24vnl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft6sf9uzy9vevnaq24vnl.png" alt="New embedded resource loading" width="700" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now 🥳 it works out of the box on Android &amp;amp; iOS 🎉💯&lt;/p&gt;

&lt;p&gt;Have a nice day 😉&lt;/p&gt;

</description>
      <category>html</category>
      <category>convert</category>
      <category>script</category>
    </item>
  </channel>
</rss>
