<?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: Mark Jack</title>
    <description>The latest articles on DEV Community by Mark Jack (@markjackmilian).</description>
    <link>https://dev.to/markjackmilian</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%2F762568%2F16a10d83-81d8-4ad2-98f0-b5de4592d80d.jpg</url>
      <title>DEV Community: Mark Jack</title>
      <link>https://dev.to/markjackmilian</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/markjackmilian"/>
    <language>en</language>
    <item>
      <title>mjm.local.docs: Open Source Local Knowledge Base with MCP</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Fri, 20 Feb 2026 13:29:37 +0000</pubDate>
      <link>https://dev.to/markjackmilian/mjmlocaldocs-open-source-local-knowledge-base-with-mcp-3711</link>
      <guid>https://dev.to/markjackmilian/mjmlocaldocs-open-source-local-knowledge-base-with-mcp-3711</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You are mid-session with Claude Code or another AI coding assistant. You ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"How does authentication work in our system?"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;"What was the decision behind using event sourcing in the orders module?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI does its best, guessing from the code it can see. But the real answer is buried in a Word document, a PDF architecture diagram, or a Markdown ADR (Architecture Decision Record) that lives somewhere on your disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mjm.local.docs&lt;/strong&gt; solves this.&lt;/p&gt;

&lt;p&gt;It is an open-source, locally-deployed knowledge base server that exposes your documents through both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Blazor Web UI&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A full &lt;strong&gt;Model Context Protocol (MCP) server&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows your AI assistant to search, read, and even update your documentation directly from chat.&lt;/p&gt;

&lt;p&gt;Built on &lt;strong&gt;.NET 10&lt;/strong&gt;, it runs entirely on your machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No mandatory cloud dependency&lt;/li&gt;
&lt;li&gt;No data leaving your environment&lt;/li&gt;
&lt;li&gt;Full support for pluggable embedding models&lt;/li&gt;
&lt;li&gt;Pluggable vector storage backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/markjackmilian/mjm.local.docs" rel="noopener noreferrer"&gt;https://github.com/markjackmilian/mjm.local.docs&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  What Is mjm.local.docs?
&lt;/h2&gt;

&lt;p&gt;mjm.local.docs is a &lt;strong&gt;self-hosted semantic document search server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At its core, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingests documents — PDF, Word (.docx), Markdown, plain text, and more&lt;/li&gt;
&lt;li&gt;Chunks and embeds them using a configurable embedding provider (local or cloud-based)&lt;/li&gt;
&lt;li&gt;Stores embeddings in a configurable vector store (SQLite, HNSW index, SQL Server, or in-memory)&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Exposes search and management through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Blazor web interface&lt;/li&gt;
&lt;li&gt;An MCP HTTP endpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Clean Architecture
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mjm.LocalDocs.Core           ← Domain models, interfaces (zero external dependencies)
Mjm.LocalDocs.Infrastructure ← Implementations: embeddings, readers, vector stores
Mjm.LocalDocs.Server         ← ASP.NET Core host, Blazor UI, MCP tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Everything is wired together via standard .NET dependency injection.&lt;/p&gt;

&lt;p&gt;Every major component is swappable via configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embedding provider&lt;/li&gt;
&lt;li&gt;Vector store&lt;/li&gt;
&lt;li&gt;File storage&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Deploy Locally in Minutes
&lt;/h2&gt;

&lt;p&gt;Clone and run:&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/markjackmilian/mjm.local.docs.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mjm.local.docs/mjm.local.docs
dotnet run &lt;span class="nt"&gt;--project&lt;/span&gt; src/Mjm.LocalDocs.Server/Mjm.LocalDocs.Server.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Web UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;admin / admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Change them in &lt;code&gt;appsettings.json&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;MCP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5024/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Docker required.&lt;br&gt;
No cloud account needed.&lt;/p&gt;

&lt;p&gt;Out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In-memory vector store&lt;/li&gt;
&lt;li&gt;Deterministic fake embedding generator&lt;/li&gt;
&lt;li&gt;No API key required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect for local development.&lt;/p&gt;


&lt;h2&gt;
  
  
  Pluggable Embedding Providers
&lt;/h2&gt;

&lt;p&gt;Embedding generation is fully pluggable via the &lt;code&gt;IEmbeddingService&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;Configured in &lt;code&gt;appsettings.json&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Provider Value&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fake&lt;/td&gt;
&lt;td&gt;Fake&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;Deterministic word-hash vectors. Dev/test only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;text-embedding-3-small&lt;/code&gt;. API key required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure OpenAI&lt;/td&gt;
&lt;td&gt;AzureOpenAI&lt;/td&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;Azure-hosted OpenAI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;Fully local embeddings.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All providers implement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IEmbeddingGenerator&amp;lt;string, Embedding&amp;lt;float&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can bring your own implementation easily.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running 100% Locally with Ollama
&lt;/h2&gt;

&lt;p&gt;Example configuration:&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;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Embeddings"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ollama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Ollama"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nomic-embed-text"&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;span class="p"&gt;}&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;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;Popular models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nomic-embed-text&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;Balanced quality/speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mxbai-embed-large&lt;/td&gt;
&lt;td&gt;1024&lt;/td&gt;
&lt;td&gt;Higher quality, slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all-minilm&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;Fastest, lower quality&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Storage Options: From SQLite to SQL Server
&lt;/h2&gt;

&lt;p&gt;Configured via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LocalDocs:Storage:Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Vector Search&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;InMemory&lt;/td&gt;
&lt;td&gt;RAM only&lt;/td&gt;
&lt;td&gt;O(n) brute-force&lt;/td&gt;
&lt;td&gt;Dev/testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sqlite&lt;/td&gt;
&lt;td&gt;EF Core + BLOB embeddings&lt;/td&gt;
&lt;td&gt;O(n) cosine&lt;/td&gt;
&lt;td&gt;Small/medium KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SqliteHnsw&lt;/td&gt;
&lt;td&gt;SQLite + HNSW index file&lt;/td&gt;
&lt;td&gt;O(log n) approx&lt;/td&gt;
&lt;td&gt;Larger KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SqlServer&lt;/td&gt;
&lt;td&gt;SQL Server 2025+ VECTOR type&lt;/td&gt;
&lt;td&gt;DiskANN&lt;/td&gt;
&lt;td&gt;Enterprise/Azure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Connection string examples:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SQLite&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Data Source=localdocs.db"&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;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SQL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Server&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Server=myserver.database.windows.net;Database=localdocs;..."&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;h2&gt;
  
  
  Vector Search Under the Hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Brute-Force (SQLite)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Embeddings stored as BLOBs&lt;/li&gt;
