<?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: Angelo Pirola</title>
    <description>The latest articles on DEV Community by Angelo Pirola (@angelodotnet).</description>
    <link>https://dev.to/angelodotnet</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1072918%2F4c8e5e98-77cf-4cb4-a7e9-17c9dafd6045.jpeg</url>
      <title>DEV Community: Angelo Pirola</title>
      <link>https://dev.to/angelodotnet</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/angelodotnet"/>
    <language>en</language>
    <item>
      <title>TinyLocalization: database-backed translations for .NET, without redeploying</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Tue, 03 Nov 2026 22:45:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/tinylocalization-database-backed-translations-for-net-without-redeploying-2nd1</link>
      <guid>https://dev.to/angelodotnet/tinylocalization-database-backed-translations-for-net-without-redeploying-2nd1</guid>
      <description>&lt;p&gt;Every .NET developer has been there. A customer finds a typo in the Italian translation, and fixing it means editing a &lt;code&gt;.resx&lt;/code&gt; file, rebuilding, and redeploying the whole application. For one word.&lt;/p&gt;

&lt;p&gt;I wanted translations to behave like data, not like code. So I built &lt;strong&gt;TinyLocalization&lt;/strong&gt;, a small open-source library that stores your translations in a database and plugs into the standard &lt;code&gt;Microsoft.Extensions.Localization&lt;/code&gt; abstractions.&lt;/p&gt;

&lt;p&gt;The repository is here: &lt;a href="https://github.com/AngeloDotNet/TinyDatabaseLocalization" rel="noopener noreferrer"&gt;AngeloDotNet/TinyDatabaseLocalization&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The idea is deliberately simple: you keep using &lt;code&gt;IStringLocalizer&lt;/code&gt;, but the strings come from a database table instead of resource files. You can add, fix or remove a translation at runtime, and the change is live without a redeploy.&lt;/p&gt;

&lt;p&gt;Main features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard API:&lt;/strong&gt; works with &lt;code&gt;IStringLocalizer&lt;/code&gt;, &lt;code&gt;IStringLocalizer&amp;lt;T&amp;gt;&lt;/code&gt; and &lt;code&gt;IStringLocalizerFactory&lt;/code&gt;, so existing code does not need to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Culture fallback:&lt;/strong&gt; current culture, then parent cultures, then an optional global fallback, then the invariant culture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format arguments:&lt;/strong&gt; &lt;code&gt;localizer["Hello {0}", name]&lt;/code&gt; works as you would expect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four database providers:&lt;/strong&gt; SQLite, SQL Server, PostgreSQL and MySQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FusionCache under the hood:&lt;/strong&gt; fail-safe, timeouts, eager refresh and stampede protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional Redis:&lt;/strong&gt; as a second-level cache and as a backplane for multi-instance invalidation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One simple table:&lt;/strong&gt; &lt;code&gt;Translations&lt;/code&gt;, unique on &lt;code&gt;(Resource, Key, Culture)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;The library targets &lt;strong&gt;.NET 10&lt;/strong&gt; and is built on &lt;strong&gt;Entity Framework Core&lt;/strong&gt;. Caching is handled by &lt;strong&gt;FusionCache&lt;/strong&gt;, which I picked over a hand-rolled &lt;code&gt;IMemoryCache&lt;/code&gt; wrapper for one reason: caching translations looks trivial until you meet cache stampedes, slow databases and multi-node invalidation. FusionCache already solved those problems, so I did not have to.&lt;/p&gt;

&lt;p&gt;Tests are written with xUnit v3 and run against a real SQLite database and a real FusionCache instance, not mocks. Run them with &lt;code&gt;dotnet test&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a lookup works
&lt;/h2&gt;