&lt;li&gt;Cosine similarity against every chunk&lt;/li&gt;
&lt;li&gt;O(n)&lt;/li&gt;
&lt;li&gt;Reliable for thousands of documents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HNSW (SqliteHnsw)
&lt;/h3&gt;

&lt;p&gt;Adds a persisted HNSW graph:&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;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Storage"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SqliteHnsw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Hnsw"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"MaxConnections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"EfConstruction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"EfSearch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AutoSaveDelayMs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&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;span class="p"&gt;}&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;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;Approximate O(log n).&lt;br&gt;
Ideal for tens of thousands of chunks.&lt;/p&gt;
&lt;h3&gt;
  
  
  SQL Server DiskANN
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="n"&gt;NVARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;vec_idx_chunk_embeddings&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;chunk_embeddings&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'cosine'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Supported metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cosine&lt;/li&gt;
&lt;li&gt;euclidean&lt;/li&gt;
&lt;li&gt;dotproduct&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Document Processing
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Supported Formats
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;.pdf&lt;/td&gt;
&lt;td&gt;PdfPig&lt;/td&gt;
&lt;td&gt;Native text only (no OCR)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.docx&lt;/td&gt;
&lt;td&gt;NPOI&lt;/td&gt;
&lt;td&gt;Modern .docx only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.md&lt;/td&gt;
&lt;td&gt;Markdown reader&lt;/td&gt;
&lt;td&gt;Preserves syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.txt&lt;/td&gt;
&lt;td&gt;Plain text&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.html, .json, .xml, .csv&lt;/td&gt;
&lt;td&gt;Fallback UTF-8&lt;/td&gt;
&lt;td&gt;Raw extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Chunking
&lt;/h3&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;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Chunking"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"MaxChunkSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OverlapSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&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;span class="p"&gt;}&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;Chunk IDs follow the format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{DocumentId}_chunk_{index}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Document Versioning
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Old version marked &lt;code&gt;IsSuperseded = true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Old chunks removed from search&lt;/li&gt;
&lt;li&gt;Full history preserved&lt;/li&gt;
&lt;li&gt;Version chain visible in UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No silent history loss.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Blazor Web UI
&lt;/h2&gt;

&lt;p&gt;Built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blazor Server&lt;/li&gt;
&lt;li&gt;MudBlazor&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Dashboard&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total projects&lt;/li&gt;
&lt;li&gt;Total documents&lt;/li&gt;
&lt;li&gt;Storage usage&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%2Fb6gee9au08ljd6vlxd5m.jpeg" 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%2Fb6gee9au08ljd6vlxd5m.jpeg" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag-and-drop multi-file upload&lt;/li&gt;
&lt;li&gt;Inline Markdown editor ("Add Know How")&lt;/li&gt;
&lt;li&gt;Version history navigation&lt;/li&gt;
&lt;li&gt;Edit, delete, download&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%2Fdal5dyfeoayhmme2uius.jpeg" 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%2Fdal5dyfeoayhmme2uius.jpeg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP Config Page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generates ready-to-use MCP config snippet&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Named Bearer tokens&lt;/li&gt;
&lt;li&gt;Optional expiry&lt;/li&gt;
&lt;li&gt;Tokens shown once&lt;/li&gt;
&lt;li&gt;Revocable anytime&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  MCP: Let Your AI Navigate Your Knowledge Base
&lt;/h2&gt;

&lt;p&gt;Exposes 10 MCP tools.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;search_docs&lt;/td&gt;
&lt;td&gt;Semantic search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add_document&lt;/td&gt;
&lt;td&gt;Add new document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;update_document&lt;/td&gt;
&lt;td&gt;Create new version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get_document_content&lt;/td&gt;
&lt;td&gt;Full extracted text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list_projects&lt;/td&gt;
&lt;td&gt;List projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;create_project&lt;/td&gt;
&lt;td&gt;Create project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get_project&lt;/td&gt;
&lt;td&gt;Project details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete_project&lt;/td&gt;
&lt;td&gt;Delete project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list_documents&lt;/td&gt;
&lt;td&gt;List documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;get_document&lt;/td&gt;
&lt;td&gt;Metadata + preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete_document&lt;/td&gt;
&lt;td&gt;Delete document&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Connecting Claude Code / OpenCode
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.claude/mcp.json&lt;/code&gt;:&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"local-docs"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5024/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer YOUR_API_TOKEN"&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;span class="p"&gt;}&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;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;h2&gt;
  
  
  UI vs MCP
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Best Via&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial setup&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulk upload&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version review&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token management&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline authoring&lt;/td&gt;
&lt;td&gt;Web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic search&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI-driven updates&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Programmatic ingestion&lt;/td&gt;
&lt;td&gt;MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Configuration Reference
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Production with Ollama + HNSW
&lt;/h3&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;"ConnectionStrings"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Data Source=localdocs.db"&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;span class="nl"&gt;"LocalDocs"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Authentication"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-secure-password"&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;span class="nl"&gt;"Mcp"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"RequireAuthentication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;span class="nl"&gt;"Embeddings"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ollama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Ollama"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:11434"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nomic-embed-text"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Storage"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SqliteHnsw"&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;span class="p"&gt;}&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;Environment variables supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_API_KEY
AZURE_OPENAI_ENDPOINT
AZURE_OPENAI_API_KEY
AZURE_STORAGE_CONNECTION_STRING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Packages
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft.SemanticKernel&lt;/td&gt;
&lt;td&gt;AI orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft.Extensions.AI&lt;/td&gt;
&lt;td&gt;Embedding abstraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ModelContextProtocol.AspNetCore&lt;/td&gt;
&lt;td&gt;MCP server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF Core&lt;/td&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MudBlazor&lt;/td&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PdfPig&lt;/td&gt;
&lt;td&gt;PDF extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NPOI&lt;/td&gt;
&lt;td&gt;Word extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure.Storage.Blobs&lt;/td&gt;
&lt;td&gt;Blob storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serilog&lt;/td&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xunit + NSubstitute&lt;/td&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;Open source and actively developed.&lt;/p&gt;

&lt;p&gt;Contributions welcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New embedding providers&lt;/li&gt;
&lt;li&gt;Storage backends&lt;/li&gt;
&lt;li&gt;Document readers&lt;/li&gt;
&lt;li&gt;MCP tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/markjackmilian/mjm.local.docs" rel="noopener noreferrer"&gt;https://github.com/markjackmilian/mjm.local.docs&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;mjm.local.docs fills a growing gap in AI-assisted development:&lt;/p&gt;

&lt;p&gt;Your AI assistant needs access to your &lt;strong&gt;knowledge&lt;/strong&gt;, not just your code.&lt;/p&gt;

&lt;p&gt;By combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Blazor UI for humans&lt;/li&gt;
&lt;li&gt;A full MCP server for AI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It bridges your team's accumulated knowledge with your AI tools — fully local if you choose.&lt;/p&gt;

&lt;p&gt;Give it a try.&lt;br&gt;
Star the repo.&lt;br&gt;
Open a PR.&lt;/p&gt;




&lt;p&gt;If you found this article helpful, follow me on GitHub, Twitter, and Bluesky.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>From Zero to Scheduled: TickerQ for .NET</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Fri, 04 Jul 2025 11:05:17 +0000</pubDate>
      <link>https://dev.to/markjackmilian/from-zero-to-scheduled-tickerq-for-net-48lg</link>
      <guid>https://dev.to/markjackmilian/from-zero-to-scheduled-tickerq-for-net-48lg</guid>
      <description>&lt;h2&gt;
  
  
  🔧 Introducing TickerQ – A Modern .NET Scheduler
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;As a .NET dev who’s tried just about every job scheduler out there, I wanted to share why TickerQ has quickly become my go-to. It’s fast, clean, and built with devs in mind. Here’s a quick dive into what it offers and how to get started.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://tickerq.arcenox.com/" rel="noopener noreferrer"&gt;TickerQ&lt;/a&gt; is a high-performance, reflection‑free background task scheduler for .NET. It leverages source generators, offers cron and time‑based execution, supports EF Core persistence, and includes a real-time dashboard UI—all with minimal overhead.&lt;/p&gt;