&lt;p&gt;This is the part I like most, because it is short:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;EfStringLocalizer&lt;/code&gt; builds the culture chain, for example &lt;code&gt;it-IT → it → global fallback → invariant&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For each culture, it asks FusionCache for the snapshot of that &lt;code&gt;(resource, culture)&lt;/code&gt; pair.&lt;/li&gt;
&lt;li&gt;On a cache miss, &lt;code&gt;LocalizationDbContext&lt;/code&gt; runs &lt;strong&gt;one query&lt;/strong&gt; that loads all keys of that resource and culture, and caches the result.&lt;/li&gt;
&lt;li&gt;The first culture in the chain that contains the key wins.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two details make a real difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loading the whole resource/culture in one query means you do not hit the database once per string.&lt;/li&gt;
&lt;li&gt;Empty cultures are cached too (negative caching). If you have no &lt;code&gt;fr&lt;/code&gt; translations, you do not query for &lt;code&gt;fr&lt;/code&gt; on every request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package TinyLocalization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the services:&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="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddLocalizationDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LocalizationDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DatabaseType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DatabaseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SQLite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Data Source=localization.db"&lt;/span&gt;&lt;span class="p"&gt;;&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDefaultFusionCache&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="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDatabaseLocalization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&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;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GlobalFallbackCulture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"en"&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;Then configure request localization in your pipeline as you normally would, and use the localizer:&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;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HomeController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IStringLocalizer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HomeController&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;localizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IActionResult&lt;/span&gt; &lt;span class="nf"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ViewData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Title"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localizer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;ViewData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Greeting"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;localizer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Hello {0}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Identity&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Name&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;View&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;The resource name for &lt;code&gt;IStringLocalizer&amp;lt;T&amp;gt;&lt;/code&gt; is the full name of &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A fair warning about the schema: the library ships &lt;strong&gt;no migrations&lt;/strong&gt;. For a quick start, &lt;code&gt;EnsureCreated()&lt;/code&gt; is fine. For production, create migrations in your own project and point the &lt;code&gt;MigrationsAssembly&lt;/code&gt; option at them. I preferred this to forcing my migration history into your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing translations at runtime
&lt;/h2&gt;

&lt;p&gt;Translations are edited through &lt;code&gt;ILocalizationManager&lt;/code&gt;, which writes to the database and invalidates the cache in one go:&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;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOrUpdateAsync&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;Translation&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"MyApp.Controllers.HomeController"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Culture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Benvenuto"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoveAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyApp.Controllers.HomeController"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Welcome"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"it"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvalidateResourceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyApp.Controllers.HomeController"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An empty culture string means the invariant culture. Concurrent inserts of the same translation are resolved as updates, so two admins saving the same key at the same time will not crash anything.&lt;/p&gt;

&lt;p&gt;One thing to remember: if you edit rows &lt;strong&gt;directly in the database&lt;/strong&gt;, the application will not see the change until the cache entry expires (10 minutes by default) or you call &lt;code&gt;InvalidateResourceAsync&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling out with Redis
&lt;/h2&gt;

&lt;p&gt;With a single instance, the in-memory cache is enough. With several instances behind a load balancer, you want an edit on node A to reach node B immediately. For that, use &lt;code&gt;AddDistributedFusionCache&lt;/code&gt; (or &lt;code&gt;AddDistributedLogsFusionCache&lt;/code&gt; if you want control over FusionCache log levels) and set &lt;code&gt;RedisCacheConnectionString&lt;/code&gt; and &lt;code&gt;RedisBackplaneConnectionString&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you need to tell other systems that translations changed, implement &lt;code&gt;ICacheInvalidationPublisher&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sensible defaults
&lt;/h2&gt;