&lt;p&gt;Key features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source‑generator discovery&lt;/strong&gt; (no reflection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TimeTickers&lt;/strong&gt; (one-time) and &lt;strong&gt;CronTickers&lt;/strong&gt; (recurring)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EF Core integration&lt;/strong&gt; for persistence of jobs, states, and history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard UI&lt;/strong&gt; built with SignalR and Tailwind (Vue.js-based)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry policies, throttling, cooldowns&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distributed coordination&lt;/strong&gt;, priorities, DI support&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 Installing &amp;amp; Configuring
&lt;/h2&gt;

&lt;p&gt;Use the following NuGet packages. While only the core &lt;code&gt;TickerQ&lt;/code&gt; package is required, in this example we will also set up optional packages for EF Core persistence and the real-time dashboard:&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 TickerQ
dotnet add package TickerQ.EntityFrameworkCore
dotnet add package TickerQ.Dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  In &lt;code&gt;Program.cs&lt;/code&gt;
&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;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;AddTickerQ&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="nf"&gt;SetMaxConcurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&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;SetExceptionHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyExceptionHandler&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;.&lt;/span&gt;&lt;span class="n"&gt;AddOperationalStore&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;efOpt&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;efOpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseModelCustomizerForMigrations&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Design-time migrations only&lt;/span&gt;
        &lt;span class="n"&gt;efOpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelMissedTickersOnApplicationRestart&lt;/span&gt;&lt;span class="p"&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="nf"&gt;AddDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/tickerq-dashboard"&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="nf"&gt;AddDashboardBasicAuth&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;UseTickerQ&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎯 Defining and Scheduling Jobs
&lt;/h2&gt;

&lt;p&gt;TickerQ uses a &lt;code&gt;[TickerFunction]&lt;/code&gt; attribute and compile‑time source generation. You can define multiple scheduled functions with different triggers:&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="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TickerFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CleanupLogs"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CleanupLogs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TickerRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do cleanup work...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;TickerFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CleanupTempFiles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;"0 */6 * * *"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CleanupTempFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TickerRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&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;"Cleaning temporary files..."&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;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&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;h3&gt;
  
  
  ▶️ TimeTicker example (one‑off or delayed)
&lt;/h3&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;timeTickerManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&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;TimeTicker&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SendWelcomeEmail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;UserId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;123&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;RunAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;Retries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;RetryIntervals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;300&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;h3&gt;
  
  
  🔁 CronTicker example (recurring)
&lt;/h3&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;cronTickerManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&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;CronTicker&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Function&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CleanupLogs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;CronExpression&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0 */6 * * *"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Retries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;RetryIntervals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;300&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;
  
  
  🗔️ Persistence with EF Core
&lt;/h2&gt;

&lt;p&gt;To enable persistence, you'll need to create the necessary tables using EF Core migrations.&lt;/p&gt;

&lt;p&gt;TickerQ’s EF Core provider persists tickers, execution history, and job states into your &lt;code&gt;DbContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are two integration methods:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Using &lt;code&gt;UseModelCustomizerForMigrations()&lt;/code&gt;&lt;/strong&gt;: Best for projects that want to avoid cluttering their &lt;code&gt;DbContext&lt;/code&gt; class with extra config. This approach automatically wires in the schema only during EF Core &lt;strong&gt;design-time operations&lt;/strong&gt; (e.g., &lt;code&gt;dotnet ef migrations add&lt;/code&gt;). At runtime, it won't interfere with your model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual configuration&lt;/strong&gt;: Required if you want full control or need schema visibility in your runtime model. This requires directly applying the necessary configurations in your &lt;code&gt;OnModelCreating&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;&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="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnModelCreating&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyConfiguration&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;TimeTickerConfigurations&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;ApplyConfiguration&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;CronTickerConfigurations&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;ApplyConfiguration&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;CronTickerOccurrenceConfigurations&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;Apply migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet ef migrations add AddTickerQTables
dotnet ef database update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌐 Dashboard UI &amp;amp; Real‑Time Monitoring
&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%2F0qsus1ywabj1pbse6xum.jpeg" 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%2F0qsus1ywabj1pbse6xum.jpeg" alt=" " width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The TickerQ Dashboard is a modern Vue.js-based web UI powered by SignalR and Tailwind CSS. It provides live updates and comprehensive control over job scheduling and monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&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;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;AddTickerQ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&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;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddOperationalStore&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetInstanceIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MyAppTickerQ"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;basePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"/tickerq-dashboard"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDashboardBasicAuth&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;UseTickerQDashboard&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add credentials in &lt;code&gt;appsettings.json&lt;/code&gt;:&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="nl"&gt;"TickerQBasicAuth"&lt;/span&gt;&lt;span class="p"&gt;:&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;span class="nl"&gt;"Username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&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;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System Overview&lt;/strong&gt;: throughput, active nodes, concurrency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job Status Breakdown&lt;/strong&gt;: Done, DueDone, Failed, Pending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CronTickers View&lt;/strong&gt;: timeline, manual trigger, duplication, deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TimeTickers View&lt;/strong&gt;: execution history, live retry/cancel/edit/delete options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add/Edit Job&lt;/strong&gt;: intuitive UI to define function, payload, retry policy.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔐 Distributed Locking via EF Core
&lt;/h2&gt;

&lt;p&gt;To safely run TickerQ across multiple nodes—without overlapping job executions—TickerQ uses EF Core-based distributed locking.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A node picks up a job and marks itself as &lt;code&gt;OwnerNode&lt;/code&gt; in the database.&lt;/li&gt;
&lt;li&gt;Row-level locking (e.g., &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;) ensures mutual exclusion.&lt;/li&gt;
&lt;li&gt;Failed jobs release locks automatically, enabling retry by others.&lt;/li&gt;
&lt;li&gt;Cooldowns and expiry provide safety against zombie tasks.
&lt;/li&gt;
&lt;/ul&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="nf"&gt;AddTickerQ&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;AddOperationalStore&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MyDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;efOpt&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;efOpt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CancelMissedTickersOnApplicationRestart&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;h3&gt;
  
  
  What You Get
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single ownership&lt;/td&gt;
&lt;td&gt;Only one node can process a job at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failover safety&lt;/td&gt;
&lt;td&gt;Locks are released on restart or timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coordination simplicity&lt;/td&gt;
&lt;td&gt;No external lock store needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF Core-friendly&lt;/td&gt;
&lt;td&gt;Leverages your existing DB and transaction logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Use this if you scale horizontally and want safe, DB-driven coordination. For very high throughput or cross-cluster workloads, consider Redis or Zookeeper.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ⚖️ How TickerQ Stacks Up
&lt;/h2&gt;

&lt;p&gt;Compared to classic schedulers like Hangfire or Quartz.NET:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;TickerQ&lt;/th&gt;
&lt;th&gt;Hangfire&lt;/th&gt;
&lt;th&gt;Quartz.NET&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reflection-free&lt;/td&gt;
&lt;td&gt;✅ source gen&lt;/td&gt;
&lt;td&gt;❌ reflection&lt;/td&gt;
&lt;td&gt;❌ reflection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time + cron jobs&lt;/td&gt;
&lt;td&gt;✅ both&lt;/td&gt;
&lt;td&gt;✅ cron, limited delayed&lt;/td&gt;
&lt;td&gt;✅ both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EF Core persistence&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard UI&lt;/td&gt;
&lt;td&gt;✅ real-time first-party&lt;/td&gt;
&lt;td&gt;⚠️ basic&lt;/td&gt;
&lt;td&gt;⚠️ third-party UI req'd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retries &amp;amp; cooldowns&lt;/td&gt;
&lt;td&gt;✅ advanced&lt;/td&gt;
&lt;td&gt;⚠️ basic&lt;/td&gt;
&lt;td&gt;⚠️ manual config req'd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed locking&lt;/td&gt;
&lt;td&gt;✅ via EF Core&lt;/td&gt;
&lt;td&gt;⚠️ partial&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI support&lt;/td&gt;
&lt;td&gt;✅ native&lt;/td&gt;
&lt;td&gt;⚠️ type-based limited&lt;/td&gt;
&lt;td&gt;⚠️ manual config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;✅ low overhead&lt;/td&gt;
&lt;td&gt;⚠️ storage-bound&lt;/td&gt;
&lt;td&gt;⚠️ thread-blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ✅ When to Choose TickerQ
&lt;/h2&gt;

&lt;p&gt;Choose TickerQ if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefer &lt;strong&gt;compile-time safety&lt;/strong&gt; over runtime reflection&lt;/li&gt;
&lt;li&gt;Need a &lt;strong&gt;lightweight, fast scheduler&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Want &lt;strong&gt;cron and time jobs&lt;/strong&gt; with retries/cooldowns&lt;/li&gt;
&lt;li&gt;Use EF Core or need &lt;strong&gt;distributed coordination&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Value a &lt;strong&gt;first-party, real-time dashboard&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations to Consider
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No Redis or distributed cache persistence (yet)&lt;/li&gt;
&lt;li&gt;No node-specific routing or tag-based job targeting&lt;/li&gt;
&lt;li&gt;Throttling and job chaining features are in progress&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;TickerQ offers a &lt;strong&gt;modern, efficient, and developer-friendly&lt;/strong&gt; background scheduler featuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reflection-free architecture via source generation&lt;/li&gt;
&lt;li&gt;Cron and time-based scheduling&lt;/li&gt;
&lt;li&gt;Robust EF Core persistence&lt;/li&gt;
&lt;li&gt;Real-time dashboard with live updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's especially suitable for applications that care about performance, visibility, and maintainability. For deeper dives, visit the &lt;a href="https://tickerq.arcenox.com" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; and check out the benchmarks on &lt;a href="https://medium.com/@albertii.kun01/meet-tickerq-a-lighter-faster-alternative-to-hangfire-and-quartz-for-background-jobs-in-net-7715529323da" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you found this article helpful, follow me on &lt;a href="https://github.com/markjackmilian" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://x.com/markjackmilian" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and &lt;a href="https://bsky.app/profile/markjackmilian.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>schedule</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Sat, 24 May 2025 13:03:30 +0000</pubDate>
      <link>https://dev.to/markjackmilian/-48ie</link>
      <guid>https://dev.to/markjackmilian/-48ie</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl" class="crayons-story__hidden-navigation-link"&gt;Who’s Afraid of the Liskov Substitution Principle?&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/markjackmilian" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F762568%2F16a10d83-81d8-4ad2-98f0-b5de4592d80d.jpg" alt="markjackmilian profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/markjackmilian" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mark Jack
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mark Jack
                
              
              &lt;div id="story-author-preview-content-2518749" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/markjackmilian" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F762568%2F16a10d83-81d8-4ad2-98f0-b5de4592d80d.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mark Jack&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 24 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl" id="article-link-2518749"&gt;
          Who’s Afraid of the Liskov Substitution Principle?
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/designpatterns"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;designpatterns&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dotnet"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dotnet&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cleancode"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cleancode&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/oop"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;oop&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>designpatterns</category>
      <category>dotnet</category>
      <category>cleancode</category>
      <category>oop</category>
    </item>
    <item>
      <title>Who’s Afraid of the Liskov Substitution Principle?</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Sat, 24 May 2025 13:03:03 +0000</pubDate>
      <link>https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl</link>
      <guid>https://dev.to/markjackmilian/whos-afraid-of-the-liskov-substitution-principle-4mfl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay... this could be scary. Let's make it simpler.&lt;/p&gt;

&lt;p&gt;The Liskov Substitution Principle (LSP) is one of the five SOLID principles of object-oriented programming. Often overlooked or misunderstood, it’s actually fundamental for writing maintainable, extensible, and truly “object-oriented” code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the Liskov Substitution Principle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Formulated by Barbara Liskov in 1987, the principle states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that program (correctness, task performed, etc.).”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Put simply: a subclass should be usable anywhere its superclass is expected, without causing unexpected behaviors. If this doesn’t happen, inheritance is being misused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is it important?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Violating the Liskov Substitution Principle leads to bugs that are hard to find, fragile code, and systems that are difficult to evolve. Respecting LSP, instead, lets you fully leverage the power of polymorphism.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A Bad Example in C#&lt;/strong&gt;&lt;br&gt;
Let’s try modeling rectangles and squares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    public override int Width
    {
        get { return base.Width; }
        set { base.Width = base.Height = value; }
    }
    public override int Height
    {
        get { return base.Height; }
        set { base.Width = base.Height = value; }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it seems reasonable: a square is just a special kind of rectangle, right? But let’s see what happens when we use these classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
Console.WriteLine(rect.Area()); // What do we expect?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might expect 5×10=50, but the result will be 100!The Square class forces width and height to always be equal, breaking the expectations set by the Rectangle base class. This is a classic violation of the Liskov Substitution Principle.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Refactor: How to Respect the Principle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To respect the principle, avoid inheritance where it doesn’t make sense. Rectangle and Square are siblings, not parent/child. You can instead have them inherit from a common abstract base class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public abstract class Shape
{
    public abstract int Area();
}

public class Rectangle : Shape
{
    public int Width { get; set; }
    public int Height { get; set; }
    public override int Area()
    {
        return Width * Height;
    }
}

public class Square : Shape
{
    public int Side { get; set; }
    public override int Area()
    {
        return Side * Side;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can use rectangles and squares polymorphically, with no unexpected behaviors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shape shape1 = new Rectangle { Width = 5, Height = 10 };
Shape shape2 = new Square { Side = 5 };
Console.WriteLine(shape1.Area()); // 50
Console.WriteLine(shape2.Area()); // 25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Ehi.. wait! But isn’t a square just a special type of rectangle?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From a mathematical perspective, yes: a square is a particular kind of rectangle (four right angles and all sides equal). So, conceptually, the relationship “Square is a Rectangle” seems to make sense.&lt;/p&gt;

&lt;p&gt;However, in object-oriented programming (OOP), this relationship can cause practical problems. The issue isn’t mathematics, but rather behavior.&lt;/p&gt;

&lt;p&gt;Why shouldn’t Square inherit from Rectangle in OOP?&lt;/p&gt;

&lt;p&gt;When you create a subclass in OOP, the Liskov Substitution Principle requires that everything true for the base class &lt;strong&gt;must remain true for the subclass&lt;/strong&gt;. In our example:&lt;/p&gt;

&lt;p&gt;A Rectangle lets you set Width and Height independently.&lt;/p&gt;

&lt;p&gt;A Square must always have Width == Height.&lt;/p&gt;

&lt;p&gt;If you inherit, you’re forced into an awkward situation where changing Width in a Square also changes Height (and vice versa), since all sides must be equal. But this means the Square no longer behaves like a regular Rectangle!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;When does inheritance make sense?&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the subclass does not alter the invariants or rules of the base class (i.e., it doesn’t change pre-conditions, post-conditions, or invariants), then yes, inheritance is fine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Let's enhance the article with another practical example:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("I'm flying!");
    }
}

public class Penguin : Bird
{
    // The Fly method is inherited, but it makes no sense for a penguin!
}

public class Program
{
    public static void MakeItFly(Bird bird)
    {
        bird.Fly();
    }

    public static void Main()
    {
        Bird tweety = new Bird();
        Penguin pingu = new Penguin();

        MakeItFly(tweety); // Output: I'm flying!
        MakeItFly(pingu);  // Output: I'm flying! (Semantic error)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public abstract class Bird
{
    // All common properties
}

public class FlyingBird : Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("I'm flying");
    }
}

public class Penguin : Bird
{

}

public class Sparrow : FlyingBird
{

}

public class Program
{
    public static void MakeItFly(FlyingBird bird)
    {
        bird.Fly();
    }

    public static void Main()
    {
        Sparrow sparrow = new Sparrow();
        Penguin pingu = new Penguin();

        MakeItFly(sparrow); // Output: I'm flying!
        // MakeItFly(pingu); // Compilation error! (GOOD!)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Conclusions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Liskov Substitution Principle isn’t just theory: ignoring it makes code fragile and unpredictable. When modeling class hierarchies, always ask yourself: “&lt;em&gt;Can I substitute the base class with the subclass without breaking anything?&lt;/em&gt;” If the answer is no, you’re probably heading for design trouble.&lt;/p&gt;

&lt;p&gt;If you found this article helpful follow me on &lt;a href="https://github.com/markjackmilian" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://x.com/markjackmilian" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and &lt;a href="https://bsky.app/profile/markjackmilian.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; for more content.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>designpatterns</category>
      <category>dotnet</category>
      <category>cleancode</category>
      <category>oop</category>
    </item>
    <item>
      <title>YASA - Yet another S.O.L.I.D. article</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Sun, 11 May 2025 17:24:49 +0000</pubDate>
      <link>https://dev.to/markjackmilian/yasa-yet-another-solid-article-1ieo</link>
      <guid>https://dev.to/markjackmilian/yasa-yet-another-solid-article-1ieo</guid>
      <description>&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%2Fucnljcn2easipltpdbnt.jpeg" 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%2Fucnljcn2easipltpdbnt.jpeg" alt=" " width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Understanding SOLID Principles in .NET: Write Better, Scalable, and Maintainable Code
&lt;/h1&gt;

&lt;p&gt;The SOLID principles are the cornerstone of good software design. Whether you're building small applications or large enterprise systems in .NET, applying these principles can help you write code that is easier to understand, maintain, and extend.&lt;/p&gt;

&lt;p&gt;In this article, we'll explore each of the SOLID principles with practical C# examples in the .NET ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 What is SOLID?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SOLID&lt;/strong&gt; is an acronym for five object-oriented design principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt; – Single Responsibility Principle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;O&lt;/strong&gt; – Open/Closed Principle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L&lt;/strong&gt; – Liskov Substitution Principle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I&lt;/strong&gt; – Interface Segregation Principle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D&lt;/strong&gt; – Dependency Inversion Principle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These principles were introduced by Robert C. Martin and have become a standard for writing clean and maintainable software.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Single Responsibility Principle (SRP)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"A class should have only one reason to change."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that a class should only have one job or responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Bad Example
&lt;/h3&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;Invoice&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CalculateTotal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveToDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PrintInvoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;Here, &lt;code&gt;Invoice&lt;/code&gt; is doing too much — it calculates, persists data, and handles printing.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Good Example
&lt;/h3&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;InvoiceCalculator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;CalculateTotal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoiceRepository&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveToDatabase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InvoicePrinter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;PrintInvoice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;Each class now has a single responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Open/Closed Principle (OCP)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Software entities should be open for extension, but closed for modification."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This principle promotes the use of abstractions so that the behavior of a module can be extended without modifying its source code.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Bad Example
&lt;/h3&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;Discount&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;CalculateDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;customerType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customerType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Regular"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customerType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Premium"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&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;h3&gt;
  
  
  ✅ Good Example
&lt;/h3&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IDiscountStrategy&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;GetDiscount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegularCustomerDiscount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDiscountStrategy&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;GetDiscount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PremiumCustomerDiscount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IDiscountStrategy&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="nf"&gt;GetDiscount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.2&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;Now you can add new strategies without modifying existing code.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Liskov Substitution Principle (LSP)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Derived classes must be substitutable for their base classes."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This principle asserts that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program. In simpler terms, subclasses should enhance, not weaken or break, the behavior promised by the base class.&lt;/p&gt;

&lt;p&gt;Violating LSP often introduces unexpected behavior, especially in polymorphic code, and can lead to fragile systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Bad Example
&lt;/h3&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;Bird&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Fly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* default flying logic */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ostrich&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Bird&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Fly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotImplementedException&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;In this example, &lt;code&gt;Ostrich&lt;/code&gt; inherits from &lt;code&gt;Bird&lt;/code&gt; and overrides the &lt;code&gt;Fly&lt;/code&gt; method by throwing an exception. This violates LSP because any code that expects a &lt;code&gt;Bird&lt;/code&gt; to fly safely might crash or behave incorrectly when passed an &lt;code&gt;Ostrich&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Better Design
&lt;/h3&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IBird&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IFlyingBird&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IBird&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Fly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Sparrow&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IFlyingBird&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Fly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* flying logic */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ostrich&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IBird&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No Fly method; ostriches do not fly&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach respects LSP by not requiring &lt;code&gt;Ostrich&lt;/code&gt; to implement functionality it does not possess. Clients depending on &lt;code&gt;IFlyingBird&lt;/code&gt; know all implementations can fly, while those dealing with general &lt;code&gt;IBird&lt;/code&gt; don't make assumptions about flying.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Avoid forcing subclasses to override or disable base class functionality.&lt;/li&gt;
&lt;li&gt;Design with appropriate abstractions that reflect real capabilities.&lt;/li&gt;
&lt;li&gt;Ensure derived classes honor the expectations set by the base class.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following LSP, you ensure that components remain interchangeable, reducing bugs and making your code more robust and extensible.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Interface Segregation Principle (ISP)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Clients should not be forced to depend on interfaces they do not use."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ❌ Bad Example
&lt;/h3&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IWorker&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Eat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Robot&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IWorker&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Eat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;NotImplementedException&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;h3&gt;
  
  
  ✅ Good Example
&lt;/h3&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IWorkable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IEatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Eat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Human&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IWorkable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IEatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Eat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Robot&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IWorkable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;Separate interfaces make code more flexible.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Dependency Inversion Principle (DIP)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Depend on abstractions, not on concretions."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;High-level modules should not depend on low-level modules. Both should depend on abstractions.&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Bad Example
&lt;/h3&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;EmailService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notification&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;EmailService&lt;/span&gt; &lt;span class="n"&gt;_emailService&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;EmailService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendEmail&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Good Example
&lt;/h3&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IMessageService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailService&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IMessageService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notification&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;readonly&lt;/span&gt; &lt;span class="n"&gt;IMessageService&lt;/span&gt; &lt;span class="n"&gt;_messageService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMessageService&lt;/span&gt; &lt;span class="n"&gt;messageService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_messageService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;messageService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_messageService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&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="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;Now &lt;code&gt;Notification&lt;/code&gt; is not tightly coupled to &lt;code&gt;EmailService&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Applying the SOLID principles in .NET helps you build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modular and testable components&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easier-to-understand codebases&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalable applications ready for change&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While not every class needs to follow all principles strictly, being mindful of SOLID will gradually elevate the quality of your code.&lt;/p&gt;




&lt;p&gt;If you found this article helpful follow me on &lt;a href="https://github.com/markjackmilian" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://x.com/markjackmilian" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and &lt;a href="https://bsky.app/profile/markjackmilian.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; for more content.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>solidprinciples</category>
      <category>programming</category>
    </item>
    <item>
      <title>b-state behaviours and processors</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Sun, 11 May 2025 09:39:14 +0000</pubDate>
      <link>https://dev.to/markjackmilian/b-state-behaviours-and-processors-4l3m</link>
      <guid>https://dev.to/markjackmilian/b-state-behaviours-and-processors-4l3m</guid>
      <description>&lt;p&gt;A clean, composable way to handle cross-cutting concerns in Blazor with BState&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%2F5x46oozhcf12xeysihhr.webp" 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%2F5x46oozhcf12xeysihhr.webp" alt=" " width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/markjackmilian/b-state-blazor-state-manager-4828"&gt;previous article&lt;/a&gt;, I introduced B-State, a lightweight and elegant state management library designed for Blazor. We looked at how B-State treats actions as first-class citizens and how dispatching them affects state updates predictably.&lt;/p&gt;

&lt;p&gt;Now, let’s explore how B-State allows us to structure &lt;strong&gt;cross-cutting concerns&lt;/strong&gt; like logging, exception handling, or validation using a &lt;strong&gt;pipeline of behaviours and processors.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;🧱 The Pipeline Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;B-State&lt;/strong&gt;, actions flow through a processing pipeline that supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Behaviours&lt;/li&gt;
&lt;li&gt;Preprocessors&lt;/li&gt;
&lt;li&gt;Postprocessors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture is inspired by the middleware pattern commonly seen in ASP.NET Core and MediatR, but tailored to the Blazor component model and optimized for state flow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The goal is to decouple concerns such as logging, validation, or side-effects from the core business logic, contributing to a clean, maintainable architecture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s break it down.&lt;/p&gt;




&lt;p&gt;🔁 &lt;strong&gt;Behaviours&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Behaviours are like middleware. They wrap around the action execution and have full control over what happens before and after the action handler runs.&lt;/p&gt;

&lt;p&gt;Here’s a logging behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class LogBehaviour : IBehaviour
{
    public async Task Run(IAction parameter, Func&amp;lt;IAction, Task&amp;gt; next)
    {
        Console.WriteLine($"{parameter.GetType().Name} started");
        await next(parameter);
        Console.WriteLine($"{parameter.GetType().Name} ended");
    }
}  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Behaviours are global. They are registered once and wrap every action unless filtered internally.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;⏮️ &lt;strong&gt;Preprocessors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Preprocessors run &lt;strong&gt;before&lt;/strong&gt; the action handler is invoked.&lt;br&gt;
Example: A global preprocessor for &lt;strong&gt;all&lt;/strong&gt; actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class AllRequestPreprocessor&amp;lt;TAction&amp;gt; : IPreProcessor&amp;lt;TAction&amp;gt;
where TAction : IAction
{
    public Task Run(IAction parameter)
    {
        Console.WriteLine("AllRequestPreprocessor");
        return Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also &lt;strong&gt;conditionally execute preprocessors&lt;/strong&gt; based on action interfaces. For example, if you tag long-running operations with ILongAction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Marker interface
public interface ILongAction { }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class ALongActionPreProcessor&amp;lt;TRequest&amp;gt; : IPreProcessor&amp;lt;TRequest&amp;gt;
where TRequest : ILongAction
{
    public Task Run(IAction parameter)
    {
        Console.WriteLine("This is a long action... please wait!");
        return Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preprocessor is &lt;strong&gt;activated only when IAction implements ILongAction&lt;/strong&gt;. &lt;em&gt;This feature provides great flexibility when building the pipeline to manage your requests.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;⏭️ &lt;strong&gt;Postprocessors&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Postprocessors work &lt;strong&gt;exactly like preprocessors&lt;/strong&gt;, but are executed after the action handler runs. Use them for tasks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cleanup&lt;/li&gt;
&lt;li&gt;Logging results&lt;/li&gt;
&lt;li&gt;Updating external systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To implement one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class MyPostProcessor&amp;lt;TAction&amp;gt; : IPostProcessor&amp;lt;TAction&amp;gt;
where TAction : IAction
{
    public Task Run(IAction parameter)
    {
        Console.WriteLine("Action completed.");
        return Task.CompletedTask;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🧩 &lt;strong&gt;Putting It Together&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddBState(configuration =&amp;gt;
{
    configuration.RegisterFrom(Assembly.GetExecutingAssembly());

    configuration.AddBehaviour&amp;lt;ExceptionBehaviour&amp;gt;();
    configuration.AddBehaviour&amp;lt;LogBehaviour&amp;gt;();

    configuration.AddOpenRequestPreProcessor(typeof(AllRequestPreprocessor&amp;lt;&amp;gt;));
    configuration.AddOpenRequestPreProcessor(typeof(ALongActionPreProcessor&amp;lt;&amp;gt;));
    // configuration.AddOpenRequestPostProcessor(typeof(MyPostProcessor&amp;lt;&amp;gt;));
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;With &lt;strong&gt;behaviours, preprocessors, and postprocessors&lt;/strong&gt;, B-State introduces a powerful yet elegantly simple pipeline model for handling cross-cutting concerns in Blazor applications. Instead of embedding logging, error handling, loading indicators, or validation logic directly into your components or action handlers, you delegate them to modular units that wrap or surround the core logic. This keeps your actions focused on business rules, while the supporting concerns &lt;strong&gt;remain isolated, reusable, and composable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The key principle here is &lt;strong&gt;composability&lt;/strong&gt;. Each behaviour or processor is independently testable, context-agnostic, and easily replaceable, enabling teams to iterate faster without fear of regressions or side effects. Whether you’re building large enterprise applications or small Blazor components, this structure brings clarity, separation of concerns, and scalability.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;B-State’s pipeline architecture provides more than just state management — it offers a clean, extensible framework to handle the complexities that naturally emerge as your application grows. By leveraging behaviours and processors, you gain precise control over how actions are prepared, executed, and handled, while keeping your application logic lean and expressive.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;For a repo example of this tutorial see &lt;a href="https://github.com/markjackmilian/bstate.tutorial/tree/tutorial1" rel="noopener noreferrer"&gt;HERE&lt;/a&gt;, for a more complete example &lt;a href="https://github.com/markjackmilian/b-state/tree/main/bstate/bstate.web.example" rel="noopener noreferrer"&gt;HERE&lt;/a&gt;&lt;br&gt;
If you found this article helpful, leave a ⭐️ on the &lt;a href="https://github.com/markjackmilian/b-state" rel="noopener noreferrer"&gt;library’s repository&lt;/a&gt; on GitHub and follow me on &lt;a href="https://github.com/markjackmilian" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://x.com/markjackmilian" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and &lt;a href="https://bsky.app/profile/markjackmilian.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; for more content.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>dotnet</category>
      <category>webdev</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>b-state Blazor state manager</title>
      <dc:creator>Mark Jack</dc:creator>
      <pubDate>Tue, 06 May 2025 10:31:28 +0000</pubDate>
      <link>https://dev.to/markjackmilian/b-state-blazor-state-manager-4828</link>
      <guid>https://dev.to/markjackmilian/b-state-blazor-state-manager-4828</guid>
      <description>&lt;p&gt;When building modern web applications, managing state efficiently becomes crucial. In Blazor, as in other front-end frameworks, state management is key to ensuring that UI updates are predictable, scalable, and easy to maintain. That’s where b.state comes in — a lightweight, pipeline-based state manager designed specifically for Blazor applications.&lt;br&gt;
b.state introduces a fresh, structured approach to handling state by organizing the flow of state changes through a customizable pipeline architecture. This design not only improves clarity and modularity but also enables the use of middleware, allowing developers to inject logic (such as logging, validation, or side-effects) at different stages of the state update process.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’re familiar with the Flux pattern (or Redux in the React ecosystem), you’ll find many familiar concepts in b.state, adapted to the Blazor architecture. Both follow a unidirectional data flow and rely on actions to describe state changes. In the table below, you can see a side-by-side comparison to help map concepts between the two approaches.&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%2F3lfqgm7ocntw1yx3pw0p.webp" 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%2F3lfqgm7ocntw1yx3pw0p.webp" alt=" " width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we’ll dive into how b.state works, why a pipeline-based model makes sense for state management, and how you can integrate middleware to build more robust and maintainable Blazor applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/markjackmilian/b-state" rel="noopener noreferrer"&gt;b-state Github Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this introductory tutorial, we’ll replace the default Counter page from the Blazor template using BState. Let’s begin by creating a new Blazor WebAssembly project and adding the following NuGet package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package b-state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your Program.cs add a basic bstate startup (an advanced setup will be shown in the next story):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddBState(configuration =&amp;gt;
{
    configuration.RegisterFrom(typeof(Program).Assembly);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;RegisterFrom&lt;/em&gt; method ensure that all Actions and States are automatically registered into application services.&lt;br&gt;
Create a new state called CounterState:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public partial class CounterState(IActionBus actionChannel) : BState(actionChannel)
{
    public int Counter { get; private set; } = 100;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Thanks to the bstate.analyzer project, your IDE will show any discrepancies from project conventions as warnings:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setters must be private inside bstate derived classes&lt;/li&gt;
&lt;li&gt;IAction implementation must be subclasses of a bstate class&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next step is to define the Actions that can modify the CounterState, to do this, I added a CounterState.Actions.cs file with the partial class that contains all the state edit actions, in this case the only possible action is Add&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public partial class CounterState
{
    record AddAction : IAction;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it’s time to create the action that will modify the state, i created a new file called CounterState.AddAction.cs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public partial class CounterState
{
    class AddActionHandler(CounterState counterState) : IActionHandler&amp;lt;AddAction&amp;gt;
    {
        public Task Execute(AddAction request)
        {
            counterState.Counter++;
            return Task.CompletedTask;
        }
    }

    public Task Add() =&amp;gt; this.ActionChannel.Send(new AddAction());
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I implemented IActionHandler as a private class, incrementing the counter in the Execute method.&lt;br&gt;
I also added a public Add() method that internally sends the AddAction.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;bstate pipeline flow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an IAction is executed through the ActionChannel, a pipeline is built to correctly manage the action.&lt;br&gt;
Several elements are added to this pipeline: behaviors (which we will cover next time), preprocessors (also covered next time), the action executor (IActionHandler), and post-processors (which we will discuss later as well).&lt;br&gt;
Once the pipeline execution is complete, a notification process is triggered to inform all components that use the part of the state affected by the 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%2Fpy795u4xdv6rukrp3o5z.webp" 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%2Fpy795u4xdv6rukrp3o5z.webp" alt=" " width="427" height="640"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;bstate component&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, for the final step of this tutorial, we will create the component.&lt;br&gt;
The component should inherit from BStateComponent and access one or more states by using the UseState method.&lt;br&gt;
This will allow the component to automatically react to any changes in the state and update itself accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@using bstate.tutorial.Features.Counter
@inherits bstate.core.Components.BStateComponent

&amp;lt;p role="status"&amp;gt;Current count: @State.Counter&amp;lt;/p&amp;gt;

&amp;lt;button class="btn btn-primary" @onclick="Increment"&amp;gt;Click me&amp;lt;/button&amp;gt;

@code {
    CounterState State =&amp;gt; this.UseState&amp;lt;CounterState&amp;gt;();
    Task Increment() =&amp;gt; this.State.Add();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tutorial is just the first step in introducing how to implement state management using BState in a Blazor application.&lt;br&gt;
In the next parts, we will explore more advanced topics, such as how to use behaviors, processors, and lifecycle extensions to further enhance the capabilities and flexibility of your components.&lt;/p&gt;




&lt;p&gt;For a repo example of this tutorial see &lt;a href="https://github.com/markjackmilian/bstate.tutorial/tree/tutorial1" rel="noopener noreferrer"&gt;HERE&lt;/a&gt;, for a more complete example &lt;a href="https://github.com/markjackmilian/b-state/tree/main/bstate/bstate.web.example" rel="noopener noreferrer"&gt;HERE&lt;/a&gt;&lt;br&gt;
If you found this article helpful, leave a ⭐️ on the &lt;a href="https://github.com/markjackmilian/b-state" rel="noopener noreferrer"&gt;library’s repository&lt;/a&gt; on GitHub and follow me on &lt;a href="https://github.com/markjackmilian" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, &lt;a href="https://x.com/markjackmilian" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; and &lt;a href="https://bsky.app/profile/markjackmilian.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; for more content.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>blazor</category>
      <category>state</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