&lt;p&gt;Everything is configurable, but the defaults are meant to be safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache duration: 10 minutes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FallbackToParentCultures&lt;/code&gt;: enabled&lt;/li&gt;
&lt;li&gt;Missing key: returns the key itself (or an empty string if you set &lt;code&gt;ReturnKeyIfNotFound = false&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fail-safe: enabled, with a 120-minute maximum stale duration, so a database outage does not take your UI strings down with it&lt;/li&gt;
&lt;li&gt;Factory timeouts: 100 ms soft, 1500 ms hard&lt;/li&gt;
&lt;li&gt;Malformed format strings stored in the database are returned unchanged instead of throwing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one comes from experience: a typo in a translated string should never become a 500 error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is missing
&lt;/h2&gt;

&lt;p&gt;The project is young, so let me be honest about the gaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No admin UI for editing translations. You build your own on top of &lt;code&gt;ILocalizationManager&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No migrations shipped, as explained above.&lt;/li&gt;
&lt;li&gt;No import/export tooling from existing &lt;code&gt;.resx&lt;/code&gt; files yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one of these matters to you, open an issue. Priorities follow real needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;TinyLocalization will not replace &lt;code&gt;.resx&lt;/code&gt; for every project, and that is fine. If your translations change often, are edited by non-developers, or must be fixed without a deploy, keeping them in a database with a solid cache in front is a very practical option, and you keep the standard .NET localization API.&lt;/p&gt;

&lt;p&gt;The project is MIT licensed. If you want to try it, star it, or contribute, fork the repo, branch from &lt;code&gt;develop&lt;/code&gt;, add or update tests, and open a pull request against &lt;code&gt;develop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A special thanks to Jody Donetti, the author of &lt;a href="https://github.com/ZiggyCreatures/FusionCache" rel="noopener noreferrer"&gt;FusionCache&lt;/a&gt;. A big part of this library's resilience is his work.&lt;/p&gt;

&lt;p&gt;And you, where do you keep your translations: &lt;code&gt;.resx&lt;/code&gt;, JSON, or a database?&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>sqlserver</category>
      <category>localization</category>
      <category>fusioncache</category>
    </item>
    <item>
      <title>SagreEventi: an offline-first app for Italian food festivals</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:00:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/sagreeventi-an-offline-first-app-for-italian-food-festivals-5fn5</link>
      <guid>https://dev.to/angelodotnet/sagreeventi-an-offline-first-app-for-italian-food-festivals-5fn5</guid>
      <description>&lt;p&gt;October in Italy means chestnuts and new wine. Every weekend there is a &lt;em&gt;sagra&lt;/em&gt; (a local food festival) somewhere, but finding them means jumping between town websites, social media groups and word of mouth. That is how &lt;strong&gt;SagreEventi&lt;/strong&gt; was born: a place where people who go to these festivals can report them and browse them, without creating an account.&lt;/p&gt;

&lt;p&gt;The code is open source (MIT license): &lt;a href="https://github.com/AngeloDotNet/SagreEventi.SQLServer" rel="noopener noreferrer"&gt;AngeloDotNet/SagreEventi.SQLServer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this article I walk through what the app does, the main technical choices and the points where I stopped to think.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The idea is deliberately simple. Anyone can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;browse&lt;/strong&gt; events: a home page with the upcoming events hanging from a festival bunting, a searchable catalog and a month-by-month agenda;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;filter and sort&lt;/strong&gt; by free text (accent-insensitive), city and period (in progress, upcoming, finished), in a grid or list view;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;report&lt;/strong&gt; a new event (name, city, dates, description) with a validated form;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;take it with them&lt;/strong&gt;: add the event to their calendar (&lt;code&gt;.ics&lt;/code&gt;), open the city on a map, share it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users with the &lt;code&gt;Admin&lt;/code&gt; or &lt;code&gt;Editor&lt;/code&gt; role sign in with email and password (ASP.NET Core Identity, bearer tokens) and can edit existing events and moderate reports. Public registration is disabled: users are created by the seed at start-up.&lt;/p&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; do: it does not sell tickets and does not handle bookings. Keeping payments and ticketing out of scope simplified the project a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ASP.NET Core (.NET 10) with Minimal API&lt;/li&gt;
&lt;li&gt;Entity Framework Core + SQL Server&lt;/li&gt;
&lt;li&gt;ASP.NET Core Identity with roles&lt;/li&gt;
&lt;li&gt;Rate limiting, native OpenAPI, FusionCache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Angular 21 (standalone components, signals, zoneless)&lt;/li&gt;
&lt;li&gt;PrimeNG 21 with a custom Aura preset, PrimeIcons&lt;/li&gt;
&lt;li&gt;Just Angular, PrimeNG and RxJS: no state library, no CSS framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A note on method: the API is a handful of endpoints, so a new client is a weekend of work. There is a single contract and it can serve several clients. The project started with a Blazor WebAssembly client, which I later removed in favor of Angular + PrimeNG.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Angular 21 and not 22?
&lt;/h3&gt;

&lt;p&gt;This choice is worth explaining. The latest PrimeNG release published under the MIT license is 21.1.x, which supports Angular 21. PrimeNG 22 adds Angular 22 support but introduces a license key (with a free Community tier that requires registration). To stay fully free and officially compatible, I pinned the client to Angular 21 and PrimeNG &lt;code&gt;^21.1.10&lt;/code&gt;. Before upgrading, it is worth rereading PrimeTek's current terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moderation: trust reporters a little less
&lt;/h2&gt;

&lt;p&gt;If anyone can add events, you need a filter. Every report has a state: &lt;code&gt;InAttesa&lt;/code&gt; (pending, the default for anonymous users), &lt;code&gt;Approvato&lt;/code&gt; (approved) or &lt;code&gt;Rifiutato&lt;/code&gt; (rejected).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GetEventi&lt;/code&gt; returns &lt;strong&gt;only&lt;/strong&gt; approved events.&lt;/li&gt;
&lt;li&gt;Neither the client nor the server trusts the state sent by a client: the &lt;code&gt;stato&lt;/code&gt; field is output only.&lt;/li&gt;
&lt;li&gt;Whoever reports an event sees it as &lt;em&gt;pending&lt;/em&gt; on their own device (the «Le mie» section). If the event is rejected, the reporter is not notified: it keeps showing as pending. The rejection reason is a note visible only to &lt;code&gt;Admin&lt;/code&gt; and &lt;code&gt;Editor&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Events reported by &lt;code&gt;Admin&lt;/code&gt;/&lt;code&gt;Editor&lt;/code&gt; are published immediately.&lt;/li&gt;
&lt;li&gt;From &lt;code&gt;/gestione/revisione&lt;/code&gt; you can approve or reject reports in bulk (up to 100 at a time) and withdraw an event that was already approved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A detail I like: the API has no &lt;code&gt;DELETE&lt;/code&gt;. A withdrawn event becomes a rejected, concluded event, and clients use that to delete their local copy at the next sync. A background service automatically concludes events whose end date has passed (the interval is configurable with &lt;code&gt;Eventi:ScadenzaIntervalloMinuti&lt;/code&gt;), and a concluded event can no longer be edited by anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline-first and sync
&lt;/h2&gt;

&lt;p&gt;At festivals, especially in the countryside, mobile coverage is often a gamble. That is why the Angular client keeps a local copy of the events (in &lt;code&gt;localStorage&lt;/code&gt;) and reconciles it with SQL Server using a three-step protocol:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Push:&lt;/strong&gt; events created or edited on the device (marked as &lt;em&gt;pending&lt;/em&gt;) are sent with &lt;code&gt;PUT /api/Eventi/UpdateEventi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pull:&lt;/strong&gt; changes made by others are fetched with &lt;code&gt;GET /api/Eventi/GetEventi?since=&amp;lt;cursor&amp;gt;&lt;/code&gt;, where the cursor is the latest modification time returned by the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge:&lt;/strong&gt; server data replaces local data, unless the local copy is pending and newer. Concluded events are removed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A sync runs at start-up, when the browser comes back online and when you press the button in the top bar, which also shows how many changes are waiting.&lt;/p&gt;

&lt;p&gt;An event looks like this (JSON, camelCase):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0b1e9e8e-5c0f-4a5e-8d0e-6b1a2f6c9a11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nomeEvento"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sagra della castagna"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cittaEvento"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Soriano nel Cimino"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataInizioEvento"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-24T00:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataFineEvento"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-26T00:00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"descrizioneEvento"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Caldarroste, vino novello e musica dal vivo."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventoConcluso"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataOraUltimaModifica"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-22T18:30:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stato"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Approvato"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server sets &lt;code&gt;dataOraUltimaModifica&lt;/code&gt; on every write: the value sent by the client is only used to decide which version is newer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: public APIs, but not defenseless
&lt;/h2&gt;

&lt;p&gt;Anonymous write endpoints call for a few precautions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-IP rate limiting&lt;/strong&gt; on writes and login, with a maximum of 20 events per request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes to existing events only with an &lt;code&gt;Admin&lt;/code&gt;/&lt;code&gt;Editor&lt;/code&gt; token:&lt;/strong&gt; anyone else gets &lt;code&gt;403&lt;/code&gt;. New events in the same request are still inserted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length limits on fields&lt;/strong&gt; (name 200, city 100, description 4000, and so on): longer values get &lt;code&gt;400&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X-Forwarded-*&lt;/code&gt; headers&lt;/strong&gt; are trusted only from the Docker network, and nginx overwrites &lt;code&gt;X-Forwarded-For&lt;/code&gt;, so the per-IP limits cannot be dodged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS&lt;/strong&gt; limited to the origins listed in &lt;code&gt;Cors:Origins&lt;/code&gt;, and a restricted &lt;code&gt;AllowedHosts&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration test suite (xUnit with in-memory SQLite) covers events, moderation, users, login, rate limits, forwarded headers, CORS and malicious input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Docker in one command
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; starts four containers on a private network:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sqlserver&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SQL Server 2022, data in the &lt;code&gt;sqldata&lt;/code&gt; volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The API on port 8080 (internal only), non-root user, health check on &lt;code&gt;/healthz&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The built Angular client, served by an unprivileged nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;proxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A dedicated nginx: the only public entry point (80/443), terminates HTTPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The client and the API share one origin, so production needs no CORS and &lt;code&gt;apiBaseUrl&lt;/code&gt; stays empty in &lt;code&gt;config.json&lt;/code&gt;: a single build runs in every environment.&lt;/p&gt;

&lt;p&gt;To try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env     &lt;span class="c"&gt;# fill in the variables&lt;/span&gt;
&lt;span class="c"&gt;# create deploy/certs/fullchain.pem and privkey.pem (a self-signed certificate is fine for a trial)&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;https://localhost&lt;/code&gt; and sign in with &lt;code&gt;ADMIN_EMAIL&lt;/code&gt; / &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt; from the &lt;code&gt;.env&lt;/code&gt; file. EF Core migrations run at start-up and the &lt;code&gt;Admin&lt;/code&gt; user is created.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A fair warning:&lt;/strong&gt; the Docker files are written but &lt;strong&gt;not yet tried&lt;/strong&gt; on a real Docker host. The manual checks to run are in the repository's &lt;code&gt;TEST-MANUALI.md&lt;/code&gt; file. The compose file is meant for trials and small deployments: for production you need real certificates, a managed or backed-up SQL Server and a secret store instead of &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Running it locally (without Docker)
&lt;/h2&gt;

&lt;p&gt;You need the .NET 10 SDK, a SQL Server instance and Node.js (&lt;code&gt;^20.19&lt;/code&gt;, &lt;code&gt;^22.12&lt;/code&gt; or &lt;code&gt;^24&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AngeloDotNet/SagreEventi.SQLServer.git
&lt;span class="nb"&gt;cd &lt;/span&gt;SagreEventi.SQLServer/src/SagreEventi.Web.Server
dotnet run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The connection string and the first users go in &lt;code&gt;appsettings.local.json&lt;/code&gt; (git-ignored). Then the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;src/SagreEventi.Web.Angular
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:4200&lt;/code&gt;: in development, requests to &lt;code&gt;/api&lt;/code&gt; are forwarded to &lt;code&gt;https://localhost:5001&lt;/code&gt;, so there is nothing else to configure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is missing (and what comes next)
&lt;/h2&gt;

&lt;p&gt;The roadmap is as honest as the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;try the Docker stack on a real host and tick off the manual checks;&lt;/li&gt;
&lt;li&gt;an installable offline app shell (service worker);&lt;/li&gt;
&lt;li&gt;server-side search and pagination once the catalog grows;&lt;/li&gt;
&lt;li&gt;automated tests for the Angular client's sync logic (the API already has its own);&lt;/li&gt;
&lt;li&gt;geolocation: events near you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SagreEventi is a small project, but it brings together quite a few concrete topics: public APIs with moderation, offline sync, roles, rate limiting, deployment behind a reverse proxy. If you need inspiration for an app with a .NET API and an Angular client, the repository can be a good starting point.&lt;/p&gt;

&lt;p&gt;If the project is useful to you, a ⭐ on &lt;a href="https://github.com/AngeloDotNet/SagreEventi.SQLServer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps others find it. Issues and pull requests are welcome (starting from the &lt;code&gt;develop&lt;/code&gt; branch).&lt;/p&gt;

&lt;p&gt;And you, how do you handle offline sync in your apps? Let me know in the comments.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>angular</category>
      <category>primeng</category>
      <category>sqlserver</category>
    </item>
    <item>
      <title>Hacktoberfest 2025</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Sat, 11 Oct 2025 07:00:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/hacktoberfest-2025-5549</link>
      <guid>https://dev.to/angelodotnet/hacktoberfest-2025-5549</guid>
      <description>&lt;p&gt;Hacktoberfest 2025: One Brick at a Time! I'm thrilled to have helped make open source a better place with Hacktoberfest. &lt;/p&gt;

&lt;p&gt;Every pull request, no matter how small, is a step toward a more collaborative and innovative future.&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%2F915pcp8iyexfh96rn2e2.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%2F915pcp8iyexfh96rn2e2.png" alt=" " width="603" height="753"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>programming</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>How to easily integrate and extend authentication in ASP.NET Core projects, using ASP.NET Core Identity</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Fri, 01 Aug 2025 07:23:02 +0000</pubDate>
      <link>https://dev.to/angelodotnet/how-to-easily-integrate-and-extend-authentication-in-aspnet-core-projects-using-aspnet-core-130p</link>
      <guid>https://dev.to/angelodotnet/how-to-easily-integrate-and-extend-authentication-in-aspnet-core-projects-using-aspnet-core-130p</guid>
      <description>&lt;p&gt;A few weeks ago, we ported the project shown in this &lt;a href="https://dev.to/angelodotnet/how-to-manage-roles-permissions-and-more-using-jwt-token-and-aspnet-core-identity-11k0"&gt;post&lt;/a&gt; to make it more dynamic and easier to implement and maintain.&lt;/p&gt;

&lt;p&gt;📢 A new GitHub repository has been created, available &lt;a href="https://github.com/AngeloDotNet/MinimalApi.Identity" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and still under development/implementation. &lt;/p&gt;

&lt;p&gt;⚠️ Important&lt;br&gt;
This library is still under development of new implementations and in the process of creating the related documentation.&lt;/p&gt;

&lt;p&gt;🧩 Features&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minimal API&lt;/strong&gt;: Built using .NET 8 Minimal API for a lightweight and efficient implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity Framework Core&lt;/strong&gt;: Uses EF Core for data access, making it easy to integrate with your existing database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular&lt;/strong&gt;: The library is designed to be modular, allowing you to add or remove features as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic&lt;/strong&gt;: Supports dynamic management of users, roles, claims, forms, licensing and policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible Configuration&lt;/strong&gt;: Easily configurable via &lt;code&gt;appsettings.json&lt;/code&gt; to suit your application's needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbox Pattern&lt;/strong&gt;: Implement the &lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;transactional outbox pattern&lt;/a&gt; for reliable email sending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows for modular management of the original library available in this GitHub &lt;a href="https://github.com/AngeloDotNet/Packages.MinimalApi.Identity" rel="noopener noreferrer"&gt;repository&lt;/a&gt; based on the project's needs, without having to configure everything immediately.&lt;/p&gt;

&lt;p&gt;Contributions are also welcome. &lt;/p&gt;

&lt;p&gt;Please report issues and pull requests on the repository, and don't forget to ⭐ on GitHub if you find this project useful to show your support and help others discover it!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webapi</category>
      <category>csharp</category>
      <category>microservices</category>
    </item>
    <item>
      <title>How can dynamically add separate API endpoints</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Thu, 05 Jun 2025 05:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/how-can-dynamically-add-separate-api-endpoints-4h56</link>
      <guid>https://dev.to/angelodotnet/how-can-dynamically-add-separate-api-endpoints-4h56</guid>
      <description>&lt;p&gt;The example in the github repository below shows a simple implementation on how you can add API endpoints (and their dependencies, via dependency injection) by linking external class library projects or adding their Nuget packages (if the class libraries are released as a Nuget package).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AngeloDotNet/NET8-SampleDynamicallyAddAPIEndpoints" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comments and/or suggestions are always welcome.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webapi</category>
      <category>csharp</category>
      <category>development</category>
    </item>
    <item>
      <title>How to connect two microservices with RabbitMQ and Rebus</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Mon, 26 May 2025 05:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/how-to-connect-two-microservices-with-rabbitmq-and-rebus-278</link>
      <guid>https://dev.to/angelodotnet/how-to-connect-two-microservices-with-rabbitmq-and-rebus-278</guid>
      <description>&lt;p&gt;The example in the github repository below shows a simple implementation of two microservices (.NET 8 Web Api) communicating with each other via RabbitMQ and Rebus.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AngeloDotNet/NET8-SampleRabbitMQRebus" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comments and/or suggestions are always welcome.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>rebus</category>
      <category>webapi</category>
    </item>
    <item>
      <title>How to manage users, roles, and claims, using jwt token and asp.net core identity</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Thu, 06 Mar 2025 06:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/how-to-manage-roles-permissions-and-more-using-jwt-token-and-aspnet-core-identity-11k0</link>
      <guid>https://dev.to/angelodotnet/how-to-manage-roles-permissions-and-more-using-jwt-token-and-aspnet-core-identity-11k0</guid>
      <description>&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; We recommend reading the updated article available at this &lt;a href="https://dev.to/angelodotnet/alternative-how-to-manage-users-roles-and-claims-using-jwt-token-and-aspnet-core-identity-c0i"&gt;link&lt;/a&gt;. Thank you.&lt;/p&gt;




&lt;p&gt;After seeing how to protect minimal APIs with JWT tokens and Asp.Net Core Identity, in &lt;a href="https://dev.to/angelodotnet/how-to-secure-minimal-api-microservices-with-aspnet-core-identity-2o68"&gt;this article&lt;/a&gt;, I want to share a next step, which I found myself facing for personal needs, that is how to make the management of JWT token claims dynamic in order to manage roles, claims, modules and licenses.&lt;/p&gt;

&lt;p&gt;To obtain a working example that uses this implementation, I created this &lt;a href="https://github.com/AngeloDotNet/Packages.MinimalApi.Identity/tree/main/IdentityManager.API" rel="noopener noreferrer"&gt;project&lt;/a&gt; (wrapper extender of ASP.NET Core Identity) that uses a SQL server database and allows this dynamic management.&lt;/p&gt;

&lt;p&gt;The implementation explained above is available as a Nuget package, available at this &lt;a href="https://www.nuget.org/packages/MinimalApi.Identity.API" rel="noopener noreferrer"&gt;link&lt;/a&gt;, while the code is available in this &lt;a href="https://github.com/AngeloDotNet/Packages.MinimalApi.Identity" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, which will be enriched with additional features and documentation in the coming weeks.&lt;/p&gt;

&lt;p&gt;The project is constantly evolving. Contributions are welcome. Feel free to file issues and pull requests on the repository and don't forget that if you find this project useful, put a ⭐ on GitHub to show your support and help others discover it!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>api</category>
      <category>identity</category>
    </item>
    <item>
      <title>How to use SignalR in a Blazor WebAssembly and Web API project</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Thu, 13 Feb 2025 06:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/how-to-use-signalr-in-a-blazor-webassembly-and-web-api-project-27cp</link>
      <guid>https://dev.to/angelodotnet/how-to-use-signalr-in-a-blazor-webassembly-and-web-api-project-27cp</guid>
      <description>&lt;p&gt;This post was born from a personal need during the development of a notification system in a festival management application, which I am currently developing (the source can be found in this &lt;a href="https://github.com/AngeloDotNet/GSWCloudApp" rel="noopener noreferrer"&gt;repository&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;There will be a need to implement a notification service in the client part (frontend), but the notifications will have to be generated automatically by the backend part.&lt;/p&gt;

&lt;p&gt;Faced with this need in the short term, I created this example &lt;a href="https://github.com/AngeloDotNet/Sample-API-SendNotify-BlazorWasm" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, which contains two client projects (client and client2), and a server project (server).&lt;/p&gt;

&lt;p&gt;By starting the server and client projects, you can have a demo of signalR generates notifications in a razor page, with the help of Mudblazor these notifications have a different color based on the severity.&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%2Fg79sgorym1lc76bwuqvu.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%2Fg79sgorym1lc76bwuqvu.png" width="351" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this example, it was chosen to use the "notify-client" endpoints generating the notification in grey and "notify-client-info" generating the notification in light blue.&lt;/p&gt;

&lt;p&gt;To generate these notifications you can use swagger of the backend project, testing the endpoints that contain the nomenclature "notify-client"&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%2Fwm1tuf3nt98rye2qi002.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%2Fwm1tuf3nt98rye2qi002.png" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead, by starting the server and client projects, you can have a demo of signalR that generates notifications in a Razor page, with the help of Mudblazor the notifications will all have the same color, in this case gray and in the backend part the endpoint to use will be the one called "notify-client2", alternatively you can use the button on the frontend "Send notification"&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%2F7foxuann9rc7skiajsnl.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%2F7foxuann9rc7skiajsnl.png" width="338" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you like my open source works don't forget to "follow me" on GitHub and/or click the repository's starred button.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>webassembly</category>
      <category>csharp</category>
    </item>
    <item>
      <title>How to secure minimal api microservices with asp.net core identity</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Fri, 03 Jan 2025 13:00:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/how-to-secure-minimal-api-microservices-with-aspnet-core-identity-2o68</link>
      <guid>https://dev.to/angelodotnet/how-to-secure-minimal-api-microservices-with-aspnet-core-identity-2o68</guid>
      <description>&lt;p&gt;The project "MinimalApi.AuthJWTIdentity" demonstrates how to configure authentication and authorization using .NET Identity and JWT tokens shared between two microservices (web APIs). &lt;/p&gt;

&lt;p&gt;Here is a detailed summary of the project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; To provide an example of configuring authentication and authorization using .NET Identity and JWT tokens for two microservices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; The example includes minimal configuration and excludes advanced features like token refresh, role management, and claims. However, these features can be added with additional configuration.&lt;br&gt;
Project Structure&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Microservice01:&lt;/strong&gt; Contains the Program.cs and appsettings.json files for its configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microservice02:&lt;/strong&gt; Similar structure to Microservice01 with its own &lt;br&gt;
Program.cs and appsettings.json.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MicroserviceAuth:&lt;/strong&gt; Manages authentication and contains the main logic for user registration and login.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main Files:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;README.md:&lt;/strong&gt; Describes the project and its purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program.cs of Microservice01:&lt;/strong&gt; Sets up the microservice, adds authentication and authorization, and defines a secure endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program.cs of Microservice02:&lt;/strong&gt; Similar to Microservice01 with its own setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Program.cs of MicroserviceAuth:&lt;/strong&gt; Manages user registration, login, and JWT token generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration Files:&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;appsettings.json of Microservice01:&lt;/strong&gt; Contains JWT settings and logging configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;appsettings.json of Microservice02:&lt;/strong&gt; Similar to Microservice01 with its own configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;appsettings.json of MicroserviceAuth:&lt;/strong&gt; Contains JWT settings and logging configuration for the authentication service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup and Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each microservice has its own Program.cs file that sets up the web application, adds authentication and authorization services, and defines endpoints.&lt;/p&gt;

&lt;p&gt;The appsettings.json files in each microservice contain the JWT token settings and logging configuration.&lt;/p&gt;

&lt;p&gt;The MicroserviceAuth service handles user registration and login, generating JWT tokens for authenticated users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication and Authorization:&lt;/strong&gt; Implemented using .NET Identity and JWT tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microservices:&lt;/strong&gt; Each microservice has its own setup and configuration, sharing the JWT token settings across the services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoints:&lt;/strong&gt; Secure endpoints are defined in each microservice, requiring authentication to access.&lt;/p&gt;

&lt;p&gt;This project provides a foundational example of using .NET Identity and JWT for securing microservices, with the potential for extending functionality with additional configuration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AngeloDotNet/MinimalApi.AuthJWTIdentity" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comments and/or suggestions are always welcome.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>jwt</category>
      <category>identity</category>
    </item>
    <item>
      <title>How to connect two microservices with RabbitMQ</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Fri, 27 Dec 2024 06:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/example-of-microservice-communication-with-rabbitmq-3b2f</link>
      <guid>https://dev.to/angelodotnet/example-of-microservice-communication-with-rabbitmq-3b2f</guid>
      <description>&lt;p&gt;The example in the github repository below shows a simple implementation of two microservices (.NET 8 Web Api) communicating with each other via RabbitMQ queues.&lt;/p&gt;

&lt;p&gt;If you want a two-way communication instead, we recommend reading the article example of Microservice Communication with RabbitMQ and MassTransit (click &lt;a href="https://dev.to/angelodotnet/making-two-microservices-communicate-using-rabbitmq-and-masstransit-2g8i"&gt;here&lt;/a&gt; to read the article)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AngeloDotNet/NET8-SampleRabbitMQ" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comments and/or suggestions are always welcome.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>microservices</category>
      <category>csharp</category>
      <category>mqtt</category>
    </item>
    <item>
      <title>How to create a simple appointment calendar</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Thu, 19 Dec 2024 06:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/example-to-create-a-appointment-calendar-477n</link>
      <guid>https://dev.to/angelodotnet/example-to-create-a-appointment-calendar-477n</guid>
      <description>&lt;p&gt;The example in the github repository below shows how to create a simple appointment calendar (currently they are generated hard coded, but there is a comment (to read the comment click &lt;a href="https://github.com/AngeloDotNet/AppointmentCalendar/blob/main/src/CalendarioEventi/Pages/Calendar.razor.cs#L45" rel="noopener noreferrer"&gt;here&lt;/a&gt;) in case you want to retrieve them from an external API service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AngeloDotNet/AppointmentCalendar" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comments and/or suggestions are always welcome.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>webdev</category>
      <category>calendar</category>
    </item>
    <item>
      <title>Docker configurations for .NET applications and more</title>
      <dc:creator>Angelo Pirola</dc:creator>
      <pubDate>Thu, 12 Dec 2024 06:30:00 +0000</pubDate>
      <link>https://dev.to/angelodotnet/docker-configurations-for-net-applications-and-more-1pg8</link>
      <guid>https://dev.to/angelodotnet/docker-configurations-for-net-applications-and-more-1pg8</guid>
      <description>&lt;p&gt;This repository contains Docker configurations, including Docker Compose files and Dockerfiles, to simplify the configuration and deployment of containerized applications.&lt;/p&gt;

&lt;p&gt;It provides a consistent and reproducible environment for development, testing, and production. Configurations are designed to be easily customizable and extensible to suit various use cases and application requirements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AngeloDotNet/Docker.Application" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>community</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
