<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Sat, 26 Sep 2026 02:53:13 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=berkansasmaz" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-the-complete-series-f7jute7n</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-the-complete-series-f7jute7n</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>abp-studio</category>
      <category>ai</category>
      <category>abp-platform</category>
      <title>Deep Dive on ABP AI Agent: The Complete Series</title>
      <description>ABP Studio is a development platform built around ABP Framework. With the introduction of ABP AI Coding Agent, it became something more: a platform where an AI agent works inside the same environment you already use to build, run, monitor, and ship ABP solutions.

</description>
      <pubDate>Tue, 23 Jun 2026 08:23:28 Z</pubDate>
      <a10:updated>2026-09-25T20:32:28Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent: The Complete Series</h1>
<p>ABP Studio is a development platform built around ABP Framework. With the introduction of <strong>ABP AI Coding Agent</strong>, it became something more: a platform where an AI agent works inside the same environment you already use to build, run, monitor, and ship ABP solutions.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-23-deep-dive-index/abp-studio-new-design.png" alt="ABP Studio with ABP AI Agent" /></p>
<p>General-purpose AI coding tools are excellent for horizontal, file-shaped work. They read source files, edit them, and run shell commands. But ABP solutions are <strong>system-shaped</strong>, not just file-shaped. A typical ABP solution is split across multiple modules and layers with strict dependency rules, composed of many runnable units (HTTP services, gateways, identity servers, background workers, Docker containers), and built on a strong set of conventions: aggregate roots, repositories, application services, DTOs, permissions, localization, event bus, distributed cache, and background jobs.</p>
<p>A generic agent has none of that vocabulary. It does not know what a module is, which project is the Domain layer, or that an <code>ApplicationService</code> should not depend on a <code>DbContext</code> directly. It cannot start your microservices, gateway, and auth server together. It cannot tell you that the latest edit caused a runtime exception in the Identity service, because it has no concept of a running application.</p>
<p>ABP AI Coding Agent was built to close exactly that gap. The agent is born inside a platform that already understands modules, run profiles, builds, migrations, proxies, Docker containers, monitoring, and Git workflows, and it uses every one of them.</p>
<p>We wrote a <strong>nine-part deep dive series</strong> to explain how each part of this system works, not as a product tour, but as a practical look at the decisions, controls, and architecture behind the experience.</p>
<h2>The Series</h2>
<ol>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-1-agent-plan-and-ask-modes-62wteg9t">Agent, Plan and Ask Modes</a></strong> — The three interaction modes that control how much action the agent is allowed to take: <strong>Ask</strong> for understanding (read-only), <strong>Plan</strong> for designing the approach before editing, and <strong>Agent</strong> for full implementation with builds, tools, and iteration.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-2-supported-ai-models-in-abp-3krbc7yc">Supported AI Models + Usage Recommendations</a></strong> — How ABP Studio separates models by role (main, research, browser, text processor, Git review) and why treating model selection as a practical decision based on the task leads to a better balance of capability, speed, and cost.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-3-rules-skills-and-lessons-ai6kxubt">Rules, Skills and Lessons</a></strong> — The three mechanisms that give the agent solution-specific memory: <strong>Rules</strong> (always-on conventions), <strong>Skills</strong> (on-demand procedures), and <strong>Lessons</strong> (corrections the agent records and carries forward).</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-4-integrated-abp-studio-tools-be2xa2om">Integrated ABP Studio Tools</a></strong> — The tools that connect the agent to ABP Studio's runtime environment: monitoring (exceptions, logs, requests), applications, containers, tasks, and build actions, with a practical walkthrough showing the difference between debugging with and without tool access.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-5-mcp-model-context-protocol-trb9o4ev">MCP (Model Context Protocol)</a></strong> — How MCP extends the agent beyond the solution boundary to reach external systems like Prometheus, SEO analyzers, or documentation services, with per-tool enable/disable controls and stdio/HTTP server support.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-6-abp-studio-git-integration-09tr41ec">ABP Studio Git Integration</a></strong> — The full Git loop inside ABP Studio: branching, diffing, AI-generated commit messages, AI code review on staged changes, GitHub issue context for starting tasks, and pull request feedback for addressing reviewer comments.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-7-scopes-tfqtkdzu">Scopes</a></strong> — How AI Scopes restrict the agent's working area to specific modules, packages, or solution areas, reducing unrelated exploration, preventing accidental edits, and making diffs easier to review.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-8-parallel-agent-execution-1o0cik6g">Parallel Agent Execution</a></strong> — Running multiple agent sessions at the same time, each with its own mode, model, scope, and workflow, plus read-only research subagents that fan out inside a single session.</p>
</li>
<li><p><strong><a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-9-workflows-7jo1adb1">Workflows</a></strong> — Repeatable before/after steps that wrap agent runs: start containers, build packages, add migrations, generate proxies, restart applications, and run validation tasks, so the agent focuses on the code change while the platform handles the deterministic parts.</p>
</li>
</ol>
<h2>The Bigger Picture</h2>
<p>Each article focuses on one feature, but the real value comes from how they work together.</p>
<p>Modes decide how much action the agent takes. Models decide which brain handles the work. Rules, Skills, and Lessons shape what the agent knows. Tools and MCP extend what it can reach. Scopes define where it can work. Workflows define what happens around the work. Git Integration makes the result reviewable and recoverable. Parallel Execution lets multiple tasks move forward at the same time.</p>
<p>That is the ABP AI Coding Agent experience: <strong>not a single AI button, but a set of controls built into a platform that already understands how ABP solutions are developed, run, and maintained.</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a2205c0-9fc8-9fea-e25a-d51ce8fe3f7a" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a2205c0-9fc8-9fea-e25a-d51ce8fe3f7a" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-8-parallel-agent-execution-1o0cik6g</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-8-parallel-agent-execution-1o0cik6g</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>concurrency</category>
      <category>abp-studio</category>
      <category>ai</category>
      <title>Deep Dive on ABP AI Agent #8: Parallel Agent Execution</title>
      <description>While the agent is busy adding a feature, a question comes up about a different module. A review is waiting. A small fix would take two minutes, but the agent is in the middle of something else. With a single session, you wait. You watch one task finish before you can start the next, even when the two have nothing to do with each other.

ABP Studio does not make you wait. It can run several agent sessions at the same time, each on its own task.</description>
      <pubDate>Thu, 18 Jun 2026 06:14:13 Z</pubDate>
      <a10:updated>2026-09-25T21:30:06Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent #8: Parallel Agent Execution</h1>
<p>Real work rarely lines up one task at a time.</p>
<p>While the agent is busy adding a feature, a question comes up about a different module. A review is waiting. A small fix would take two minutes, but the agent is in the middle of something else. With a single session, you wait. You watch one task finish before you can start the next, even when the two have nothing to do with each other.</p>
<p>ABP Studio does not make you wait. It can run several agent sessions at the same time, each on its own task.</p>
<h2>Parallel Agent Sessions</h2>
<p>A session is one conversation with the agent. It has its own history, its own mode, its own model, its own scope, and its own workflow. ABP Studio keeps multiple sessions per solution, and they can run in parallel.</p>
<p>There is a limit, and it is there on purpose. By default you can run up to <strong>3 sessions at once</strong>, and you can set that anywhere from <strong>1 to 5</strong>. When you send more prompts than there are open slots, the extra ones are queued and start as soon as a slot frees up.</p>
<p>So &quot;parallel&quot; here is not a trick of switching back and forth quickly. The sessions actually run at the same time, up to the limit you choose.</p>
<h2>Parallel Session Use Cases</h2>
<p>The point is to stop letting one task block another. A few ways it plays out:</p>
<ul>
<li>One session implements a feature while another answers questions about a different part of the solution.</li>
<li>Two unrelated modules get worked on at once, each in its own session.</li>
<li>One session writes the code while another reviews a diff or does research.</li>
</ul>
<p>Because each session is separate, you can even point them at different parts of the solution and give them different jobs:</p>
<pre><code class="language-text">Session 1 (Agent): Add a Category screen to the Catalog module.
Session 2 (Ask): Explain how permissions flow from the API to the UI.
</code></pre>
<p>The first one edits files. The second one only reads and answers. They do not interfere, because they are different sessions with different settings.</p>
<h2>Per-Session Settings Isolation</h2>
<p>This is the part that makes parallel work predictable instead of chaotic.</p>
<p>When a session sends its first message, two things happen at different levels. The session permanently locks its <strong>scope</strong> and <strong>workflow</strong>—these stay fixed for the entire session lifetime and cannot be changed once the first message is sent. On the other hand, the <strong>model</strong>, <strong>mode</strong>, and <strong>active tools</strong> are snapshotted fresh at each run (each turn the agent takes), so they reflect whatever is configured at the moment the run starts but remain stable for its duration.</p>
<p>That matters the moment you have more than one session open. Say one session is running in the background, scoped to the <code>Catalog</code> module. You switch the foreground to a different scope to start a second task. The background session does not notice. It keeps the scope and workflow it was locked to, and each of its runs uses whatever model and tools were configured at the moment that run began.</p>
<p>Without this, parallel sessions would quietly corrupt each other every time you changed a setting. With it, each session is a sealed unit of work.</p>
<h2>The Prompt Queue</h2>
<p>You do not have to wait for a session to be idle to line up its next step.</p>
<p>While a session is running, you can queue more prompts on it. They attach to the same session and are sent one after another, each after the current turn finishes. The queue keeps the session's settings, so a queued prompt runs with the same mode, scope, model, workflow, and active plan as the rest of that session.</p>
<p>This works together with the concurrency limit. Prompts that cannot start right away, because every slot is busy, simply wait their turn instead of failing.</p>
<h2>Keeping Parallel Work Safe</h2>
<p>Running several sessions at once is powerful, and it also gives you a new way to get in your own way: two sessions editing the same files.</p>
<p>ABP Studio helps here, but it does not pretend the problem away. It tracks file changes, so if one session edited a file after another session read it, the second is told to read it again before overwriting. It also serializes operations that cannot safely overlap, such as adding a migration while a build is running, so two sessions do not run conflicting <code>dotnet</code> commands at the same time. And because each session tracks its own pending questions, one session waiting on your input does not freeze the others.</p>
<p>Still, the simplest rule is the best one: keep parallel Agent-mode sessions on separate parts of the solution. This is exactly where scopes earn their place. Give each session its own scope, and they stay in their own lanes by design instead of by luck.</p>
<h2>A Second Kind Of Parallel: Subagents</h2>
<p>There is also a smaller, quieter form of parallel work that happens inside a single session.</p>
<p>When the agent needs to look something up, it can fan out multiple <strong>subagents</strong> in a single turn: read-only helpers that research in parallel and return their results before the main agent continues. There are a few kinds, each with a narrow job:</p>
<ul>
<li>one that researches your solution and code,</li>
<li>one that searches the web,</li>
<li>one that searches and reads the official ABP documentation.</li>
</ul>
<p>These research-style subagents (code research, web search, documentation search) are read-only—they cannot modify files or solution state. They only read, gather, and hand back a short summary. That keeps the main session's context clean, because the digging happens elsewhere and only the answer comes back. (Note that the browser subagent is an exception: it is stateful and can mutate the shared browser session.)</p>
<p>ABP Studio can use a separate model for research subagents, configured apart from your main one. A fast, cheap model is the right choice for this kind of lookup work.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-8-parallel-agent-execution/model-settings.png" alt="Model Settings: the research model used by subagents is set separately from the main model" /></p>
<p>So there are two scales of parallel here. Sessions run independent tasks side by side. Subagents run read-only research inside one task. Both keep the slow parts from blocking the useful parts.</p>
<h2>ABP Studio Parallel Execution Model</h2>
<p>Plenty of tools let you open more than one chat, and that is genuinely useful. So the idea of running agents in parallel is not, by itself, an ABP feature.</p>
<p>The difference is that each session here carries the context of an ABP solution and stays inside it. A session locks its scope and workflow permanently, and snapshots its model and tools per run, so background work does not drift when you change the foreground. Studio coordinates the operations that would otherwise collide, like builds and migrations, across all the running sessions. And scopes give each session a clear boundary, so parallel does not turn into a pile of agents editing the same files.</p>
<p>In short, the parallelism is not just several chat windows. It is several controlled, solution-aware sessions that know how to stay out of each other's way.</p>
<h2>Conclusion</h2>
<p>Parallel execution is about respecting how work actually arrives: more than one thing at a time, often unrelated.</p>
<p>ABP Studio lets you run several agent sessions together, each sealed to its own settings, with a queue for what comes next and guardrails for the places where parallel work could collide. Inside each session, subagents add a second layer of parallel research without cluttering the main task.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21eb8a-7c0f-f050-17f4-0def6dbfd1bf" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21eb8a-7c0f-f050-17f4-0def6dbfd1bf" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-5-mcp-model-context-protocol-trb9o4ev</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-5-mcp-model-context-protocol-trb9o4ev</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>abp-studio</category>
      <category>ai</category>
      <category>ABP Framework</category>
      <category>MCP</category>
      <title>Deep Dive on ABP AI Agent #5: MCP (Model Context Protocol)</title>
      <description>MCP is an open standard for connecting external tools and data to AI agents.</description>
      <pubDate>Mon, 15 Jun 2026 06:06:24 Z</pubDate>
      <a10:updated>2026-09-26T01:47:56Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent #5: MCP (Model Context Protocol)</h1>
<p>When I use an AI coding agent inside ABP Studio, most of the work starts inside the solution.</p>
<p>The agent can read the code. It can edit files. It can use the ABP Studio tools I enabled. It can build the solution, run tasks, start applications, inspect containers, and check monitoring data when something fails. <a href="https://abp.io/community/articles/deep-dive-on-abp-ai-agent-4-integrated-abp-studio-tools-be2xa2om">In the previous article of this</a> series, we focused on exactly that: the integrated tools that connect the agent to the ABP Studio development environment.</p>
<p>But there is another point where plain solution awareness is not enough.</p>
<p>Sometimes the information I need is not in the repository. It is not in the running application. It is not in the build output, container state, or ABP Studio monitoring screen.</p>
<p>It may be in a live Prometheus workspace. It may be in Google Search Console. It may be in an SEO analyzer, a documentation system, a database, a customer support tool, or another service that has nothing to do with the ABP solution itself.</p>
<p>That is where <strong>MCP</strong> becomes useful.</p>
<p>MCP, short for <strong>Model Context Protocol</strong>, gives ABP AI Coding Agent a standard way to reach external tools and data sources. It does not replace the built-in ABP Studio tools. It extends the agent beyond the solution boundary when the task depends on something outside that boundary.</p>
<blockquote>
<p><strong>Note:</strong> ABP AI Coding Agent is available directly to ABP license holders. License holders have predefined credits so they can try the integrated experience without setting up a separate AI workflow first. MCP is one of the ways this experience can be extended when the agent needs access to systems outside ABP Studio.</p>
</blockquote>
<h2>Why MCP Matters</h2>
<p>Most coding agents start from the same place: <strong>the files they can see</strong>.</p>
<p>That is a good start, but real development work often needs context from somewhere else: production metrics, SEO data, analytics, search performance, customer records, internal APIs, or product knowledge.</p>
<p>If the agent cannot reach those systems, the developer becomes the bridge:</p>
<ul>
<li>Copy this metric result.</li>
<li>Paste that SEO report.</li>
<li>Export this analytics data.</li>
<li>Summarize this dashboard.</li>
<li>Explain which production signal matters.</li>
<li>Tell the agent what the external system says.</li>
</ul>
<p>That works, but it is not the best workflow. The more information I manually copy into the chat, the easier it is to miss something, simplify too much, or give the agent an outdated snapshot.</p>
<p>MCP reduces that manual work. It lets the agent ask an external system for context directly, within the permission boundary I configure.</p>
<p>The important detail is that MCP is not &quot;the agent can do anything now.&quot; It is a controlled extension point. A server is connected. Its tools are listed. Individual tools can be enabled or disabled. The agent can only use what is available in the current session.</p>
<p>That gives me a practical balance: the ABP-aware development experience stays in ABP Studio, and external context can be added only when the task needs it.</p>
<h2>What MCP Actually Is</h2>
<p>MCP is an open standard for connecting external tools and data to AI agents.</p>
<p>Instead of every tool inventing its own integration for every agent, MCP defines a common shape. A program that exposes tools or resources through this standard is called an <strong>MCP server</strong>. An agent that understands MCP can connect to that server and use what it provides.</p>
<p>For me, the easiest way to think about it is simple:</p>
<ul>
<li>ABP Studio tools connect the agent to the ABP development environment.</li>
<li>MCP servers connect the agent to systems outside that environment.</li>
</ul>
<p>The server might expose a tool for querying Prometheus, checking Search Console data, running an SEO audit, inspecting a database, or calling an internal API. The exact capability depends on the server.</p>
<p>MCP is not a prompt, a rule, a skill, or a lesson. Those shape what the agent knows or how it behaves. MCP changes what the agent can reach.</p>
<p>That makes MCP more like equipment than instruction. It gives the agent access to a tool or data source that was previously outside its working area.</p>
<p>There are already many community and vendor MCP servers for different systems. A useful place to discover examples is the <a href="https://github.com/punkpeye/awesome-mcp-servers">awesome-mcp-servers</a> repository, which collects MCP servers across many categories.</p>
<p>For concrete examples, there are MCP servers for <a href="https://github.com/pab1it0/prometheus-mcp-server">Prometheus metrics</a>, <a href="https://github.com/awslabs/mcp/tree/main/src/prometheus-mcp-server">AWS Managed Prometheus</a>, and <a href="https://github.com/g-battaglia/mcp-seo">SEO analysis</a>. The exact server you choose depends on your environment, but the pattern is the same: expose a focused external capability to the agent through MCP.</p>
<h2>When You Actually Need It</h2>
<p>Most of the time, I would not start with MCP.</p>
<p>For normal ABP development, the built-in tools are usually the right first layer. The agent can already work with the solution, run profiles, applications, containers, tasks, builds, monitoring data, and documentation. If the task is completely inside the ABP solution, MCP may not add anything.</p>
<p>MCP becomes useful when the task crosses the solution boundary.</p>
<p>For example, I may ask:</p>
<pre><code class="language-text">Run an SEO audit for https://abp.io, summarize the weak areas, and suggest which content or technical improvements would matter most.
</code></pre>
<p>Without MCP, I would need to open a separate SEO tool, run the audit, copy the results, and paste them into the chat. With the right MCP server enabled, the agent can call the SEO tool directly and reason from the structured result.</p>
<p>Or I may ask:</p>
<pre><code class="language-text">Connect to our Prometheus MCP server, check the error rate and p95 latency for the AuthServer over the last 30 minutes, and tell me whether the last deployment changed anything.
</code></pre>
<p>That is a very different kind of task. The answer is not in the source code alone. It lives in a monitoring system. Prometheus MCP servers exist for this kind of workflow; for example, some expose tools for instant PromQL queries, range queries, metric discovery, and target inspection.</p>
<p>Another realistic prompt could be:</p>
<pre><code class="language-text">Use Search Console data to find pages that lost traffic this month, then inspect the related docs pages and suggest focused improvements.
</code></pre>
<p>Again, the important part is not that the agent magically knows everything. The important part is that I can connect a specific external system, expose a specific set of tools, and let the agent use them when they are relevant.</p>
<p>If the work starts outside the codebase but ends inside the codebase, MCP can help connect those two parts of the workflow.</p>
<h2>Setting Up An MCP Server</h2>
<p>MCP servers are configured under <strong>Settings &gt; MCP Servers</strong>.</p>
<p>When no server is connected, the page is intentionally simple. It is an empty list waiting for the first server. That is important because MCP should be explicit. If I have not connected a server, the agent should not behave as if it has access to that external system.</p>
<p><strong>Add integration</strong> opens a curated gallery of common MCP servers. Each entry shows information such as whether it is official, read-only, authenticated by the provider, or runs a local command. If the integration has configurable values, ABP Studio opens a focused setup form and lets me update those values later. New integrations are disabled until I review and enable them.</p>
<p>There are two main connection types:</p>
<ul>
<li><strong>Stdio:</strong> ABP Studio runs the MCP server as a local process. I provide the command, arguments, and environment variables the server needs.</li>
<li><strong>HTTP:</strong> ABP Studio connects to an MCP server over the network. I provide the URL and any required headers.</li>
</ul>
<p>This is useful because different MCP servers are packaged in different ways. Some are local command-line programs. Some are hosted services. Some need environment variables for tokens or configuration.</p>
<p>For integrations outside the gallery, <strong>Edit JSON</strong> opens the complete server collection as one <code>mcpServers</code> document. ABP Studio validates and applies the entire document together, so a server omitted from the document is removed. Environment-variable and HTTP-header secrets are stored separately and appear in the editor as <code>${secret:NAME}</code> placeholders.</p>
<p>See <a href="https://github.com/abpframework/abp/blob/dev/docs/en/studio/ai-agent-configuration.md#mcp-tool-connections">ABP Studio: AI Agent Configuration</a> for the supported JSON fields and secret-storage behavior.</p>
<h2>What A Connected Server Shows</h2>
<p>After a server is connected, ABP Studio shows the important parts of the connection.</p>
<p>I can see whether the server is connected, how many tools it exposes, which tools are available, and whether it provides resources. I can also inspect resources directly from the settings page.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-5-mcp/seo-analyzer-mcp-tools.png" alt="A connected SEO Analyzer MCP server and its available tools" /></p>
<p>This visibility matters because an MCP server is not just a checkbox. It is a surface area. It may expose one tool or many. Some tools may be read-only, and some may perform actions outside the repository.</p>
<p>The agent does not need me to call those tools manually. I describe the task, and if the tool is available and relevant, the agent can decide to use it. That is why a prompt can stay natural:</p>
<pre><code class="language-text">Read the product requirement from the connected knowledge base and compare it with the current implementation.
</code></pre>
<p>The prompt does not need to become a tool invocation script. The agent still owns the reasoning loop. MCP only gives it a new place to look.</p>
<h2>Tool Access: What The Agent Is Allowed To Use</h2>
<p>The most important part of MCP in ABP Studio is not only connecting servers. It is controlling what the agent is allowed to use.</p>
<p>Individual tools can be disabled. If a tool is disabled, it is not offered to the agent. The agent should not plan around it, call it, or assume it has access to it.</p>
<p>That is useful for safety, but also for quality.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-5-mcp/seo-analyzer-mcp-tools-disabled-indivually.png" alt="Disabling individual tools exposed by the SEO Analyzer MCP server" /></p>
<p>Every enabled tool becomes part of the menu the agent considers. If a server exposes ten tools but the current task only needs two, I usually prefer to enable only those two. A shorter tool list is easier for the agent to choose from. It also makes the session easier for me to reason about.</p>
<p>The permission model is simple:</p>
<ul>
<li>If a server is not connected, the agent cannot use it.</li>
<li>If a server is disabled, the agent cannot use its tools.</li>
<li>If an individual tool is disabled, the agent cannot use that tool.</li>
<li>If the session is not in Agent mode, MCP tools are not available.</li>
</ul>
<p>That last point is important. MCP tools are available in <strong>Agent mode only</strong>.</p>
<p>Plan and Ask modes are read-only by design. They are useful for understanding, planning, and discussing changes, but they do not receive MCP tools. If I want the agent to call an external MCP tool, I need to work in Agent mode.</p>
<p>This keeps the model consistent with the rest of the ABP AI Coding Agent experience. The mode determines what kind of work the agent is allowed to perform.</p>
<h2>Keeping MCP Safe</h2>
<p>MCP is powerful because it lets an AI agent reach systems outside the solution.</p>
<p>That is also why it needs care.</p>
<p>An MCP server is a program. It may read files, call APIs, query databases, send requests, or perform actions depending on how it was built. The agent is not inventing those capabilities. It is using what the server exposes.</p>
<p>So trust matters at two levels.</p>
<p>First, I need to trust the actions. If a tool can update an issue, send a message, change a record, or trigger a workflow, that is a real side effect.</p>
<p>Second, I need to trust the descriptions. When an MCP server connects, the names and descriptions of its tools become part of what the model can read. A careless or hostile server can use that text to influence the model before I even write my prompt.</p>
<p>That means MCP safety is not only about &quot;what can this tool do?&quot; It is also about &quot;what instructions or descriptions does this server place in front of the model?&quot;</p>
<p>Two habits keep this manageable:</p>
<ul>
<li>Connect only servers I trust.</li>
<li>Disable tools the agent should not use for the current task.</li>
</ul>
<p>This sits on top of the agent's normal guardrails. Shell commands, URL fetches, downloads, and other sensitive actions can still require permission. Also, the tool list is fixed for a running session, so changing a server in the middle of a session does not quietly alter the tool surface already given to that session.</p>
<p>That is the behavior I want from this kind of integration. MCP can extend the agent, but it should do so through visible, intentional configuration.</p>
<h2>A Practical MCP Walkthrough</h2>
<p>Let me make this more concrete with a small scenario.</p>
<p>Imagine I am working on the public website or documentation side of an ABP-based product. The task is not a compiler error or a failing unit test. I want to understand how the website looks from an SEO perspective and what I would improve first.</p>
<p>Without MCP, I would open a separate SEO tool, run the audit, wait for the result, copy the score, paste the table into the chat, and then ask the agent to interpret it. That is not terrible, but it turns me into a copy-paste integration layer.</p>
<p>With an SEO Analyzer MCP server connected, I can start from a better prompt:</p>
<pre><code class="language-text">Please rate the SEO work on the https://abp.io website on a scale of 1 to 10. I'd also like to know what you would do if you were in charge.
</code></pre>
<p>At that point, the agent can use the SEO Analyzer MCP tool instead of guessing from general SEO knowledge alone. It can call the audit tool, read the structured result, and turn it into a concrete improvement plan.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-5-mcp/using-mcp.png" alt="ABP AI Coding Agent using the SEO Analyzer MCP server" /></p>
<p>The value is not only that the agent saved me a few seconds. The value is that the agent starts from live tool output instead of a manually summarized report.</p>
<h3>First, Without The MCP Server</h3>
<p>If no SEO MCP server is connected, the agent cannot run the audit directly.</p>
<p>It may still give a reasonable answer from general knowledge. It may say that the site should have good titles, descriptions, headings, performance, backlinks, structured data, and content targeting. That advice can be useful, but it is generic.</p>
<p>For example, I might write:</p>
<pre><code class="language-text">Review the SEO of abp.io and tell me what to improve.
</code></pre>
<p>The agent can reason about common SEO practices, but it does not have a fresh audit result. It does not know which categories scored well, which areas are weak, or whether the live page data confirms the concern.</p>
<p>This is similar to debugging without monitoring tools. The agent can reason, but it is reasoning from weaker evidence.</p>
<h3>Then, With The MCP Server Enabled</h3>
<p>Now imagine the SEO Analyzer MCP server is connected, enabled, and the relevant audit tool is enabled.</p>
<p>I can ask:</p>
<pre><code class="language-text">Run an audit on https://abp.io, rate the result, and suggest the highest-impact improvements.
</code></pre>
<p>This time the agent can gather the external context first. In the screenshot, the server exposes tools like <code>run_audit_anonymous</code>, <code>run_audit</code>, <code>get_audit</code>, <code>list_audits</code>, <code>get_audit_pdf</code>, and <code>get_organic_traffic</code>. The agent uses the audit tool and then explains the result in human terms.</p>
<p>That changes the workflow:</p>
<ol>
<li>Run the external SEO audit.</li>
<li>Read the score and category breakdown.</li>
<li>Identify the weakest areas.</li>
<li>Suggest concrete technical or content improvements.</li>
<li>If needed, inspect the related website or documentation files.</li>
</ol>
<p>The agent is no longer guessing from best practices alone. It can connect the outside report to the actual website and then, if the relevant files are in the solution, help improve them.</p>
<h3>Another Example: Live Monitoring</h3>
<p>SEO is only one example. Monitoring is another strong fit for MCP.</p>
<p>Prometheus MCP servers can expose tools for PromQL instant queries, range queries, metric discovery, targets, and server information. That means I can ask operational questions in natural language while the agent queries the monitoring system through MCP.</p>
<p>For example:</p>
<pre><code class="language-text">Use the Prometheus MCP server to check p95 latency, request rate, and error rate for the public web application after the last deployment. If something changed, identify the most likely area to inspect in the ABP solution.
</code></pre>
<p>That does not mean the agent should blindly change production behavior. It means the agent can start from live evidence: metrics, trends, targets, and recent changes. Then it can use ABP Studio context to inspect the related application, module, or configuration.</p>
<h3>Combining MCP With ABP Studio Tools</h3>
<p>The best part is that MCP does not replace the ABP Studio tools from the previous article. It works beside them.</p>
<p>In a real workflow, MCP may provide the outside signal, and ABP Studio tools may provide the inside development loop:</p>
<ol>
<li>MCP brings in the external context.</li>
<li>ABP-aware reasoning maps it to the solution.</li>
<li>ABP Studio tools help implement and validate the change.</li>
</ol>
<p>That is where the integration becomes more useful than a generic MCP checkbox. The agent is connected to an external tool while still understanding the ABP development environment.</p>
<h2>How MCP Fits With Everything Else</h2>
<p>MCP is an open standard, so connecting an MCP server is not an ABP-only idea.</p>
<p>Tools like Cursor, Claude, VS Code extensions, and other AI coding environments can also use MCP. That is part of its appeal. Teams can build or adopt a server once and use it across different tools.</p>
<p>What makes MCP especially useful in ABP Studio is the company it keeps. ABP AI Coding Agent already understands the ABP solution structure, application layers, modules, migrations, proxies, build actions, run profiles, containers, monitoring data, and official ABP documentation. MCP adds the outside world to that picture.</p>
<p>So I do not need to choose between:</p>
<ul>
<li>an agent that understands ABP,</li>
<li>and an agent that can reach my team's external systems.</li>
</ul>
<p>The better experience is both together, under visible configuration. Put simply:</p>
<ul>
<li>Built-in ABP Studio tools handle the solution and runtime environment.</li>
<li>MCP servers handle external tools and data sources.</li>
<li>Rules, skills, and lessons shape how the agent behaves.</li>
<li>Agent mode decides whether tool use is available for the session.</li>
</ul>
<p>Each part has a different role. Keeping those roles clear makes the agent easier to trust. This is also why I would start small in a real team workflow: one low-risk, high-value server, probably a read-only analytics, documentation, or monitoring server, with only the tools that support a clear workflow enabled.</p>
<p>MCP by itself is a protocol. The difference in ABP Studio is that MCP becomes part of an ABP development session. The agent can read an external SEO report or monitoring signal, but it can also understand which ABP application, module, page, or configuration owns the behavior and validate the change with ABP Studio tools.</p>
<p>Most real tasks are not only &quot;call an external tool.&quot; They are more like:</p>
<ol>
<li>Understand the requirement from outside the repository.</li>
<li>Find where that behavior lives in the ABP solution.</li>
<li>Make the smallest correct change.</li>
<li>Validate it in the same development environment.</li>
</ol>
<p>MCP helps with the first part. ABP Studio AI helps connect the rest.</p>
<p><strong>That makes MCP valuable not because it is another tool list, but because it extends the ABP AI Coding Agent beyond the repository without disconnecting it from the ABP workflow.</strong></p>
<h2>Conclusion</h2>
<p>MCP is the agent's connection to everything that is not already inside the ABP solution.</p>
<p>You will not need it for every task. But when the work depends on SEO data, live metrics, external documentation, internal services, or team knowledge, MCP gives the agent a standard way to reach that context.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21dc10-4191-3c8c-b4f4-2019d7bc8add" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21dc10-4191-3c8c-b4f4-2019d7bc8add" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-3-rules-skills-and-lessons-ai6kxubt</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-3-rules-skills-and-lessons-ai6kxubt</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>abp</category>
      <category>abp-studio</category>
      <category>ai</category>
      <title>Deep Dive on ABP AI Agent #3: Rules, Skills and Lessons</title>
      <description>A generic agent with no harness tuning stays roughly as capable on its thousandth task in your codebase as on its first. With Rules, Skills, and Lessons, ABP Agent accumulates solution-specific knowledge over time, especially through Lessons and the promotions you make from them.

</description>
      <pubDate>Thu, 11 Jun 2026 06:09:39 Z</pubDate>
      <a10:updated>2026-09-26T01:48:22Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent #3: Rules, Skills and Lessons</h1>
<p>Every new chat starts from zero.</p>
<p>The model can write the API, the permissions, and the UI. What disappears between sessions is everything specific to <em>this</em> solution: your naming rules, your feature checklist, the demo-data fix from yesterday. You end up re-teaching before you start building.</p>
<p>That is not an intelligence problem. The model is already capable enough. What does not carry over is the setup around it.</p>
<h2>Agent, Model, and Harness</h2>
<p>Three terms get mixed up constantly. Keeping them separate makes everything else in this article click.</p>
<p><strong>The model</strong> is the LLM: the weights, the training data, the reasoning engine. You do not get to change it. You pick which one to use, and that is about it.</p>
<p><strong>The harness</strong> is everything wrapped around the model so it can actually finish work in <em>your</em> environment: the system prompt, the tools it can call, the files it may read, the checks that run on its output, the limits on what it may touch, and the instructions injected into every session. A raw model is not an agent. An agent is the model plus the harness. If you are not the model, you are building the harness.</p>
<p><strong>The agent</strong> is the whole system: model plus harness, running in a loop until the task is done. The behaviour you experience is dominated by the harness, not just the model. <a href="https://addyosmani.com/blog/agent-harness-engineering/">Addy Osmani</a> puts the same idea plainly: <em>a decent model with a great harness beats a great model with a bad harness.</em> The gap between what today's models can do and what you actually see them do in your codebase is mostly a harness gap.</p>
<p>The habit that makes a harness good is simple: treat every mistake as a reason to update the setup, not just to fix the output in chat. Each time the agent gets something wrong, change what it always knows, can look up, or carries forward so it cannot repeat the same error. Do that consistently and the agent stops repeating mistakes and starts working the way your team works.</p>
<p>In <strong>ABP Studio AI Coding Agent</strong>, the harness includes three pieces that hold what the agent knows about your solution: <strong>Rules</strong>, <strong>Skills</strong>, and <strong>Lessons</strong>. They live under <strong>Settings &gt; Rules &amp; Skills</strong>.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-3-rules-skills-lessons/rules-and-skills-settings.png" alt="The Rules &amp; Skills settings page in ABP Studio" /></p>
<h2>Rules: The House Rules On The Wall</h2>
<p>A <strong>Rule</strong> is a standing instruction that the harness injects into the agent's context on every turn of every session. In harness-engineering terms, it is always-on context injection: the same role as an <code>AGENTS.md</code> or <code>CLAUDE.md</code> at the root of a repo, except scoped to your solution or your profile inside ABP Studio.</p>
<p>Think of the house rules pinned to a kitchen wall: how we do things here, every shift, no exceptions. Unlike a glance at a poster, though, a Rule is not ambient background. It is read on every turn, so it costs context budget every time the agent acts.</p>
<p>ABP Agent already follows the framework's own conventions by default. It goes through the data layer instead of hitting the database directly, uses the built-in permission system instead of hand-rolled checks, and reads user-facing text from the translation files instead of hardcoding it. You do not write those down.</p>
<p>Rules are where <em>your</em> conventions go. The ones the framework cannot guess because they belong to your solution and your team. The good part is that these read like plain conventions any developer would recognize, not framework trivia:</p>
<ul>
<li>New code goes in the same place, with the same naming, as the code already around it.</li>
<li>Every endpoint that changes data checks permissions before it does anything.</li>
<li>Money is stored in minor units (cents), never as a floating-point number.</li>
<li>API errors use our standard response shape, never ad-hoc JSON.</li>
</ul>
<p>With those on the wall, I stop repeating them. Instead of writing this:</p>
<pre><code class="language-text">Add a Category screen. Use our data layer, do not query the database
directly. Check permissions on every write. Keep money in cents. Put the
files where the other features live.
</code></pre>
<p>I can write this:</p>
<pre><code class="language-text">Add a Category screen.
</code></pre>
<p>The conventions are not renegotiated, one prompt at a time, in every session. The agent applies them the way a teammate who has read the contributor guide would.</p>
<p>This matters more than it looks. A model with no standing rules does not stay neutral. It falls back on the defaults from its training data. Ask a generic model for &quot;an endpoint that returns categories&quot; and you often get one with the database query written straight into the handler, because that is the most common shape on the public internet. Rules steer the agent's output back toward <em>your</em> codebase.</p>
<p>One caution: treat the rule list like a pilot's checklist, not a style guide. A Rule is injected on every turn, so keep the list short. Every line should earn its place, ideally traceable to a real failure or a hard constraint you cannot ignore. If a line does not change what the agent produces, it is noise, and it dilutes the rules that actually matter.</p>
<p>You also choose where a rule is saved. Global rules sit under your user profile and apply to every solution on your machine. Solution rules apply only to the current solution, and only while it is open.</p>
<h2>Skills: Recipes You Pull Off The Shelf</h2>
<p>A <strong>Skill</strong> is the same kind of note as a Rule, but loaded only when the task calls for it. The harness keeps a short description of each skill in context at all times; the agent reads the full text only when that description matches what you asked it to do. Harness engineers call this <strong>progressive disclosure</strong>: reveal instructions and detail only when they are needed, instead of stuffing everything into the opening prompt.</p>
<p>A recipe is not pinned to the wall. It sits in a drawer, and the cook pulls it out only when making that dish. That is a Skill.</p>
<p>In ABP Studio, a rule and a skill are the same kind of note with one switch between them.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-3-rules-skills-lessons/create-rule-skill-dialog.png" alt="Creating a rule or a skill: the Always Apply toggle decides which one it is" /></p>
<p>You write a name (which becomes the file name) and some content, then set <strong>Always Apply</strong>. Turn it on and the note is a Rule, always in context. Turn it off and the note is a Skill, fetched on demand. So if a skill turns out to be something the agent should never skip, you flip one switch and it becomes a rule.</p>
<p>My favorite example of a skill is a &quot;build a feature end to end&quot; checklist: the ordered steps your team follows to ship one complete feature. The data model comes first, then the data access layer, then the application service, then the API, then the translations, then the UI, and finally the demo data. Write it once, and the agent follows it whenever it builds a feature, instead of inventing its own order each time.</p>
<p>Writing a skill is like writing an onboarding note for a new teammate. You are not making them smarter. You are saving them the week it would take to work out how your team does this one thing.</p>
<p>A skill can be long without slowing anything down. The agent sees only the short description of each skill at all times, and reads the full text only when the description matches the task. The detail stays in the drawer until it is needed.</p>
<p>Skills are also portable. A skill is plain Markdown, so you can import one written elsewhere instead of retyping it.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-3-rules-skills-lessons/import-rules.png" alt="Importing skills from .md files into a solution or your global profile" /></p>
<p>You point the importer at a file or a folder of <code>.md</code> files and choose whether they go into the current solution or your global profile. A procedure one person worked out can travel to the rest of the team, or to your next solution.</p>
<p>A simple test for which one to reach for:</p>
<ul>
<li>If it should hold no matter what you are doing, it is a <strong>Rule</strong>. (&quot;Always return errors in our standard shape.&quot;)</li>
<li>If it is a procedure you follow only for a certain kind of task, it is a <strong>Skill</strong>. (&quot;Here is our checklist for adding a feature end to end.&quot;)</li>
</ul>
<h2>Lessons: What The Agent Learns On Its Own</h2>
<p>Rules and Skills are written by a person. <strong>Lessons</strong> are written by the agent.</p>
<p>Think of a shift handoff log in the kitchen: after something goes wrong, someone writes down what happened and what to do differently next time. The next cook reads it before repeating the same work. In ABP Studio, you correct the mistake; the agent records the verified fix so future sessions do not trip over it again. That entry is a Lesson.</p>
<p>When the agent gets something wrong and is corrected, by you, by a failing build, or by the official ABP documentation, it can record the correction as a short, verified note. The harness carries that note into later turns and later sessions as high-priority context. That is the <code>ai-learned-lessons</code> entry you saw in the settings list, growing as the agent works in your solution.</p>
<p>The idea behind it is simple: when the agent gets something wrong, fix it once so it never gets it wrong the same way again. You already do a version of this when you edit a notes file by hand. Lessons do the recording for you, at the moment of the correction, when the reason is still fresh and you would otherwise forget to write it down.</p>
<p>Here is the shape of a real one. I ask the agent to add a <code>Category</code> screen. It builds the data model, the API, and the UI, but forgets to grant the new permission in the demo data. The screen compiles, the API responds, everything looks finished, and the only symptom is that real users get &quot;access denied.&quot; I correct it, the agent fixes the demo data, and it records a note: in this solution, a new permission is not done until it is granted in the demo data. Next time, it remembers.</p>
<p>A Lesson is not a general fact about ABP. The documentation already covers that, and the agent reads it. A Lesson is a solution-specific, verified correction: the kind of knowledge a team usually keeps in people's heads and loses when they move on. Here it is written down the moment it is learned, instead of weeks later, if ever.</p>
<h2>When To Use Which?</h2>
<p>Once the three are clear, they sort themselves out:</p>
<p>| Mechanism | Who writes it | When it loads | Answers |
| --- | --- | --- | --- |
| <strong>Rules</strong> | You | Always, every turn | &quot;What must always be true here?&quot; |
| <strong>Skills</strong> | You | When relevant to the task | &quot;How do we do this kind of task?&quot; |
| <strong>Lessons</strong> | The agent | In later sessions, as high-priority context | &quot;What did we get wrong before in this solution?&quot; |</p>
<p>They feed into each other. A Lesson that keeps coming back is a sign it should be promoted. If the agent records the same correction again and again, it is no longer a one-off note. It belongs in a Rule, if it is an always-true convention, or in a Skill, if it is a procedure. Knowledge tends to move from Lessons toward Rules and Skills: the agent finds the pattern by tripping over it, and you write it down properly once it has earned the spot.</p>
<p>Keep an eye on the budget, because all three share the same limited space, the context window. Rules cost the most, since they are always present, so an overstuffed rule list weighs down every turn and crowds out the rules that matter. Skills are cheaper, paid for only when used, which is a good reason to move anything situational out of Rules and into a Skill. Lessons add up too, so clear out the stale ones now and then. The point is not to write down everything you know. It is to write down the few things that change what the agent produces, and put each one where it loads at the right time.</p>
<h2>Why This Is Different From Generic Coding Agents?</h2>
<p>To be fair, the ideas themselves are not unique to ABP. Tools like Cursor, Claude Code, Codex, and Windsurf are strong general-purpose coding tools, and they already have always-on instruction files and on-demand skill files. If all you compare is whether a tool can hold rules and skills, there is no real difference, and I would not pretend otherwise.</p>
<p>The difference is in the two places where a generic tool has to guess.</p>
<p>The first is memory of its own mistakes. In most tools, remembering a past mistake means you stop and edit a memory file by hand. Lessons remove that step. The agent records the correction itself, the moment it happens, so the knowledge survives without you having to maintain it.</p>
<p>The second, and the one that matters most, is what all of this sits on. A generic instruction file can say &quot;use the data layer,&quot; but the tool still has to read your files and guess what the data layer is and where it lives. ABP Agent does not guess. It starts every session with an ABP-aware map of your solution, and when it is unsure how something should be done, it checks the official ABP documentation instead of the average of the public internet. So the rules and skills you write are not hints dropped into a tool that barely understands the project. They are backed by one that already knows the framework underneath them.</p>
<p>There is also a smaller convenience worth a line: a rule and a skill are the same note with one switch between them, scoped to the solution or your profile from the same place, instead of two separate file formats to keep track of.</p>
<h2>Conclusion</h2>
<p>It is best to see the three as one system, not three switches.</p>
<ul>
<li><strong>Rules</strong> hold the conventions you refuse to repeat.</li>
<li><strong>Skills</strong> hold the procedures you want to reuse.</li>
<li><strong>Lessons</strong> hold the corrections the agent learns as it works.</li>
</ul>
<p>Together they answer the blank-memory problem. They do not give the model new weights. They inject, at the start of each session, the smallest useful set of instructions so the agent can work as if it has been here before.</p>
<p>A generic agent with no harness tuning stays roughly as capable on its thousandth task in your codebase as on its first. With Rules, Skills, and Lessons, ABP Agent accumulates solution-specific knowledge over time, especially through Lessons and the promotions you make from them.</p>
<p>That is the real value: <strong>not just an AI that writes code, but one that learns how your solution is built and keeps that knowledge from one session to the next.</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21c779-cc6d-372e-fe9b-e508c2af3851" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21c779-cc6d-372e-fe9b-e508c2af3851" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/announcing-.net-aspire-integration-for-abp-microservice-template-1308lujt</guid>
      <link>https://abp.io/community/posts/announcing-.net-aspire-integration-for-abp-microservice-template-1308lujt</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>startup-templates</category>
      <category>microservices</category>
      <category>abp</category>
      <category>abp-studio</category>
      <category>dotnet-aspire</category>
      <title>Announcing .NET Aspire Integration for ABP Microservice Template</title>
      <description>We are excited to announce the integration of .NET Aspire into the ABP microservice solution, available starting with ABP Studio v2.0.0. This integration brings a unified development experience for building, running, debugging, and deploying distributed applications. With Aspire, you can now orchestrate your entire microservice ecosystem with a single command, eliminating complex configurations and making local development effortless.</description>
      <pubDate>Tue, 13 Jan 2026 13:16:51 Z</pubDate>
      <a10:updated>2026-09-26T00:03:57Z</a10:updated>
      <content:encoded><![CDATA[<h1>Announcing Aspire Integration for ABP Microservice Template</h1>
<p>We are excited to announce the integration of <strong>Aspire</strong> into the ABP microservice solution, available starting with <strong>ABP Studio v2.0.0</strong>. This integration brings a unified development experience for building, running, debugging, and deploying distributed applications. With Aspire, you can now orchestrate your entire microservice ecosystem with a single command, eliminating complex configurations and making local development effortless.</p>
<h2>What is Aspire?</h2>
<p><a href="https://aspire.dev/get-started/what-is-aspire/">Aspire</a> is a cloud-ready stack designed to streamline the development of distributed applications. It provides:</p>
<ul>
<li><strong>Orchestration</strong>: A code-first approach to defining and running distributed applications, managing dependencies, and launch order.</li>
<li><strong>Integrations</strong>: Pre-built components for common services (databases, caches, message brokers) with automatic configuration.</li>
<li><strong>Tooling</strong>: A developer dashboard for real-time monitoring of logs, traces, metrics, and resource health.</li>
<li><strong>Service Discovery</strong>: Automatic service-to-service communication without hardcoded endpoints.</li>
<li><strong>Observability</strong>: Built-in OpenTelemetry support for distributed tracing, metrics, and structured logging.</li>
</ul>
<h2>How Does It Work with ABP?</h2>
<p>When you enable Aspire in an ABP microservice solution, you get a fully integrated development experience where:</p>
<ul>
<li>All microservices, gateways, and applications are orchestrated through a single entry point (AppHost).</li>
<li>Infrastructure containers (databases, Redis, RabbitMQ, Elasticsearch, etc.) are managed as code.</li>
<li>OpenTelemetry, health checks, and service discovery are automatically configured for all projects via the shared ServiceDefaults project.</li>
</ul>
<h2>Enabling Aspire in Your Solution</h2>
<p>When creating a new microservice solution via ABP Studio:</p>
<ol>
<li>In the solution creation wizard, look for the <strong>&quot;Aspire Integration&quot;</strong> step.</li>
<li>Toggle the option to <strong>enable Aspire</strong>.</li>
<li>Complete the wizard—Aspire projects will be generated along with your solution.</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-configuration.png" alt="Enable Aspire in ABP Studio" /></p>
<h2>Solution Structure Changes</h2>
<p>When Aspire is enabled, two additional projects are added to your solution:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-solution-structure.png" alt="Aspire Solution Structure" /></p>
<h3>AppHost (Orchestrator)</h3>
<p><a href="https://aspire.dev/get-started/app-host/"><code>AppHost</code></a> is the Aspire orchestrator project that declares all resources (services, databases, containers, applications) and their dependencies in C# code. It provides:</p>
<ul>
<li><strong>Centralized orchestration</strong>: Start your entire microservice ecosystem with a single command.</li>
<li><strong>Code-first infrastructure</strong>: Databases, Redis, RabbitMQ, Elasticsearch, and observability tools are defined programmatically.</li>
<li><strong>Dependency management</strong>: Services start in the correct order using <code>WaitFor()</code> declarations.</li>
<li><strong>Automatic configuration</strong>: Connection strings, endpoints, and environment variables are injected automatically.</li>
</ul>
<h3>ServiceDefaults</h3>
<p><a href="https://aspire.dev/fundamentals/service-defaults/"><code>ServiceDefaults</code></a> is a shared library that provides common cloud-native configuration for all projects in the solution. Every service uses the same observability, health check, and resilience patterns.</p>
<p>| Feature | Description |
|---------|-------------|
| OpenTelemetry | Tracing, metrics, and structured logging with automatic instrumentation |
| Health Checks | <code>/health</code> and <code>/alive</code> endpoints for Kubernetes-style probes |
| Service Discovery | Automatic resolution of service endpoints |
| HTTP Resilience | Retry policies, timeouts, and circuit breakers for HTTP clients |</p>
<h2>Running the Solution with Aspire</h2>
<p>Running your microservice solution has never been easier:</p>
<ol>
<li>Open <strong>Solution Runner</strong> in ABP Studio.</li>
<li>Select the <strong>Aspire</strong> profile.</li>
<li>Run <code>AppHost</code>.</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/solution-runner-aspire-profile.png" alt="Solution Runner with Aspire" /></p>
<p>AppHost automatically:</p>
<ul>
<li>Starts all infrastructure containers (database, Redis, RabbitMQ, Elasticsearch, etc.).</li>
<li>Launches all microservices, gateways, and applications in dependency order.</li>
<li>Injects connection strings and environment variables.</li>
<li>Opens the Aspire Dashboard for monitoring.</li>
</ul>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-apphost-topology.png" alt="Aspire AppHost Resource Topology" /></p>
<h2>Aspire Dashboard</h2>
<p>The Aspire Dashboard provides real-time tracking of your application's state. It enables you to monitor logs, traces, metrics, and environment configurations in an intuitive UI.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-resources.png" alt="Aspire Dashboard Resources" /></p>
<h3>Key Dashboard Features</h3>
<h4>Console Logs</h4>
<p>Display console logs from all resources in real-time. Filter by resource and log level to quickly find relevant information during development and debugging.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-console.png" alt="Aspire Dashboard Console" /></p>
<h4>Structured Logs</h4>
<p>View structured logs from all resources with advanced filtering capabilities. Search and filter logs by resource, log level, timestamp, and custom properties.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-structured-logs.png" alt="Aspire Dashboard Structured Logs" /></p>
<h4>Distributed Traces</h4>
<p>Explore distributed traces across your microservices to understand request flows and identify performance bottlenecks.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-traces.png" alt="Aspire Dashboard Traces" /></p>
<h4>Metrics</h4>
<p>Monitor real-time metrics including HTTP requests, response times, garbage collection, memory usage, and custom metrics.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-dashboard-metrics.png" alt="Aspire Dashboard Metrics" /></p>
<h2>Pre-Configured Observability Tools</h2>
<p>AppHost comes with pre-configured observability and management tools:</p>
<h3>Grafana</h3>
<p>Visualization and analytics platform for monitoring metrics with interactive dashboards.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-grafana-dashboard.png" alt="Grafana Dashboard" /></p>
<h3>Jaeger</h3>
<p>Distributed tracing system to monitor and troubleshoot problems across microservices.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-jaeger-traces.png" alt="Jaeger Traces" /></p>
<h3>Kibana</h3>
<p>Visualization tool for Elasticsearch data with search and data visualization capabilities for logs.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-kibana-dashboard.png" alt="Kibana Dashboard" /></p>
<h3>Prometheus</h3>
<p>Monitoring and alerting toolkit that collects and stores metrics as time series data.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-prometheus-dashboard.png" alt="Prometheus Dashboard" /></p>
<h3>RabbitMQ Management</h3>
<p>Web-based interface for managing and monitoring the RabbitMQ message broker.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-rabbitmq-management.png" alt="RabbitMQ Management" /></p>
<h3>Redis Insight</h3>
<p>Visual tool for Redis that allows you to browse data, run commands, and monitor performance.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-redis-insight.png" alt="Redis Insight" /></p>
<h3>Database Admin Tools</h3>
<p>The database management admin tool varies by database type:</p>
<p>| Database | Tool |
|----------|------|
| SQL Server | DBeaver CloudBeaver |
| MySQL | phpMyAdmin |
| PostgreSQL | pgAdmin |
| MongoDB | Mongo Express |</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Blog-Posts/2025-12-24-Announcing-Aspire-For-Microservice-Template/aspire-database-postgre-pgadmin.png" alt="pgAdmin Dashboard" /></p>
<h2>Get Started Today</h2>
<p>Ready to experience the power of Aspire with ABP? Create a new microservice solution in ABP Studio and enable the Aspire integration option. For detailed documentation, visit our <a href="https://abp.io/docs/latest/solution-templates/microservice/aspire-integration">Aspire Integration documentation</a>.</p>
<p>To learn more about Aspire, visit: <a href="https://aspire.dev/get-started/what-is-aspire/">https://aspire.dev</a></p>
<p>We are excited to bring this integration to you and can't wait to hear your feedback. If you have any questions or suggestions, please drop a comment below.</p>
<p>Happy coding!</p>
<p><strong>The Volosoft Team</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1ec9ad-5a37-1341-8e10-c96760ae1cf6" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1ec9ad-5a37-1341-8e10-c96760ae1cf6" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/uncovering-abps-hidden-magic-supercharging-asp.net-core-development-5o01gnmx</guid>
      <link>https://abp.io/community/posts/uncovering-abps-hidden-magic-supercharging-asp.net-core-development-5o01gnmx</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>abp</category>
      <category>abp-features</category>
      <category>application-development</category>
      <category>asp.net-core</category>
      <title>Uncovering ABP’s Hidden Magic: Supercharging ASP.NET Core Development</title>
      <description>ABP’s multitude of behind-the-scenes features are not about making the framework look impressive on paper – they’re about making you, the developer, more productive and happier in your job. By handling the boring, complex, or repetitive stuff, ABP lets you focus on building great software.</description>
      <pubDate>Tue, 21 Oct 2025 11:50:00 Z</pubDate>
      <a10:updated>2026-09-26T00:44:28Z</a10:updated>
      <content:encoded><![CDATA[<h1>Uncovering ABP’s Hidden Magic: Supercharging ASP.NET Core Development</h1>
<p>Experienced back-end developers often approach new frameworks with healthy skepticism. But many who try the ABP Framework quickly notice something different: things “just work” with minimal boilerplate. There’s a good reason ABP can feel magical – it silently handles a host of tedious tasks behind the scenes. In this article, we’ll explore how ABP’s out-of-the-box features and modular architecture dramatically boost productivity. We’ll compare with plain ASP.NET Core where relevant, so you can appreciate what ABP is doing for you under the hood.</p>
<h2>Beyond the Basics: Why ABP Feels Magical</h2>
<p>ABP isn’t a typical library; it’s a full application framework that goes beyond the basics. From the moment you start an ABP project, a lot is happening automatically. Have you ever built an ASP.NET Core app and spent time wiring up cross-cutting concerns like error handling, logging, security tokens, or multi-tenancy? With ABP, much of that comes pre-configured. You might find that you write just your business logic, and ABP has already enabled security, transactions, and even APIs for you by convention. This can be disorienting at first (“Where’s the code that does X?”) until you realize ABP’s design is doing it for you, in line with best practices.</p>
<p>For example, ABP completely automates CSRF (anti-forgery) protection and it works out-of-the-box without any configuration. In a plain ASP.NET Core project, you’d have to add anti-forgery tokens to your views or enable a global filter and manually include the token in AJAX calls. ABP’s startup template already includes a global antiforgery filter and even sets up the client-side code to send the token on each request, without you writing a line. This kind of “invisible” setup is repeated across many areas. ABP’s philosophy is to take care of the plumbing – like unit of work, data filters, audit logging, etc. – so you can focus on the real code. It feels magical because things that would normally require explicit code or packages in ASP.NET Core are just handled. As we peel back the layers in the next sections, you’ll see how ABP pulls off these tricks.</p>
<h2>Zero to Hero: Rapid Application Development with ABP</h2>
<p>One of the most striking benefits of ABP is how quickly you can go from zero to a fully functional application – it’s a true rapid application development platform. With ASP.NET Core alone, setting up a new project with identity management, localization, an API layer, and a clean architecture can be a day’s work or more. In contrast, ABP’s startup templates give you a solution with all those pieces pre-wired. You can create a new ABP project (using the ABP CLI or ABP Studio) and run it, and you already have: user login and registration, role-based permission management, an admin UI, a REST API layer with Swagger, and a clean domain-driven code structure. It’s essentially a jump-start that takes you from zero to hero in record time.</p>
<p>Rapid development is further enabled by ABP’s coding model. Define an entity and an application service, and ABP can generate the REST API endpoints for you automatically (via Conventional Controllers). You don’t need to write repetitive controllers that just call the service; ABP’s conventions map your service methods to HTTP verbs and routes by naming convention. For instance, a method name <code>GetListAsync()</code> in an <code>AppService</code> becomes an HTTP <code>GET</code> to <code>/api/app/your-entity</code> without extra attributes. The result: you implement application logic once in the application layer, and ABP instantly exposes it as an API (and even provides client proxies for UI).</p>
<p>The tooling in the ABP ecosystem multiplies this productivity. The ABP Suite tool, for example, allows you to visually design entities and then generate a full-stack CRUD page for your entities in seconds, complete with UI forms, validation, DTOs, application services, and even unit tests. The generated code follows ABP’s best practices (layered architecture, proper authorization checks, etc.), so you’re not creating a maintenance headache. You get a working feature out-of-the-box and can then tweak it to your needs. All these accelerators mean you can deliver features at a higher velocity than ever, turning a blank project into a real application with minimal grunt work.</p>
<h2>Modular Architecture: Building Like Digital Lego</h2>
<p>Perhaps the greatest strength of ABP is its modular architecture. Think of modules as building blocks – “digital Lego” pieces – that you can snap together to compose your application. ABP itself is built on modules (for example, Identity, Audit Logging, Language Management, etc.), and you can develop your own modules as well. This design encourages separation of concerns and reusability. Need a certain functionality? Chances are, ABP has a module for it – just plug it in, and it works seamlessly with the others.</p>
<p>With plain ASP.NET Core, setting up a modular system requires a lot of upfront design. ABP, however, “is born to be a modular application development structure”, where every feature is compatible with modular development by default. The framework ensures that each module can encapsulate its own domain, application services, database migrations, UI pages, etc., without tight coupling. For example, the ABP Identity module provides all the user and role management functionality (built atop ASP.NET Core Identity), the SaaS module provides multi-tenant management, the Audit Logging module records user activities, and so on. You can include these modules in your project, gaining enterprise-grade functionality in literally one line of configuration. As the official documentation puts it, ABP provides “a lot of re-usable application modules like payment, chat, file management, audit log reporting… All of these modules are easily installed into your solution and directly work.” This is a huge time saver – you’re not reinventing the wheel for common requirements.</p>
<p>The Lego-like nature also means you can remove or swap pieces without breaking the whole. If a built-in module doesn’t meet your needs, you can extend it or replace it (we’ll talk about customization later). Modules can even be maintained as separate packages, enabling teams to develop features in isolation and share modules across projects. Ultimately, ABP’s modularity gives your architecture a level of flexibility and organization that plain ASP.NET Core doesn’t provide out-of-the-box. It’s a solid foundation for either monolithic applications or microservice systems, as you can start with a modular monolith and later split modules into services if needed. In short, ABP provides the architectural “bricks” – you design the house.</p>
<h2>Out-of-the-Box Features that Save Weeks of Work</h2>
<p>Beyond the big building blocks, ABP comes with a plethora of built-in features that operate behind the scenes to save you time. These are things that, in a non-ABP project, you would likely spend days or weeks implementing and fine-tuning – but ABP gives them to you on Day 1. Here are some of the key hidden gems ABP provides out-of-the-box:</p>
<ul>
<li>CSRF Protection: As mentioned earlier, ABP automatically enables anti-forgery tokens for you. You get robust CSRF/XSRF protection by default – the server issues a token cookie and expects a header on modify requests, all handled by ABP’s infrastructure without manual setup. This means your app is defended against cross-site request forgery with essentially zero effort on your part.</li>
<li>Automated Data Filtering: ABP uses data filters to transparently apply common query conditions. For example, if an entity implements <code>ISoftDelete</code>, it will not be retrieved in queries unless you explicitly ask for deleted data. ABP automatically sets <code>IsDeleted=true</code> instead of truly deleting and filters it out on queries, so you don’t accidentally show or modify soft-deleted records. Similarly, if an entity implements <code>IMultiTenant</code>, ABP will “silently in the background” filter all queries to the current tenant and fill the <code>TenantId</code> on new records – no need to manually add tenant clauses to every repository query. These filters (and others) are on by default and can be toggled when needed, giving you multi-tenancy and soft delete behavior out-of-the-box.</li>
<li>Concurrency Control: In enterprise apps, it’s important to handle concurrent edits to avoid clobbering data. ABP makes this easy with an optimistic concurrency system. If you implement <code>IHasConcurrencyStamp</code> on an entity, ABP will automatically set a GUID stamp on insert and check that stamp on updates to detect conflicts, throwing an exception if the record was changed by someone else. In ASP.NET Core EF you’d set up a RowVersion or concurrency token manually – ABP’s built-in approach is a ready-to-use solution to ensure data consistency.</li>
<li>Data Seeding: Most applications need initial seed data (like an admin user, initial roles, etc.). ABP provides a modular data seeding system that runs on application startup or during migration. You can implement an <code>IDataSeedContributor</code> and ABP will automatically discover and execute it as part of the seeding process. Different modules add their own seed contributors (for example, the Identity module seeds the admin user/role). This system is database-independent and even works in production deployments (the templates include a DbMigrator tool to apply migrations and seed data). It’s more flexible than EF Core’s native seeding and saves you writing custom seeding scripts.</li>
<li>Audit Logging: ABP has an integrated auditing mechanism that logs details of each web request. By default, an audit log is created for each API call or MVC page hit, recording who did what and when. It captures the URL and HTTP method, execution duration, the user making the call, the parameters passed to application services, any exceptions thrown, and even entity changes saved to the database during the request. All of this is saved automatically (for example, into the AbpAuditLogs table if using EF Core). The startup templates enable auditing by default, so you have an audit trail with no extra coding. In a vanilla ASP.NET Core app, you’d have to implement your own logging to achieve this level of detail.</li>
<li>Unit of Work &amp; Transaction Management: ABP implements the Unit of Work pattern globally. When you call a repository or an application service method, ABP will automatically start a UOW (database transaction) for you if one isn’t already running. It will commit on success or roll back on error. By convention, all app service methods, controller actions, and repository methods are wrapped in a UOW – so you don’t explicitly call SaveChanges() or begin transactions in most cases. For example, if you create or update multiple entities in an app service method, they either all succeed or all fail as a unit. This behavior is there “for free”, whereas in raw ASP.NET Core you’d be writing try/catch and transaction code around such operations. (ABP even avoids opening transactions on read-only GET requests by default for performance.)</li>
<li>Global Exception Handling: No need to write a global exception filter – ABP provides one. If an unhandled exception occurs in an API endpoint, ABP’s exception handling system catches it and returns a standardized error response in JSON. It also maps known exception types to appropriate HTTP status codes and can localize error messages. This means your client applications always get a clean, consistent error format (with an error code, message, validation details, etc.) instead of ugly stack traces or HTML error pages. Internally, ABP logs the error details and hides the sensitive info from the client by default. Essentially, you get production-ready error handling without writing it yourself.</li>
<li>Localization &amp; Multi-Language Support: ABP’s localization system is built on the .NET localization extension but adds convenient enhancements. It automatically determines the user’s language/culture for each request (by checking the browser or tenant settings) and you can define localization resources in JSON files easily. ABP supports database-backed translations via the Language Management module as well. From day one, your app is ready to be translated – even exception messages and validation errors are localization-friendly. The default project template sets up a default resource and uses it for all framework-provided texts, meaning things like error messages or menu items are already localized (and you can add new languages through the UI if you include the module). In short, ABP bakes in multi-lingual capabilities so you don’t have to internationalize your app from scratch.</li>
<li>Background Jobs: Need to run tasks in the background (e.g. send emails, generate reports) without blocking the user? ABP has a built-in background job infrastructure. You can simply implement a job class and enqueue it via <code>IBackgroundJobManager</code>. By default, jobs are persisted and executed, and ABP has providers to integrate with popular systems like Hangfire, RabbitMQ and Quartz if you need scalability. For example, sending an email after a user registers can be offloaded to a background job with one method call. ABP will handle retries on failure and storing the job info. This saves you the effort of configuring a separate job runner or scheduler – it’s part of the framework.</li>
<li>Security &amp; Defaults: ABP comes with sensible security defaults. It’s integrated with ASP.NET Core Identity, so password policies, lockout on multiple failed logins, and other best practices are in place by default. The framework also adds standard security headers to HTTP responses (against XSS, clickjacking, etc.) through its startup configuration. Additionally, ABP’s permission system is pre-configured: every module brings its own permission definitions, and you can easily check permissions with an attribute or method call. There’s even a built-in Permission Management UI (if you include the module) where you can grant or revoke permissions per role or user at runtime. All these defaults mean a lot of the “boring” but critical security work is done for you.</li>
<li>Paging &amp; Query Limiting: ABP encourages efficient data access patterns. For list endpoints, the framework DTOs usually include paging parameters (MaxResultCount, SkipCount), and if you don't specify them, ABP will assume default values (often 10). ABP also enforces an upper limit on how many records can be requested in a single call, preventing potential performance issues from overly large queries. This protects your application from accidentally pulling thousands of records in one go. Of course, you can configure or override these limits, but the safe defaults are there to protect your application.</li>
</ul>
<p>That’s a long list – and it’s not even exhaustive – but the pattern is clear. ABP spares you from writing a lot of infrastructure and “glue” code. And if you do need multi-tenancy (or any of these advanced features), the time savings grow even more. These out-of-the-box capabilities let you focus on your business logic, since the baseline features are already in place. Next, let’s zoom in on a couple of these areas (like multi-tenancy and security) that typically cause headaches in pure ASP.NET Core but are a breeze with ABP.</p>
<h2>Seamless Multi-Tenancy: Scaling Without the Headaches</h2>
<p>Multi-tenant architecture – supporting multiple isolated customers (tenants) in one application – is notoriously tricky to implement from scratch. You have to partition data per tenant, ensure no cross-tenant data leaks, manage connection strings if using separate databases, and adapt authentication/authorization to be tenant-aware. ABP Framework makes multi-tenancy almost trivial in comparison.</p>
<p>Out of the box, ABP supports both approaches to multi-tenancy: single database with tenant segregation and separate databases per tenant, or even a hybrid of the two. If you go the single database route, as many SaaS apps do for simplicity, ABP will ensure every entity that implements the tenant interface (<code>IMultiTenant</code>) gets a <code>TenantId</code> value and is automatically filtered. As we touched on earlier, you don’t have to manually add <code>.Where(t =&gt; t.TenantId == currentTenant.Id)</code> on every query – ABP’s data filter does that behind the scenes based on the logged-in user’s tenant. If a user from Tenant A tries to access Tenant B’s data by ID, they simply won’t find it, because the filter is in effect on all repositories. Similarly, when saving data, ABP sets the <code>TenantId</code> for you. This isolation is enforced at the ORM level by ABP’s infrastructure.</p>
<p>For multiple databases, ABP’s SaaS (Software-as-a-Service) module handles tenant management. At runtime, the framework can switch the database connection string based on the tenant context. In the ABP startup template, there’s a “tenant management” UI that lets an admin add new tenants and specify their connection strings. If a connection string is provided, ABP will use that database for that tenant’s data. If not, it falls back to the default shared database. Remarkably, from a developer’s perspective, the code you write is the same in both cases – ABP abstracts the difference. In practice, you just write repository queries as usual; ABP will route those to the appropriate place and filter as needed.</p>
<p>Another pain point that ABP solves is making other subsystems tenant-aware. For example, ASP.NET Core Identity (for user accounts) isn’t multi-tenant by default, and neither is Keycloak, IdentityServer or OpenIddict (for authentication). ABP takes care of configuring these to work in a tenant context. When a user logs in, they do so with a tenant domain or tenant selection, and the identity system knows about the tenant. Permissions in ABP are also tenant-scoped by default – a tenant admin can only manage roles/permissions within their tenant, for instance. ABP’s modules are built to respect tenant boundaries out-of-the-box.</p>
<p>What does all this mean for you? It means you can offer a multi-tenant SaaS solution without writing the bulk of the isolation logic. Instead of spending weeks on multi-tenancy infrastructure, you essentially flip a switch in ABP (enable multi-tenancy, use the SaaS module) and focus on higher-level concerns.</p>
<h2>Security That Works Without the Pain</h2>
<p>Security is one area you do not want to get wrong. With plain ASP.NET Core, you have great tools (Identity, etc.) at your disposal, but a lot of configuration and integration work to tie them together in a full application. ABP takes the sting out of implementing security by providing a comprehensive, pre-integrated security model.</p>
<p>To start, ABP’s application templates include the Identity Module, which is a ready-made integration of ASP.NET Core Identity (the membership system) with ABP’s framework. You get user and role entities extended to fit in ABP’s domain model, and a UI for user and role management. All the heavy lifting of setting up identity tables, password hashing, email confirmation, two-factor auth, etc. is done. The moment you run an ABP application, you can log in with the seeded admin account and manage users and roles through a built-in administration page. This would take significant effort to wire up yourself in a new ASP.NET Core app; ABP gives it to you out-of-the-box.</p>
<p>Permission management is another boon. In an ABP solution, you don’t have to hard-code what each role can do – instead, ABP provides a declarative way to define permissions and a UI to assign those permissions to roles or users. The Permission Management module’s UI allows dynamic granting/revoking of permissions. Under the hood, ABP’s authorization system will automatically check those permissions when you annotate your application services or controllers with [Authorize] and a policy name (the policy maps to a permission). For example, you might declare a permission Inventory.DeleteProducts. In your ProductAppService’s DeleteAsync method, you add [Authorize(&quot;Inventory.DeleteProducts&quot;)]. ABP will ensure the current user has that permission (through their roles or direct assignment) before allowing the method to execute. If not, it throws a standardized authorization exception. This is standard ASP.NET Core policy-based auth, but ABP streamlines defining and managing the policies by its permission system. The result: secure by default – it’s straightforward to enforce role-based access control throughout your application, and even non-developers (with access to the admin UI) can adjust permissions as requirements evolve.</p>
<p>We already discussed CSRF protection, but it’s worth reiterating in the security context: ABP saves you from common web vulnerabilities by enabling defenses by default. Anti-forgery tokens are automatic, and output encoding (to prevent XSS) is naturally handled by using Razor Pages or Angular with proper binding (framework features that ABP leverages). ABP also sets up ASP.NET Core’s Data Protection API for things like cookie encryption and CSRF token generation behind the scenes in its startup, so you get a proper cryptographic key management for free.</p>
<p>Another underappreciated aspect is exception shielding. In development, you want to see detailed errors, but in production you should not reveal internal details (stack traces, etc.) to the client. ABP’s exception filter will output a generic error message to the client while logging the detailed exception on the server. This prevents information leakage that attackers could exploit, without you having to configure custom middleware or filters.</p>
<p>On the topic of authentication: ABP supports modern authentication scenarios too. If you want to build a microservice or single-page app (SPA) architecture, ABP provides modules for OpenID Connect and OAuth2 protocol implementations. The ABP Commercial version even provides an OpenIddict setup out-of-the-box for issuing JWTs to SPAs or mobile apps. This means you can stand up a secure token service and resource servers with minimal configuration. With ABP, much of the configuration (clients, scopes, grants) is abstracted by the framework.</p>
<p>In short, ABP’s approach to security is holistic and follows the mantra of secure by default. New ABP developers are often pleasantly surprised that they didn’t have to spend days on user auth or protecting API endpoints – it’s largely handled. Of course, you still design your authorization logic (defining who can do what), but ABP provides the scaffolding to enforce it consistently. The painful parts of security – getting the plumbing right – are taken care of, so you can focus on the policies and rules that matter for your domain. This dramatically lowers the risk of security holes compared to rolling it all yourself.</p>
<h2>Customization Without Chaos</h2>
<p>With all this magic happening automatically, you might wonder: “What if I need to do it differently? Can I customize or override ABP’s behavior?” The answer is a resounding yes. ABP is designed with extension points and configurability in mind, so you can change the defaults without hacking the framework. This is important for keeping your project maintainable – you get ABP’s benefits, but you’re not boxed in when requirements demand a change.</p>
<p>One way ABP enables customization is through its powerful dependency injection system and the modular structure. Because each feature is delivered via services (interfaces and classes) in DI, you can replace almost any ABP service with your own implementation if needed. For example, if you want to change how the IdentityUserAppService (the service behind user management) works, you can create your own class inheriting or implementing the same interface, and register it with <code>Dependency(ReplaceServices = true)</code>. ABP will start using your class in place of the original. This is an elegant way to override behavior without modifying ABP’s source – keeping you on the upgrade path for new versions. ABP’s team intentionally makes most methods virtual to support overriding in derived classes. This means you can subclass an ABP application service or domain service and override just the specific method you need to change, rather than writing a whole service from scratch.</p>
<p>Beyond swapping out services, ABP offers configuration options for its features. Virtually every subsystem has an options class you can configure in your module startup. Not liking the 10-item default page size? You can change the default MaxResultCount. Want to disable a filter globally? You can toggle, say, soft-delete filtering off by default using <code>AbpDataFilterOptions</code>. Need to turn off auditing for certain operations? Configure <code>AbpAuditingOptions</code> to ignore them. These options give you a lot of control to tweak ABP’s behavior. And because they’re central configurations, you aren’t scattering magic numbers or settings throughout your code – it’s a structured approach to customization.</p>
<p>Another area is UI and theming. ABP’s UI (if you use the integrated UI) is also modular and replaceable. You can override Razor components or pages from a module by simply re-declaring them in your web project. For instance, if you want to modify the login page from the Account module, you can add a Razor page with the same path in your web layer – ABP will use yours instead of the default. The documentation has guidance on how to override views, JavaScript, CSS, etc., in a safe manner for Angular, Blazor, and MVC. The LeptonX theme that ABP uses can be customized via SCSS variables or entirely new theme derivations. The key point is, you’re never stuck with the “out-of-the-box” look or logic if it doesn’t fit your needs. ABP gives you the foundation, and you’re free to build on top of it or change it.</p>
<p>The best part? These customizations stay clean and organized. ABP's extension patterns prevent your project from becoming a mess of patches. When ABP releases updates, your overrides remain intact – no more copy-pasting framework code or dealing with merge conflicts. You get ABP's smart defaults plus the freedom to customize when needed.</p>
<h2>Ecosystem Power: ABP’s Tools, Templates, and Integrations</h2>
<p>ABP is more than just a runtime framework; it’s surrounded by an ecosystem of tools and libraries that amplify productivity. We’ve touched on a few (like the ABP Suite code generator), but let’s look at the broader ecosystem that comes with ABP.</p>
<ul>
<li>Project Templates: ABP provides multiple startup templates (via the ABP CLI or Studio) for different architectures – from a simple monolithic web app to a layered modular monolith, or even a microservice-oriented solution with multiple projects pre-configured. These templates are not empty skeletons; they include working examples of authentication, a UI theme, navigation, and so on for your own modules. The microservice template, for instance, sets up separate identity, administration, and SaaS services with communication patterns already wired. Using these templates can save you a huge amount of setup time and ensure you follow best practices from the get-go.</li>
<li>ABP CLI: The command-line tool abp is a developer’s handy companion. With it, you can generate new solutions or modules, add package references, update your ABP version, and even client proxy generations with simple commands.</li>
<li>ABP Studio: It is a cross-platform desktop environment designed to make working with ABP solutions smoother and more insightful. It provides a unified UI to create, run, monitor, and manage your ABP projects – whether you're building a monolith or a microservice system. With features like a real-time Application Monitor, Solution Runner, and Kubernetes integration, it brings operational visibility and ease-of-use to development workflows. Studio also includes tools for managing modules, packages, and even launching integrated tools like ABP Suite – all from a single place. Think of it as a control center for your ABP solutions.</li>
<li>ABP Suite: It is a powerful visual tool (included in PRO licenses) that helps you generate full-stack CRUD pages in minutes. Define your entities, their relationships, and hit generate – ABP Suite scaffolds everything from the database model to the HTTP APIs, application services, and UI components. It supports one-to-many and many-to-many relationships, master-detail patterns, and even lets you generate from existing database tables. Developers can customize the generated code using predefined hook points that persist across regenerations.</li>
<li>3rd-Party Integrations: Modern applications often need to integrate with messaging systems, distributed caching, search engines, etc. ABP recognizes this and provides integration packages for many common technologies. Want to use RabbitMQ for event bus or background jobs? ABP has you covered. The same goes for others: ABP has modules or packages for Redis caching, Kafka distributed event bus, SignalR real-time hubs, Twilio SMS, Stripe payments, and more. Each integration is done in a way that it feels like a natural extension of the ABP environment (for example, using the same configuration system and dependency injection). This saves you from writing repetitive integration code or dealing with each library’s nuances in every project.</li>
<li>UI Themes and Multi-UI Support: ABP comes with a modern default theme (LeptonX) for web applications, and it supports Angular, MVC/Razor Pages and Blazor out-of-the-box. If you prefer Angular for frontend, ABP offers an Angular UI package that works with the same backend. There’s also support for mobile via React Native or MAUI templates. The ability to switch UI front-ends (or even support multiple simultaneously, e.g. an Angular SPA and a Blazor server app using the same API) is facilitated by ABP’s API and authentication infrastructure. This dramatically reduces the friction when setting up a new client application – you don’t have to hand-roll API clients or auth flows.</li>
<li>Community and Samples: While not a tool per se, the ABP community is part of the ecosystem and adds a lot of value. There are official sample projects (like eShopOnAbp, a full microservice reference application) and many community-contributed modules on GitHub. The consistency of ABP’s structure means community modules or examples are easier to understand and plug in. Being in a community where “everyone follows similar coding styles and principles” means code and knowledge are highly transferable. Developers share open source ABP modules (for example, there are community modules for things like blob storage management, setting UI, React frontend support, etc., beyond the official ones). This network effect is an often overlooked part of the ecosystem: as ABP’s adoption grows, so do the resources you can draw on, from Q&amp;A to reusable code.</li>
</ul>
<p>In summary, ABP’s ecosystem provides a full-platform experience. It’s not just the core framework, but also the tooling to work with that framework efficiently and the integrations to connect it with the wider tech world. By using ABP, you’re not piecing together disparate tools – you have a coherent set of solutions designed to work in concert. This is the kind of ecosystem that traditionally only large enterprises or opinionated tech stacks provided, but ABP makes it accessible in the .NET open-source space. It supercharges development in a way that goes beyond just writing code faster; it’s about having a robust infrastructure around your code, so you can deliver more value with less guesswork.</p>
<h2>Developer Happiness: The Hidden Productivity Boost</h2>
<p>All these features and time-savers aren’t just about checking off technical boxes – they have a profound effect on developer happiness and productivity. When a framework handles the heavy lifting and enforces good practices, developers can spend more time on interesting problems (and less on boilerplate or bug-hunting). ABP’s “hidden” features – the things that work without you even noticing – contribute to a less stressful development experience.</p>
<p>Think about the common sources of frustration in back-end development: security holes that come back to bite you, race conditions or transaction bugs, deployment issues because some configuration was missed, writing the same logging or exception handling code in every project… ABP’s approach preempts many of these. There’s confidence in knowing that the framework has built-in solutions for common pitfalls. For instance, you’re less likely to have a data inconsistency bug because ABP’s unit of work ensured all your DB operations were atomic. This confidence means developers can focus on delivering features rather than constantly firefighting or re-architecting core pieces.</p>
<p>Another aspect of developer happiness is consistency. ABP provides a uniform structure – every module has the same layering (Domain, Application, etc.), every web endpoint returns a standard response, and so on. Once you learn the patterns, you can navigate and contribute to any part of an ABP application with ease. New team members or even outside contributors ramp up faster because the project structure is familiar (it’s the ABP structure). This reduces the bus factor and onboarding time on teams – a source of relief for developers and managers alike.</p>
<p>Moreover, by taking away a lot of the “yak shaving” (the endless setup tasks), ABP lets you as a developer spend your energy on creative problem-solving and delivering value. It’s simply more fun to develop when you can swiftly implement a feature without being bogged down in plumbing code. The positive feedback loop of having working features quickly (thanks to things like ABP Suite, or just the rapid scaffolding of ABP) can be very motivating. It feels like you have an expert co-pilot who has already wired the security system, laid out the architecture, and packed the toolkit with everything you need – so you can drive the project forward confidently.</p>
<p>Finally, the community support adds to this happiness. There’s a thriving Discord server and forum where ABP developers help each other. Since ABP standardizes a lot, advice from one person’s experience often applies directly to your scenario. That sense of not being alone when you hit a snag – because others likely encountered and solved it – reduces anxiety and speeds up problem resolution. It’s the kind of developer experience where things “just work,” and when they occasionally don’t, you have a clear path to figure it out (good docs, support, community). In the daily life of a software developer, this can make a huge difference.</p>
<p>In conclusion, ABP’s multitude of behind-the-scenes features are not about making the framework look impressive on paper – they’re about making you, the developer, more productive and happier in your job. By handling the boring, complex, or repetitive stuff, ABP lets you focus on building great software. It’s like having a teammate who has already done half the work before you even start coding. When you combine that with ABP’s extensibility and strong foundation, you get a framework that not only accelerates development but also encourages you to do things the right way. For experienced engineers and newcomers alike, that can indeed feel a bit like magic. But now that we’ve uncovered the “magic tricks” ABP is doing under the hood, you can fully appreciate how it all comes together – and decide if this framework’s approach aligns with your goals of building applications faster, smarter, and with fewer headaches. Chances are, once you experience the productivity boost of ABP, you won’t want to go back. Happy coding!</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1d18c7-a743-d428-0f53-62a7f4b5a310" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1d18c7-a743-d428-0f53-62a7f4b5a310" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-dynamically-set-the-connection-string-in-ef-core-30k87fpj</guid>
      <link>https://abp.io/community/posts/how-to-dynamically-set-the-connection-string-in-ef-core-30k87fpj</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>EfCore</category>
      <category>application-configuration</category>
      <category>entity-framework-core</category>
      <category>asp.net-core</category>
      <title>How to Dynamically Set the Connection String in EF Core</title>
      <description>In this article, I'll walk you through creating a practical solution for dynamic connection string resolution by building a real ASP.NET Core application. We'll start with a standard template and gradually implement our own IConnectionStringResolver pattern.</description>
      <pubDate>Fri, 26 Sep 2025 12:20:10 Z</pubDate>
      <a10:updated>2026-09-25T22:00:57Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Dynamically Set the Connection String in EF Core</h1>
<p>In modern web applications, there are scenarios where you need to determine which database to connect to at runtime rather than at compile time. This could be for multi-tenant applications, environment-specific configurations, or modular architectures where different parts of your application connect to different databases.</p>
<p>In this article, I'll walk you through creating a practical solution for dynamic connection string resolution by building a real ASP.NET Core application. We'll start with a standard template and gradually implement our own <code>IConnectionStringResolver</code> pattern.</p>
<blockquote>
<p><strong>Note</strong>: The code examples are simplified for demonstration purposes. Production applications require additional error handling, logging, and caching.</p>
</blockquote>
<h2>The Scenario: Building a Multi-Tenant Web Application</h2>
<p>Let's imagine we're building a SaaS application where different tenants can have their own databases. Some tenants share a common database, while premium tenants get their own dedicated database for better performance and data isolation.</p>
<p>Our requirements:</p>
<ul>
<li>Default behavior: Use the standard connection string</li>
<li>Multi-tenant support: Route tenants to their specific databases</li>
<li>Fallback mechanism: If a tenant-specific database isn't available, use the default</li>
<li>Simple tenant identification: Use query parameters for this example</li>
</ul>
<blockquote>
<p><strong>Note</strong>: We're building this from scratch to understand the concepts, but ABP Framework already handles all of this automatically - and does it much better! It supports separate databases for each tenant, different connection strings for different modules, and automatic fallback when connections aren't found. See how comprehensive ABP's approach is in the <a href="https://abp.io/docs/latest/framework/fundamentals/connection-strings">Connection Strings documentation</a>.</p>
</blockquote>
<h3>Step 1: Creating the Project</h3>
<p>First, create a new ASP.NET Core Web App with Razor Pages and Individual Authentication:</p>
<pre><code class="language-bash">dotnet new webapp --auth Individual -n DynamicConnectionDemo
cd DynamicConnectionDemo
</code></pre>
<p>When you open the project, you'll see the default connection in <code>appsettings.json</code>:</p>
<pre><code class="language-json">{
  &quot;ConnectionStrings&quot;: {
    &quot;DefaultConnection&quot;: &quot;DataSource=app.db;Cache=Shared&quot;
  }
}
</code></pre>
<p>And in <code>Program.cs</code>, you'll find the standard Entity Framework configuration:</p>
<pre><code class="language-csharp">var connectionString = builder.Configuration.GetConnectionString(&quot;DefaultConnection&quot;) ??  
                       throw new InvalidOperationException(&quot;Connection string 'DefaultConnection' not found.&quot;);  

builder.Services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;  
    options.UseSqlite(connectionString));  

builder.Services.AddDefaultIdentity&lt;IdentityUser&gt;(options =&gt; options.SignIn.RequireConfirmedAccount = true)  
    .AddEntityFrameworkStores&lt;ApplicationDbContext&gt;();
</code></pre>
<h3>Step 2: Testing the Base Application</h3>
<p>Run the application and test the registration/login functionality to ensure everything works:</p>
<pre><code class="language-bash">dotnet run
</code></pre>
<p>Navigate to the registration page, create an account, and verify that the basic authentication flow works correctly.</p>
<h2>Building Our Connection String Resolver</h2>
<p>Now let's implement our dynamic connection string resolver. We'll start by defining the interface and implementation.</p>
<h3>Step 3: Creating the Interface</h3>
<p>Create a new interface <code>IConnectionStringResolver</code> in <code>Data</code> folder:</p>
<pre><code class="language-csharp">public interface IConnectionStringResolver
{
    string Resolve(string connectionName = null);
}
</code></pre>
<h3>Step 4: Implementing the Resolver</h3>
<p>Create the <code>ConnectionStringResolver</code> class in <code>Data</code> folder:</p>
<pre><code class="language-csharp">public class ConnectionStringResolver : IConnectionStringResolver
{
    private readonly IConfiguration _configuration;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ConnectionStringResolver(
        IConfiguration configuration,
        IHttpContextAccessor httpContextAccessor)
    {
        _configuration = configuration;
        _httpContextAccessor = httpContextAccessor;
    }

    public string Resolve(string connectionName = null)
    {
        // Add caching logic here if needed
        return GetConnectionString(connectionName);
    }

    private string GetConnectionString(string connectionName)
    {
        // Try to get given named connection string
        if (!string.IsNullOrEmpty(connectionName))
        {
            var connectionString = _configuration.GetConnectionString(connectionName);
            if (!string.IsNullOrEmpty(connectionString))
            {
                return connectionString;
            }
        }

        // Try to get tenant-specific connection string (for multi-tenant apps)
        var tenantId = GetCurrentTenantIdOrNull();
        if (!string.IsNullOrEmpty(tenantId))
        {
            var tenantConnectionString = _configuration.GetConnectionString($&quot;Tenant_{tenantId}&quot;);
            if (!string.IsNullOrEmpty(tenantConnectionString))
            {
                return tenantConnectionString;
            }
        }

        // Fallback to default connection string
        return _configuration.GetConnectionString(&quot;DefaultConnection&quot;);
    }

    private string? GetCurrentTenantIdOrNull()
    {
        var context = _httpContextAccessor.HttpContext;
        if (context == null)
        {
            return null;
        }

        // Adds support for subdomain-based, route-based, or header-based tenant identification
        
        // Example: Query string-based tenant identification
        if (context.Request.Query.ContainsKey(&quot;tenant&quot;))
        {
            return context.Request.Query[&quot;tenant&quot;].ToString();
        }

        return null;
    }
}
</code></pre>
<h3>Step 5: Registering the Service</h3>
<p>Add the service registration to your <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.Services.AddScoped&lt;IConnectionStringResolver, ConnectionStringResolver&gt;();
</code></pre>
<h3>Step 6: Updating the DbContext Configuration</h3>
<p>Now we need to modify our <code>Program.cs</code> to use the resolver instead of the static connection string.</p>
<p>Replace this code:</p>
<pre><code class="language-csharp">var connectionString = builder.Configuration.GetConnectionString(&quot;DefaultConnection&quot;) ??  
                       throw new InvalidOperationException(&quot;Connection string 'DefaultConnection' not found.&quot;);  
builder.Services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt;  
    options.UseSqlite(connectionString));
</code></pre>
<p>With this simpler version:</p>
<pre><code class="language-csharp">builder.Services.AddDbContext&lt;ApplicationDbContext&gt;();
</code></pre>
<h3>Step 7: Modifying ApplicationDbContext</h3>
<p>Update your <code>ApplicationDbContext</code> in <code>Data</code> folder to use the resolver:</p>
<pre><code class="language-csharp">public class ApplicationDbContext : IdentityDbContext
{
    private readonly IConnectionStringResolver _connectionStringResolver;

    public ApplicationDbContext(DbContextOptions&lt;ApplicationDbContext&gt; options, IConnectionStringResolver connectionStringResolver)
        : base(options)
    {
        _connectionStringResolver = connectionStringResolver;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            var connectionString = _connectionStringResolver.Resolve();
            optionsBuilder.UseSqlite(connectionString);
        }
    }
}
</code></pre>
<h3>Step 8: Testing the Implementation</h3>
<p>Let's add a simple way to see our resolver in action. Update your <code>Pages/Index.cshtml</code>:</p>
<pre><code class="language-html">@page
@model IndexModel
@inject IConnectionStringResolver ConnectionStringResolver

@{
    ViewData[&quot;Title&quot;] = &quot;Home page&quot;;
}

&lt;div class=&quot;text-center&quot;&gt;
    &lt;h1 class=&quot;display-4&quot;&gt;Welcome&lt;/h1&gt;
    &lt;p&gt;Connection String: @ConnectionStringResolver.Resolve()&lt;/p&gt;
&lt;/div&gt;
</code></pre>
<h3>Step 9: Adding Multi-Tenant Configuration</h3>
<p>To test the multi-tenant functionality, add some tenant-specific connection strings to your <code>appsettings.json</code>:</p>
<pre><code class="language-json">{
  &quot;ConnectionStrings&quot;: {
    &quot;DefaultConnection&quot;: &quot;DataSource=app.db;Cache=Shared&quot;,
    &quot;Tenant_acme&quot;: &quot;DataSource=acme.db;Cache=Shared&quot;,
    &quot;Tenant_globex&quot;: &quot;DataSource=globex.db;Cache=Shared&quot;
  }
}
</code></pre>
<h3>Step 10: Testing Multi-Tenant Functionality</h3>
<p>Now run your application and test the multi-tenant functionality:</p>
<ol>
<li><strong>Default behavior</strong>: Visit <code>https://localhost:5001/</code> - you should see the default connection string</li>
<li><strong>Tenant-specific</strong>: Visit <code>https://localhost:5001/?tenant=acme</code> - you should see the ACME tenant's connection string</li>
<li><strong>Another tenant</strong>: Visit <code>https://localhost:5001/?tenant=globex</code> - you should see the Globex tenant's connection string</li>
<li><strong>Non-existent tenant</strong>: Visit <code>https://localhost:5001/?tenant=unknown</code> - you should see the default connection string (fallback behavior)</li>
</ol>
<blockquote>
<p><strong>Note</strong>: The port number may be different on your system. Check the console output when you run <code>dotnet run</code> to see the actual URL.</p>
</blockquote>
<blockquote>
<p><strong>Important</strong>: This demo shows that our connection string resolver is working correctly, but it only displays which connection string would be used. In a real application, thanks to our <code>ApplicationDbContext</code> modifications, the actual database operations would use the resolved connection string automatically. I kept this demo simple for clarity, but if you create actual tenant databases and test with real data operations, you'll see it works as expected.</p>
</blockquote>
<h2>Understanding the Implementation</h2>
<p>Let's break down what we've accomplished:</p>
<p>Our resolver follows this priority order:</p>
<ol>
<li><strong>Named Connection</strong>: If a specific connection name is provided, use that</li>
<li><strong>Tenant-Specific</strong>: Check for tenant-specific connection strings</li>
<li><strong>Default Fallback</strong>: Use the default connection string</li>
</ol>
<blockquote>
<p><strong>Production Note</strong>: This example uses query string parameters for simplicity. In production, you might use subdomains (<code>acme.myapp.com</code>), custom headers (<code>X-Tenant-ID</code>), route parameters, or JWT claims for tenant identification. Also consider adding caching, proper error handling and so on.</p>
</blockquote>
<h2>How ABP Framework Handles This</h2>
<p>The approach we've implemented above is very similar to how ABP Framework handles dynamic connection strings. ABP provides a built-in <code>IConnectionStringResolver</code> that works almost identically to our custom implementation, but with additional enterprise features:</p>
<h3>ABP's IConnectionStringResolver</h3>
<p>ABP Framework includes a sophisticated connection string resolver that:</p>
<ul>
<li>Automatically handles multi-tenancy scenarios</li>
<li>Supports module-specific connection strings out of the box</li>
<li>Integrates seamlessly with ABP's configuration system</li>
<li>Provides advanced caching and performance optimizations</li>
</ul>
<pre><code class="language-csharp">// In ABP applications, you can simply inject IConnectionStringResolver
public class ProductService : ITransientDependency
{
    private readonly IConnectionStringResolver _connectionStringResolver;

    public ProductService(IConnectionStringResolver connectionStringResolver)
    {
        _connectionStringResolver = connectionStringResolver;
    }

    public async Task&lt;string&gt; GetConnectionStringAsync()
    {
        // ABP automatically handles tenant context, module resolution, and fallbacks
        return await _connectionStringResolver.ResolveAsync(&quot;ProductModule&quot;);
    }
}
</code></pre>
<p>ABP's version provides automatic tenant detection, module integration, and enterprise features out of the box.</p>
<h2>Conclusion</h2>
<p>The <code>IConnectionStringResolver</code> pattern provides a clean way to handle dynamic connection strings in ASP.NET applications. By centralizing connection string logic, you can easily support multi-tenant scenarios, environment-specific configurations, and modular architectures.</p>
<p>This pattern is particularly valuable for applications that need to scale and adapt to different deployment scenarios. Whether you implement your own resolver or use ABP Framework's built-in solution, this approach will make your application more flexible.</p>
<h2>Further Reading</h2>
<ul>
<li><a href="https://docs.microsoft.com/en-us/ef/core/">Entity Framework Core Documentation</a></li>
<li><a href="https://docs.microsoft.com/en-us/ef/core/miscellaneous/multitenancy">Multi-tenant Applications with EF Core</a></li>
<li><a href="https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-strings-and-configuration-files">Connection Strings and Configuration in .NET</a></li>
<li><a href="https://abp.io/docs/latest/framework/architecture/multi-tenancy">ABP Framework Multi-Tenancy</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1c9824-4b80-7c83-539d-e75900526af8" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1c9824-4b80-7c83-539d-e75900526af8" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/understanding-the-domain-and-application-layers-in-abp-framework-1fipc4x4</guid>
      <link>https://abp.io/community/posts/understanding-the-domain-and-application-layers-in-abp-framework-1fipc4x4</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <title>Understanding the Domain and Application Layers in ABP Framework</title>
      <description>A deep dive into how ABP Framework structures Domain and Application layers using Domain-Driven Design principles. Learn how to model business logic cleanly with entities, aggregates, services, and use case orchestration.

</description>
      <pubDate>Fri, 16 May 2025 09:36:07 Z</pubDate>
      <a10:updated>2026-04-26T21:17:26Z</a10:updated>
      <content:encoded><![CDATA[A deep dive into how ABP Framework structures Domain and Application layers using Domain-Driven Design principles. Learn how to model business logic cleanly with entities, aggregates, services, and use case orchestration.

<br \><a href="https://www.berkansasmaz.com/understanding-the-domain-and-application-layers-in-abp-framework/" rel="nofollow noopener noreferrer" title="Go to the Post">Go to the Post</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a19eaa0-4f13-5283-5019-2ee96c0ebc95" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a19eaa0-4f13-5283-5019-2ee96c0ebc95" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-do-we-maintain-code-quality-and-technical-debt-in-our-.net-codebase-z7glpya1</guid>
      <link>https://abp.io/community/posts/how-do-we-maintain-code-quality-and-technical-debt-in-our-.net-codebase-z7glpya1</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <title>How Do We Maintain Code Quality and Technical Debt in Our .NET Codebase?</title>
      <description>Maintaining code quality and managing technical debt is a never-ending journey in any codebase — and it’s no different for us working with .NET. As a maintainer of the ABP Framework and someone who regularly provides support and training for developers building real-world enterprise applications with ABP, I want to share some practical strategies and lessons we apply to keep our codebase healthy.</description>
      <pubDate>Tue, 22 Apr 2025 14:21:56 Z</pubDate>
      <a10:updated>2026-04-23T20:49:00Z</a10:updated>
      <content:encoded><![CDATA[Maintaining code quality and managing technical debt is a never-ending journey in any codebase — and it’s no different for us working with .NET. As a maintainer of the ABP Framework and someone who regularly provides support and training for developers building real-world enterprise applications with ABP, I want to share some practical strategies and lessons we apply to keep our codebase healthy.<br \><a href="https://www.berkansasmaz.com/how-do-we-maintain-code-quality-and-technical-debt-in-our-net-codebase/" rel="nofollow noopener noreferrer" title="Go to the Post">Go to the Post</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a19700d-5afb-fabc-46de-a85446a287c3" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a19700d-5afb-fabc-46de-a85446a287c3" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/building-a-crud-api-with-abp-framework-asp.net-core-and-postgresql-elrj0old</guid>
      <link>https://abp.io/community/posts/building-a-crud-api-with-abp-framework-asp.net-core-and-postgresql-elrj0old</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <title>Building a CRUD API with ABP Framework, ASP.NET Core, and PostgreSQL</title>
      <description>This guide builds a CRUD API with ABP Framework, ASP.NET Core, and PostgreSQL, improving a basic example with permissions. It creates a solid API for managing products using ABP’s handy features.</description>
      <pubDate>Fri, 21 Feb 2025 15:47:16 Z</pubDate>
      <a10:updated>2026-04-25T02:14:20Z</a10:updated>
      <content:encoded><![CDATA[This guide builds a CRUD API with ABP Framework, ASP.NET Core, and PostgreSQL, improving a basic example with permissions. It creates a solid API for managing products using ABP’s handy features.<br \><a href="https://dev.to/berkansasmazz/building-a-crud-api-with-abp-framework-aspnet-core-and-postgresql-14p8" rel="nofollow noopener noreferrer" title="Go to the Post">Go to the Post</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a183b5d-e971-9fe5-2c76-ad7f0199dadc" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a183b5d-e971-9fe5-2c76-ad7f0199dadc" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/enhance-your-abp-project-with-custom-cursor-rules-pzwxzumo</guid>
      <link>https://abp.io/community/posts/enhance-your-abp-project-with-custom-cursor-rules-pzwxzumo</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>ai</category>
      <category>cursor</category>
      <category>live-coding</category>
      <title>Enhance Your ABP Project with Custom Cursor Rules</title>
      <description>Learn how to integrate custom Cursor rules into your ABP-based backend to improve code quality and streamline your development process.</description>
      <pubDate>Fri, 07 Feb 2025 14:06:48 Z</pubDate>
      <a10:updated>2026-04-22T14:28:31Z</a10:updated>
      <content:encoded><![CDATA[Learn how to integrate custom Cursor rules into your ABP-based backend to improve code quality and streamline your development process.<br \><a href="https://www.berkansasmaz.com/building-my-latest-project-with-asp-net-blazor-and-cursor-a-journey-to-abp/" rel="nofollow noopener noreferrer" title="Go to the Post">Go to the Post</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a17f2e8-e820-e97c-ccb3-d9ea10f24d6f" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a17f2e8-e820-e97c-ccb3-d9ea10f24d6f" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-use-openai-api-with-abp-framework-rsfvihla</guid>
      <link>https://abp.io/community/posts/how-to-use-openai-api-with-abp-framework-rsfvihla</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>ai</category>
      <title>How to Use OpenAI API with ABP Framework</title>
      <description>This article demonstrates how to integrate OpenAI API with the ABP Framework through three practical examples: Chat, Retrieval-Augmented Generation (RAG), and Image Generation. </description>
      <pubDate>Mon, 02 Dec 2024 14:59:48 Z</pubDate>
      <a10:updated>2026-09-25T20:29:47Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Use OpenAI API with ABP Framework</h1>
<p>In this article, I will show you how to integrate and use the <a href="https://github.com/openai/openai-dotnet?tab=readme-ov-file#getting-started">OpenAI API</a> with the <a href="https://abp.io/">ABP Framework</a>. We will explore step-by-step how these technologies can work together to enhance your application with powerful AI capabilities, such as natural language processing, image generation, and more.</p>
<h2>Creating an ABP Project</h2>
<p>To begin integrating OpenAI API with ABP Framework, you first need to create an ABP project. Follow these steps to create and set up your ABP project:</p>
<h3>Step 1: Install ABP CLI</h3>
<p>The ABP CLI is a command-line interface tool that helps you create and manage ABP projects easily. To install the ABP CLI, run the following command in your terminal:</p>
<pre><code class="language-bash">dotnet tool install -g Volo.Abp.Studio.Cli
</code></pre>
<h3>Step 2: Create a New ABP Project</h3>
<p>Once you have installed the ABP CLI, you can create a new ABP project using the following command:</p>
<pre><code class="language-bash">abp new Acme.OpenAIIntegration -t app --ui-framework mvc --database-provider ef -dbms PostgreSQL --csf
</code></pre>
<blockquote>
<p>This command will generate a complete ABP project with an <a href="https://abp.io/docs/latest/framework/ui/mvc-razor-pages/overall">MVC UI</a>. The examples provided in this article make use of UI controllers for demonstration purposes. However, the same approach can easily be applied to other UI types supported by ABP, such as Blazor or Angular. You can find other options <a href="https://abp.io/docs/latest/cli">here</a>.</p>
</blockquote>
<h2>OpenAI Integration Setup</h2>
<p>To begin integrating OpenAI API with ABP Framework, follow these steps:</p>
<h3>Step 1: Create an API Key</h3>
<p>To use the OpenAI services, you first need an API key. To obtain one, first <a href="https://platform.openai.com/signup">create a new OpenAI account</a> or <a href="https://platform.openai.com/login">log in</a>. Next, navigate to the <a href="https://platform.openai.com/account/api-keys">API key page</a> and select &quot;Create new secret key&quot;, optionally naming the key. Make sure to save your API key somewhere safe and do not share it with anyone.</p>
<p>This key will be used to authenticate your application when making requests to the OpenAI endpoints.</p>
<h3>Step 2: Adding <em>Microsoft.Extensions.AI</em> Package</h3>
<p>To integrate OpenAI API with ABP, we use <a href="https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI/">Microsoft.Extensions.AI</a>. This package offers a unified API for integrating AI services, making it easy for developers to work with different AI providers. You can find more details in <a href="https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/">this blog post</a>.</p>
<p>To begin integrating OpenAI API with ABP Framework, follow these steps:</p>
<ol>
<li>Add the <strong>Microsoft.Extensions.AI</strong> and <strong>Microsoft.Extensions.AI.OpenAI</strong> (used to interact specifically with OpenAI services. Additionally, this package has alternatives like <a href="https://www.nuget.org/packages/Microsoft.Extensions.AI.OpenAI/">Azure OpenAI</a>, <a href="https://www.nuget.org/packages/Microsoft.Extensions.AI.AzureAIInference/">Azure AI Inference</a>, and <a href="https://www.nuget.org/packages/Microsoft.Extensions.AI.Ollama/">Ollama</a>, offering flexibility for developers to choose the AI provider that best fits their needs) packages:</li>
</ol>
<pre><code class="language-bash">dotnet add package Microsoft.Extensions.AI --prerelease
dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease
</code></pre>
<ol start="2">
<li>Add the required configuration to the <code>appsettings.json</code> file located inside the <code>Acme.OpenAIIntegration.Web</code> project and dependencies to your <code>ConfigureServices</code> method:</li>
</ol>
<pre><code class="language-json">&quot;AI&quot;: {
    &quot;OpenAI&quot;: {
        &quot;Key&quot;: &quot;YOUR-API-KEY&quot;,
        &quot;Chat&quot;: {
            &quot;ModelId&quot;: &quot;gpt-4o-mini&quot;
        }
    }
}
</code></pre>
<blockquote>
<p>Replace the value of the <code>Key</code> with your OpenAI API key.</p>
</blockquote>
<blockquote>
<p><strong>Important Security Note</strong>: Storing sensitive information like API keys in <code>appsettings.json</code> is not recommended due to security concerns. Please refer to the <a href="https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets">official Microsoft documentation</a> for secure secret management best practices.</p>
</blockquote>
<p>Next, add the following code to the <code>ConfigureServices</code> method in <code>OpenAIIntegrationBlazorModule</code>:</p>
<pre><code class="language-csharp">context.Services.AddSingleton(new OpenAIClient(configuration[&quot;AI:OpenAI:Key&quot;]));
  
context.Services.AddChatClient(services =&gt;  
    services.GetRequiredService&lt;OpenAIClient&gt;().AsChatClient(configuration[&quot;AI:OpenAI:Chat:ModelId&quot;] ?? &quot;gpt-4o-mini&quot;));
</code></pre>
<h2>Creating a Sample Page</h2>
<p>To demonstrate the use of OpenAI API, let's create a page named <code>Sample</code> in the <code>Acme.OpenAIIntegration.Web</code> project:</p>
<p>Create a <code>Sample</code> folder under the <code>Pages</code> folder of the <code>Acme.OpenAIIntegration.Web</code> project. Add a new Razor Page by right-clicking the <code>Sample</code> folder then selecting <code>Add &gt; Razor Page</code>. Name it <code>Index</code>.</p>
<p>Open the <code>Index.cshtml</code> and change the whole content as shown below:</p>
<blockquote>
<p>Note: This example demonstrates a simple implementation of a sample page that interacts with the OpenAI API, covering chat, <a href="https://github.com/openai/openai-dotnet?tab=readme-ov-file#how-to-use-assistants-with-retrieval-augmented-generation-rag">retrieval-augmented generation (RAG)</a>, and image generation features. Each example is explained in detail in the next section, so feel free to continue for a better understanding of the steps and logic involved.</p>
</blockquote>
<pre><code class="language-html">@page
@model Acme.OpenAIIntegration.Web.Pages.Sample
@{
    ViewData[&quot;Title&quot;] = &quot;OpenAI API Demonstration&quot;;
}

&lt;h1&gt;@ViewData[&quot;Title&quot;]&lt;/h1&gt;

&lt;br/&gt;&lt;br/&gt;

&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-4&quot;&gt;
        &lt;h2&gt;Chat Example&lt;/h2&gt;
        &lt;form method=&quot;post&quot; asp-page-handler=&quot;Chat&quot;&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;label asp-for=&quot;ChatInput&quot;&gt;Enter your message:&lt;/label&gt;
                &lt;textarea asp-for=&quot;ChatInput&quot; class=&quot;form-control&quot; rows=&quot;4&quot;&gt;&lt;/textarea&gt;
            &lt;/div&gt;
            &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary mt-2&quot;&gt;Send&lt;/button&gt;
        &lt;/form&gt;
        @if (!string.IsNullOrEmpty(Model.ChatResponse))
        {
            &lt;h3 class=&quot;mt-3&quot;&gt;Response:&lt;/h3&gt;
            &lt;p&gt;@Model.ChatResponse&lt;/p&gt;
        }
    &lt;/div&gt;

    &lt;div class=&quot;col-md-4&quot;&gt;
        &lt;h2&gt;RAG Example&lt;/h2&gt;
        &lt;form method=&quot;post&quot; asp-page-handler=&quot;RAG&quot;&gt;
            &lt;div class=&quot;form-group mt-2&quot;&gt;
                &lt;label asp-for=&quot;RAGQuery&quot;&gt;Query:&lt;/label&gt;
                &lt;input asp-for=&quot;RAGQuery&quot; class=&quot;form-control&quot; /&gt;
            &lt;/div&gt;
            &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary mt-2&quot;&gt;Ask&lt;/button&gt;
        &lt;/form&gt;
        @if (!string.IsNullOrEmpty(Model.RAGResponse))
        {
            &lt;h3 class=&quot;mt-3&quot;&gt;Result:&lt;/h3&gt;
            &lt;p&gt;@Model.RAGResponse&lt;/p&gt;
        }
    &lt;/div&gt;

    &lt;div class=&quot;col-md-4&quot;&gt;
        &lt;h2&gt;Image Generation Example&lt;/h2&gt;
        &lt;form method=&quot;post&quot; asp-page-handler=&quot;ImageGeneration&quot;&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;label asp-for=&quot;ImagePrompt&quot;&gt;Image Description:&lt;/label&gt;
                &lt;input asp-for=&quot;ImagePrompt&quot; class=&quot;form-control&quot; /&gt;
            &lt;/div&gt;
            &lt;button type=&quot;submit&quot; class=&quot;btn btn-primary mt-2&quot;&gt;Generate Image&lt;/button&gt;
        &lt;/form&gt;
        @if (Model.GeneratedImageBytes != null)
        {
            &lt;h3 class=&quot;mt-3&quot;&gt;Generated Image:&lt;/h3&gt;
            &lt;img src=&quot;https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/data%3Aimage/png%3Bbase64%2C%40Convert.ToBase64String%28Model.GeneratedImageBytes%29&quot; alt=&quot;Generated image&quot; class=&quot;img-fluid mt-2&quot; /&gt;
        }
    &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p><code>Index.cshtml.cs</code> content should be like that:</p>
<pre><code class="language-csharp">using System;
using System.ClientModel;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Assistants;
using OpenAI.Files;
using OpenAI.Images;

namespace Acme.OpenAIIntegration.Web.Pages;

public class Sample : PageModel
{
    [BindProperty]
    public string ChatInput { get; set; }
    public string ChatResponse { get; set; }

    [BindProperty]
    public string RAGQuery { get; set; }
    public string RAGResponse { get; set; }

    [BindProperty]
    public string ImagePrompt { get; set; }
    public byte[] GeneratedImageBytes { get; set; }
    
    private readonly IChatClient _chatClient;
    private readonly OpenAIClient _openAiClient;

    public Sample(
        IChatClient chatClient,
        OpenAIClient openAiClient)
    {
        _chatClient = chatClient;
        _openAiClient = openAiClient;
    }

    public async Task&lt;IActionResult&gt; OnPostChatAsync()
    {
        ChatResponse = $&quot;Chat response: {(await _chatClient.CompleteAsync(ChatInput)).Message}&quot;;
        return Page();
    }

    public async Task&lt;IActionResult&gt; OnPostRAGAsync()
    {
#pragma warning disable OPENAI001
        var fileClient = _openAiClient.GetOpenAIFileClient();
        var assistantClient = _openAiClient.GetAssistantClient();

        using var document = BinaryData.FromBytes(GetExceptionHandlingDocumentContent().ToArray()).ToStream();
        var exceptionHandlingDoc = await fileClient.UploadFileAsync(
            document,
            &quot;ExceptionHandling.md&quot;,
            FileUploadPurpose.Assistants);

        AssistantCreationOptions assistantOptions = new()
        {
            Name = &quot;Exception Handling Assistant&quot;,
            Instructions =
                &quot;&quot;&quot;
                This assistant helps you with exception handling in ABP Framework. You can ask questions about exception handling and get answers.

                - Do not make any assumptions when asked for information that is not in the document
                - Give the most accurate information possible
                - Give short(max 1-2 sentence) and concise answers
                - Do not provide file citations
                &quot;&quot;&quot;,

            Tools =
            {
                new FileSearchToolDefinition(),
            },
            ToolResources = new()
            {
                FileSearch = new()
                {
                    NewVectorStores =
                    {
                        new VectorStoreCreationHelper([exceptionHandlingDoc.Value.Id]),
                    }
                }
            },
        };

        var assistant = await assistantClient.CreateAssistantAsync(&quot;gpt-4o&quot;, assistantOptions);

        ThreadCreationOptions threadOptions = new()
        {
            InitialMessages = { RAGQuery }
        };

        ThreadRun threadRun = assistantClient.CreateThreadAndRun(assistant.Value.Id, threadOptions);

        do
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            threadRun = assistantClient.GetRun(threadRun.ThreadId, threadRun.Id);
        } while (!threadRun.Status.IsTerminal);

        CollectionResult&lt;ThreadMessage&gt; messages
            = assistantClient.GetMessages(threadRun.ThreadId,
                new MessageCollectionOptions() { Order = MessageCollectionOrder.Ascending });

        var response = new StringBuilder();

        foreach (var message in messages)
        {
            response.AppendLine($&quot;[{message.Role.ToString().ToUpper()}]: &quot;);
            foreach (var contentItem in message.Content)
            {
                if (!string.IsNullOrEmpty(contentItem.Text))
                {
                    response.AppendLine(contentItem.Text);

                    if (contentItem.TextAnnotations.Count &gt; 0)
                    {
                        response.AppendLine(&quot;&quot;);
                    }
                }
            }

            response.AppendLine(&quot;&quot;);
#pragma warning restore OPENAI001
        }
        
        RAGResponse = response.ToString();
        
        return Page();
    }

    public async Task&lt;IActionResult&gt; OnPostImageGenerationAsync()
    {
        var client = _openAiClient.GetImageClient(&quot;dall-e-3&quot;);

        var image = await client.GenerateImageAsync(ImagePrompt, new ImageGenerationOptions
        {
            ResponseFormat = GeneratedImageFormat.Bytes
        });
        
        var imageBytes = image.Value.ImageBytes;
        
        using var memoryStream = new MemoryStream();
        await imageBytes.ToStream().CopyToAsync(memoryStream);
        GeneratedImageBytes = memoryStream.ToArray();
        
        return Page();
    }

    public ReadOnlySpan&lt;byte&gt; GetExceptionHandlingDocumentContent()
    {
        return &quot;&quot;&quot;
               # Exception Handling

               ABP provides a built-in infrastructure and offers a standard model for handling exceptions.

               * Automatically **handles all exceptions** and sends a standard **formatted error message** to the client for an API/AJAX request.
               * Automatically hides **internal infrastructure errors** and returns a standard error message.
               * Provides an easy and configurable way to **localize** exception messages.
               * Automatically maps standard exceptions to **HTTP status codes** and provides a configurable option to map custom exceptions.

               ## Automatic Exception Handling

               `AbpExceptionFilter` handles an exception if **any of the following conditions** are met:

               * Exception is thrown by a **controller action** which returns an **object result** (not a view result).
               * The request is an AJAX request (`X-Requested-With` HTTP header value is `XMLHttpRequest`).
               * Client explicitly accepts the `application/json` content type (via `accept` HTTP header).

               If the exception is handled it's automatically **logged** and a formatted **JSON message** is returned to the client.
               
               ## Business Exceptions
               
               Most of your own exceptions will be business exceptions. The `IBusinessException` interface is used to mark an exception as a business exception.
               
               `BusinessException` implements the `IBusinessException` interface in addition to the `IHasErrorCode`, `IHasErrorDetails` and `IHasLogLevel` interfaces. The default log level is `Warning`.
               
               Usually you have an error code related to a particular business exception. For example:
               
               ````C#
               throw new BusinessException(QaErrorCodes.CanNotVoteYourOwnAnswer);
               ````

               ### User Friendly Exception
               
               If an exception implements the `IUserFriendlyException` interface, then ABP does not change it's `Message` and `Details` properties and directly send it to the client.
               
               `UserFriendlyException` class is the built-in implementation of the `IUserFriendlyException` interface. Example usage:
               
               ````C#
               throw new UserFriendlyException(
                   &quot;Username should be unique!&quot;
               );
               ````
               * The `IUserFriendlyException` interface is derived from the `IBusinessException` and the `UserFriendlyException` class is derived from the `BusinessException` class.
    
               &quot;&quot;&quot;u8;
    }
}
</code></pre>
<h2>Running the Application</h2>
<p>After completing the setup, you can run the application using the following command:</p>
<pre><code class="language-bash"> dotnet run --project ./src/Acme.OpenAIIntegration.Web
</code></pre>
<p>Once the application is running, open your browser and navigate to <code>/Sample</code>. You should see the <code>Sample</code> page we created, which contains sections for Chat, RAG (Retrieval-Augmented Generation), and Image Generation. You can find the screenshot of the page below:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/sample-page.png" alt="sample page" /></p>
<h2>Examples Overview</h2>
<p>To showcase the integration of the OpenAI API with the ABP Framework, we implemented three different examples:</p>
<ol>
<li><strong>Chat Example</strong>: This example demonstrates how to use OpenAI's chat capabilities by allowing users to enter a message and receive an AI-generated response. The implementation involves setting up a simple form on the <code>Sample</code> page where users can input their message. The form submission triggers the <code>OnPostChatAsync</code> method, which uses the <code>IChatClient</code> to generate a response.</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/chat-example.gif" alt="chat-example" /></p>
<ol start="2">
<li><strong>Retrieval-Augmented Generation (RAG) Example</strong>: In this example, we use OpenAI to answer user queries by referencing custom documents uploaded to the OpenAI API. The implementation involves uploading a document using the <code>OpenAIFileClient</code> and creating an assistant with specific instructions to handle the uploaded content. In this case, the document is a section from ABP's Exception Handling documentation, which includes examples on how ABP handles exceptions, user-friendly error messages, and business exceptions. Users can input their query on the <code>Sample</code> page, and the <code>OnPostRAGAsync</code> method processes the query to generate precise answers based on the document content. If users ask questions that are not covered in the document, the assistant clearly indicates that the information is not available, as per the instructions provided. For example, when asked about <code>Object Extensions</code>, the response begins with: &quot;The uploaded document does not contain information about <code>Object Extensions</code>...&quot;. This demonstrates how the assistant adheres to the provided instructions. You can also find this example illustrated in the GIF below.</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-1.gif" alt="rag-example-1" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/rag-example-2.gif" alt="rag-example-2" /></p>
<ol start="3">
<li><strong>Image Generation Example</strong>: This example leverages the <a href="https://openai.com/index/dall-e-3/">DALL-E</a> model to generate images based on user-provided prompts. On the <code>Sample</code> page, users can provide a description of the image they want to generate, and the <code>OnPostImageGenerationAsync</code> method uses the <code>OpenAIClient</code> to generate the image.</li>
</ol>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-12-01-OpenAI-Integration/image-generation-example.gif" alt="image-generation-example" /></p>
<h2>Conclusion</h2>
<p>In this article, we covered how to integrate the OpenAI API with the ABP Framework by creating a sample project, setting up the OpenAI services, and implementing examples for conversational AI, knowledge-based assistance, and image generation. By following these steps, you can add powerful AI-driven capabilities to your application, making it more interactive, intelligent, and capable of meeting user needs effectively.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a169a0f-59bf-0d78-0bc5-fe242baf35d0" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a169a0f-59bf-0d78-0bc5-fe242baf35d0" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/ef-core-9-readonly-primitive-collections-iy6ztbx8</guid>
      <link>https://abp.io/community/posts/ef-core-9-readonly-primitive-collections-iy6ztbx8</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>dotnet-9.0</category>
      <title>EF Core 9 Read-only Primitive Collections</title>
      <description>In this article, we will explore the new features introduced in EF Core 9, specifically focusing on Read-only Primitive Collections.</description>
      <pubDate>Fri, 15 Nov 2024 06:16:24 Z</pubDate>
      <a10:updated>2026-09-26T01:43:21Z</a10:updated>
      <content:encoded><![CDATA[<h1>EF Core 9 Read-only Primitive Collections</h1>
<p>In this article, we will explore the new features introduced in EF Core 9, specifically focusing on Read-only Primitive Collections. EF Core 8 introduced support for mapping arrays and mutable lists of primitive types, and you can read more about it <a href="https://abp.io/community/articles/ef-core-8-primitive-collections-ttn5b6xp">here</a>. This has been expanded in EF Core 9 to include read-only collections/lists. Specifically, EF Core 9 supports collections typed as <code>IReadOnlyList</code>, <code>IReadOnlyCollection</code>, or <code>ReadOnlyCollection</code>.</p>
<h2>Introduction to EF Core 9 Read-only Primitive Collections</h2>
<p>Entity Framework Core 9 introduces several enhancements, one of which is the support for Read-only Primitive Collections. This feature aims to provide better support for scenarios where collections of primitive types, such as <code>int</code>, <code>string</code>, or <code>bool</code>, need to be used in a read-only manner in your entity classes. Previously, developers had to use complex workarounds to ensure collections couldn't be modified, but EF Core 9 now provides a simpler, built-in solution to handle this more effectively.</p>
<h3>Why Read-only Primitive Collections Matter</h3>
<p>Read-only Primitive Collections are particularly useful when you need to guarantee the integrity of certain data within your entities. For example, imagine you have a <code>Car</code> entity that has a collection of <code>Colors</code>, represented as a set of enums. You might not want these colors to be modified after they're initially set, ensuring that any business logic reliant on these values remains consistent.</p>
<p>EF Core 9 introduces a convenient way to define these collections as read-only, helping developers maintain stricter control over their data.</p>
<h3>How It Works</h3>
<p>Defining a read-only primitive collection is quite straightforward in EF Core 9. You can use the <code>IReadOnlyList&lt;T&gt;</code>, <code>IReadOnlyCollection&lt;T&gt;</code>, or <code>ReadOnlyCollection&lt;T&gt;</code> types to declare your properties, ensuring a consistent read-only behavior. This helps maintain data integrity by preventing modifications after the collection is set. Below is an example that includes a <code>Car</code> class and a <code>Color</code> enum. The <code>Car</code> class has a <code>Colors</code> property that holds a read-only list of available colors, ensuring that these values cannot be modified after being initially set:</p>
<pre><code class="language-csharp">public enum Color
{
    Black,
    White,
    Red,
    Blue
}

public class Car
{
    public int Id { get; set; }
    public string Brand { get; set; }
    public string Model { get; set; }
    public IReadOnlyList&lt;Color&gt; Colors { get; private set; } = new List&lt;Color&gt; { Color.Black, Color.White }.AsReadOnly();
  
    protected Car()
    {
    	/* This constructor is for deserialization / ORM purpose */
    }

    public Car(string brand, string model, IEnumerable&lt;Color&gt; colors)
    {
        Brand = brand;
        Model = model;
        Colors = colors.ToList().AsReadOnly();
    }
}
</code></pre>
<p>In the example above, <code>Colors</code> is defined as a read-only list, preventing any accidental modifications once it is set. This ensures that data integrity is maintained without the need for manual validation.</p>
<p>To query cars with specific colors, you can use the following example:</p>
<pre><code class="language-csharp">var colors = new List&lt;Color&gt; { Color.Black, Color.White };
var cars = await context.Cars
    .Where(c =&gt; c.Colors.Intersect(colors).Any())
    .ToListAsync();
</code></pre>
<p>The query selects all cars that have any of the specified colors in their <code>Colors</code> collection.</p>
<p>The SQL result looks like this; as you can see, it sends colors as parameters instead of adding them inline. It also uses the <code>json_each</code> function to deserialize on the database side:</p>
<pre><code class="language-sql">SELECT &quot;c&quot;.&quot;id&quot;,
       &quot;c&quot;.&quot;brand&quot;,
       &quot;c&quot;.&quot;colors&quot;,
       &quot;c&quot;.&quot;model&quot;
FROM   &quot;cars&quot; AS &quot;c&quot;
WHERE  EXISTS (SELECT 1
               FROM   (SELECT &quot;c0&quot;.&quot;value&quot;
                       FROM   Json_each(&quot;c&quot;.&quot;colors&quot;) AS &quot;c0&quot;
                       INTERSECT
                       SELECT &quot;c1&quot;.&quot;value&quot;
                       FROM   Json_each(@__colors_0) AS &quot;c1&quot;) AS &quot;i&quot;) 
</code></pre>
<h3>Conclusion</h3>
<p>Read-only primitive collections make it easier to enforce data integrity by preventing changes to your collection data. This feature helps simplify your code while ensuring that critical parts of your data remain consistent.</p>
<h2>References</h2>
<ul>
<li>https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew#read-only-primitive-collections</li>
<li>https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#primitive-collections</li>
<li>https://abp.io/community/articles/ef-core-8-primitive-collections-ttn5b6xp</li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-use-.net-aspire-with-abp-framework-h29km4kk</guid>
      <link>https://abp.io/community/posts/how-to-use-.net-aspire-with-abp-framework-h29km4kk</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>aspire</category>
      <title>How to use .NET Aspire with ABP framework</title>
      <description>This guide explores how to combine .NET Aspire with ABP, enabling developers to create observable, and feature-rich applications.</description>
      <pubDate>Fri, 28 Jun 2024 13:17:27 Z</pubDate>
      <a10:updated>2026-09-25T20:37:10Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to use Aspire with ABP framework</h1>
<p><a href="https://aspire.dev/get-started/what-is-aspire/">Aspire</a> is an opinionated, cloud-ready stack designed for building observable, production-ready, and distributed applications. On the other hand, the <a href="https://docs.abp.io/en/abp/latest">ABP framework</a> offers a complete, modular and layered software architecture based on Domain Driven Design principles and patterns. This guide explores how to combine Aspire with ABP, enabling developers to create observable, and feature-rich applications.</p>
<h2>When to Use Aspire?</h2>
<p>Using Aspire with the ABP framework can be beneficial in various scenarios where you need to combine the strengths of both technologies. Here are some situations when using Aspire with ABP can be advantageous:</p>
<ul>
<li><strong>Enterprise Web Applications:</strong> ABP is well-suited for building enterprise web applications with its opinionated architecture and best practices. When combined with Aspire, you can leverage ABP's features for rapid development of user interfaces, backend services, and business logic while benefiting from Aspire's cloud-native capabilities and observability features.</li>
<li><strong>Observability and Monitoring:</strong> Aspire's emphasis on observability, including logging, monitoring, and tracing, can enhance ABP applications by providing deeper insights into system behavior, performance metrics, and diagnostics, which is key for maintaining and optimizing enterprise-grade applications.</li>
</ul>
<h2>Creating a new ABP Solution</h2>
<p>To demonstrate the usage of Aspire with the ABP framework, I've created an ABP solution. If you want to create the same solution from scratch, follow the steps below:</p>
<p>Install the ABP CLI if you haven't installed it before:</p>
<pre><code class="language-bash">dotnet tool install -g Volo.Abp.Cli
</code></pre>
<p>Create a new solution with the ABP framework's Application Startup Template with Tiered MVC UI and EF Core database:</p>
<pre><code class="language-bash">abp new AspirationalAbp -u mvc --database-provider ef -dbms PostgreSQL --csf --tiered
</code></pre>
<blockquote>
<p>The startup template selection matters for this article. I chose these options so that the demo solution can cover complex scenarios.</p>
</blockquote>
<p><strong>Disclaimer-I:</strong> This article is based on version <code>8.0.1</code> of Aspire and version <code>8.2.0</code> of ABP Framework.</p>
<p><strong>Disclaimer-II:</strong> ABP and Aspire may not be fully compatible in some respects. This article aims to explain how these two technologies can be used together in the simplest way possible, even if they are not fully compatible.</p>
<h2>Add Aspire</h2>
<p>After creating the solution, run the following commands in the <code>src</code> folder of your solution to add Aspire:</p>
<pre><code class="language-bash">// Adding AppHost
dotnet new aspire-apphost -n AspirationalAbp.AppHost
dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj

// Adding ServiceDefaults
dotnet new aspire-servicedefaults -n AspirationalAbp.ServiceDefaults
dotnet sln ../AspirationalAbp.sln add ./AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj
</code></pre>
<p>These commands add two new projects to the solution:</p>
<ul>
<li><strong>AspirationalAbp.AppHost</strong>: An orchestrator project designed to connect and configure the different projects and services of your app.</li>
<li><strong>AspirationalAbp.ServiceDefaults</strong>: An Aspire shared project to manage configurations that are reused across the projects in your solution related to <a href="https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience">resilience</a>, <a href="https://aspire.dev/fundamentals/service-discovery/">service discovery</a>, and <a href="https://aspire.dev/fundamentals/telemetry/">telemetry</a>.</li>
</ul>
<p>We have added Aspire to our ABP based solution, but we have not registered our projects in the Aspire orchestration. Now, let's enroll our projects, which implement the db migrator, web user interface, API, and auth, in Aspire orchestration.</p>
<h2>Registering projects to Aspire orchestration</h2>
<p>First of all, we need to add the reference of related projects to the <code>AspirationalAbp.AppHost</code> project. For this, add the following <code>ItemGroups</code> to the <code>AspirationalAbp.AppHost/AspirationalAbp.AppHost.csproj</code> file:</p>
<pre><code class="language-csharp">&lt;ItemGroup&gt;  
  &lt;ProjectReference Include=&quot;..\AspirationalAbp.AuthServer\AspirationalAbp.AuthServer.csproj&quot; /&gt;  
  &lt;ProjectReference Include=&quot;..\AspirationalAbp.HttpApi.Host\AspirationalAbp.HttpApi.Host.csproj&quot; /&gt;  
  &lt;ProjectReference Include=&quot;..\AspirationalAbp.Web\AspirationalAbp.Web.csproj&quot;/&gt;  
  &lt;ProjectReference Include=&quot;..\AspirationalAbp.DbMigrator\AspirationalAbp.DbMigrator.csproj&quot; /&gt;  
&lt;/ItemGroup&gt;  
  
&lt;ItemGroup&gt;  
  &lt;PackageReference Include=&quot;Aspire.Hosting.PostgreSQL&quot; Version=&quot;8.0.1&quot; /&gt;  
  &lt;PackageReference Include=&quot;Aspire.Hosting.Redis&quot; Version=&quot;8.0.1&quot; /&gt;  
&lt;/ItemGroup&gt;
</code></pre>
<p>With the first <code>ItemGroup</code>, we added the references of <code>AuthServer</code>, <code>HttpApi.Host</code>, <code>Web</code>, and <code>DbMigrator</code> projects to the app host project. So, we can orchestrate them within the app model.</p>
<p>With the second <code>ItemGroup</code>, to model the <strong>PostgreSQL</strong> server resource and <strong>Redis</strong> resource in the app host, installed the <code>Aspire.Hosting.PostgreSQL</code> and <code>Aspire.Hosting.Redis</code></p>
<p>Now let's update the <code>Program</code> class of the <code>AspirationalAbp.AppHost</code> project as follows:</p>
<pre><code class="language-csharp">using Microsoft.Extensions.Hosting;  
  
var builder = DistributedApplication.CreateBuilder(args);  
  
var postgres = builder  
    .AddPostgres(&quot;postgres&quot;)  
    .AddDatabase(&quot;AspirationalAbp&quot;);  
  
var redis = builder.AddRedis(&quot;redis&quot;);  
  
// DbMigrator  
if (builder.Environment.IsDevelopment())  
{  
    builder  
        .AddProject&lt;Projects.AspirationalAbp_DbMigrator&gt;(&quot;dbMigrator&quot;)  
        .WithReference(postgres, &quot;Default&quot;)  
        .WithReference(redis, &quot;Redis&quot;)   
        .WithReplicas(1);
}  
  
// AuthServer  
var authServerLaunchProfile = &quot;AspirationalAbp.AuthServer&quot;;  
builder  
    .AddProject&lt;Projects.AspirationalAbp_AuthServer&gt;(&quot;authserver&quot;, launchProfileName: authServerLaunchProfile)  
    .WithExternalHttpEndpoints()  
    .WithReference(postgres, &quot;Default&quot;)  
    .WithReference(redis);  
  
// HttpApi.Host  
var httpApiHostLaunchProfile = &quot;AspirationalAbp.HttpApi.Host&quot;;  
builder  
    .AddProject&lt;Projects.AspirationalAbp_HttpApi_Host&gt;(&quot;httpapihost&quot;, launchProfileName: httpApiHostLaunchProfile)  
    .WithExternalHttpEndpoints()
    .WithReference(postgres, &quot;Default&quot;)  
    .WithReference(redis);  
  
// Web  
builder  
    .AddProject&lt;Projects.AspirationalAbp_Web&gt;(&quot;web&quot;, &quot;AspirationalAbp.Web&quot;)  
    .WithReference(redis);  
  
builder.Build().Run();
</code></pre>
<p>With the code above, the following operations were performed below:</p>
<ol>
<li>Creates an <code>IDistributedApplicationBuilder</code> instance by calling <code>DistributedApplication.CreateBuilder(args)</code>.</li>
<li>Adds PostgreSQL and the <code>AspirationalAbp</code> database.</li>
<li>Adds Redis.</li>
<li>Adds the <code>DbMigrator</code> project with references to PostgreSQL and Redis, ensuring one replica in development.</li>
<li>Adds the <code>AuthServer</code> project with external HTTP endpoints, referencing PostgreSQL and Redis.</li>
<li>Adds the <code>HttpApi.Host</code> project with external HTTP endpoints, referencing PostgreSQL and Redis.</li>
<li>Adds the <code>Web</code> project, referencing Redis.</li>
<li>Builds and runs the application.</li>
</ol>
<p>Now let's make the projects we added to the app host compatible with Aspire.</p>
<h2>Configuring Projects for Aspire</h2>
<p>To make the <code>AspirationalAbp.DbMigrator</code>, <code>AspirationalAbp.AuthServer</code>, <code>AspirationalAbp.HttpApi.Host</code>, and <code>AspirationalAbp.Web</code> projects compatible with Aspire, we need to add and configure several packages. For that, we need to add the <code>Aspire.StackExchange.Redis</code> package to all these projects and the <code>Aspire.Npgsql.EntityFrameworkCore.PostgreSQL</code> package to the <code>AspirationalAbp.EntityFrameworkCore</code> project. Additionally, we will add the  <code>AspirationalAbp.ServiceDefaults</code> reference to host projects except <code>AspirationalAbp.DbMigrator</code>. Also, we need to convert <a href="https://serilog.net/">Serilog</a> events into <a href="https://opentelemetry.io/">OpenTelemetry</a> <code>LogRecord</code>s, for that we will add a  <code>Serilog.Sinks.OpenTelemetry</code> reference to host projects. Let's begin with configuring <code>AspirationalAbp.DbMigrator</code>.</p>
<h3>AspirationalAbp.DbMigrator</h3>
<p>First, let's add the <code>Aspire.StackExchange.Redis</code>, and <code>Serilog.Sinks.OpenTelemetry</code> packages to the <code>AspirationalAbp.DbMigrator</code> project. For this, let's run the following .NET CLI command inside the <code>AspirationalAbp.DbMigrator</code> project:</p>
<pre><code class="language-bash">dotnet add package Aspire.StackExchange.Redis --version 8.0.1
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313
</code></pre>
<p>Then let's override the <code>PreConfigureServices</code> method in <code>AspirationalAbpDbMigratorModule</code> as below:</p>
<pre><code class="language-csharp">public override void PreConfigureServices(ServiceConfigurationContext context)  
{  
    var configuration = context.Services.GetConfiguration();  
    configuration[&quot;Redis:Configuration&quot;] = configuration[&quot;ConnectionStrings:Redis&quot;];  
}
</code></pre>
<p>To use the <strong>OpenTelemetry</strong> sink we have installed the <code>Serilog.Sinks.OpenTelemetry</code> package and now let's enable the sink. For this, let's write the following code block just before calling the <code>CreateLogger</code> method in the logger configuration in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">/// .WriteTo.Async(c =&gt; c.Console())  
.WriteTo.Async(c =&gt; c.OpenTelemetry())  
/// .CreateLogger();
</code></pre>
<p>Now let's continue with <code>AspirationalAbp.EntityFrameworkCore</code>.</p>
<h3>AspirationalAbp.EntityFrameworkCore</h3>
<p>Now let's add the <code>Aspire.Npgsql.EntityFrameworkCore.PostgreSQL</code> package to the <code>AspirationalAbp.EntityFrameworkCore</code> project. For this, you can run the following command in the <code>AspirationalAbp.EntityFrameworkCore</code> project:</p>
<pre><code class="language-bash">dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.1
</code></pre>
<p>Now let's continue with <code>AspirationalAbp.AuthServer</code>.</p>
<h3>AspirationalAbp.AuthServer</h3>
<p>First, let's add the <code>Serilog.Sinks.OpenTelemetry</code>, <code>Aspire.StackExchange.Redis</code> and <code>AspirationalAbp.ServiceDefaults</code> packages to the <code>AspirationalAbp.AuthServer</code> project. For this, let's run the following .NET CLI command inside the <code>AspirationalAbp.AuthServer</code> project:</p>
<pre><code class="language-bash">dotnet add package Aspire.StackExchange.Redis --version 8.0.1
dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313
</code></pre>
<p>Then add the following code block after defining the builder variable in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.AddServiceDefaults();  
builder.AddRedisClient(&quot;redis&quot;);  
builder.AddNpgsqlDbContext&lt;AspirationalAbpDbContext&gt;(&quot;Default&quot;,  
    options =&gt;  
    {  
        options.DisableRetry = true;  
    });
</code></pre>
<p>Then add the following code to the <code>PreConfigureServices</code> method in the <code>AspirationalAbpAuthServerModule</code> class:</p>
<pre><code class="language-csharp">configuration[&quot;Redis:Configuration&quot;] = configuration[&quot;ConnectionStrings:Redis&quot;];
</code></pre>
<p>To use the <strong>OpenTelemetry</strong> sink we have installed the <code>Serilog.Sinks.OpenTelemetry</code> package and now let's enable the sink. For this, let's write the following code block just before calling the <code>CreateLogger</code> method in logger configuration in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">/// .WriteTo.Async(c =&gt; c.Console())  
.WriteTo.Async(c =&gt; c.OpenTelemetry())  
/// .CreateLogger();
</code></pre>
<p>So far we have made <code>AspirationalAbp.DbMigrator</code>, <code>AspirationalAbp.EntityFrameworkCore</code>, and <code>AspirationalAbp.AuthServer</code> compatible with Aspire. Now let's continue with <code>AspirationalAbp.HttpApi.Host</code>.</p>
<h3>AspirationalAbp.HttpApi.Host</h3>
<p>First, let's add the <code>Serilog.Sinks.OpenTelemetry</code>, <code>Aspire.StackExchange.Redis</code> and <code>AspirationalAbp.ServiceDefaults</code> packages to the <code>AspirationalAbp.HttpApi.Host</code> project. For this, let's run the following .NET CLI command inside the <code>AspirationalAbp.HttpApi.Host</code> project:</p>
<pre><code class="language-bash">dotnet add package Aspire.StackExchange.Redis --version 8.0.1
dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313
</code></pre>
<p>Then add the following code block after defining the builder variable in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.AddServiceDefaults();  
builder.AddRedisClient(&quot;redis&quot;);  
builder.AddNpgsqlDbContext&lt;AspirationalAbpDbContext&gt;(&quot;Default&quot;,  
    options =&gt;  
    {  
        options.DisableRetry = true;  
    });
</code></pre>
<p>Then let's override the <code>PreConfigureServices</code> method in <code>AspirationalAbpHttpApiHostModule</code> as below:</p>
<pre><code class="language-csharp">public override void PreConfigureServices(ServiceConfigurationContext context)  
{  
    var configuration = context.Services.GetConfiguration();  
    configuration[&quot;Redis:Configuration&quot;] = configuration[&quot;ConnectionStrings:Redis&quot;];  
}
</code></pre>
<p>To use the <strong>OpenTelemetry</strong> sink we have installed the <code>Serilog.Sinks.OpenTelemetry</code> package and now let's enable the sink. For this, let's write the following code block just before calling the <code>CreateLogger</code> method in the logger configuration in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">/// .WriteTo.Async(c =&gt; c.Console())  
.WriteTo.Async(c =&gt; c.OpenTelemetry())  
/// .CreateLogger();
</code></pre>
<p>Finally, let's make <code>AspirationalAbp.Web</code> compatible with Aspire.</p>
<h3>AspirationalAbp.Web</h3>
<p>First, let's add the <code>Serilog.Sinks.OpenTelemetry</code>, <code>Aspire.StackExchange.Redis</code> and <code>AspirationalAbp.ServiceDefaults</code> packages to the <code>AspirationalAbp.Web</code> project. For this, let's run the following .NET CLI command inside the <code>AspirationalAbp.Web</code> project:</p>
<pre><code class="language-bash">dotnet add package Aspire.StackExchange.Redis --version 8.0.1
dotnet add reference ../AspirationalAbp.ServiceDefaults/AspirationalAbp.ServiceDefaults.csproj
dotnet add package Serilog.Sinks.OpenTelemetry --version 4.0.0-dev-00313
</code></pre>
<p>Then add the following code block after defining the builder variable in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">builder.AddServiceDefaults();  
builder.AddRedisClient(&quot;redis&quot;);
</code></pre>
<p>Then add the following code to the <code>PreConfigureServices</code> method in the <code>AspirationalAbpWebModule</code> class:</p>
<pre><code class="language-bash">var configuration = context.Services.GetConfiguration();  
configuration[&quot;Redis:Configuration&quot;] = configuration[&quot;ConnectionStrings:Redis&quot;];
</code></pre>
<p>To use the <strong>OpenTelemetry</strong> sink we have installed the <code>Serilog.Sinks.OpenTelemetry</code> package and now let's enable the sink. For this, let's write the following code block just before calling the <code>CreateLogger</code> method in logger configuration in <code>Program.cs</code>:</p>
<pre><code class="language-csharp">/// .WriteTo.Async(c =&gt; c.Console())  
.WriteTo.Async(c =&gt; c.OpenTelemetry())  
/// .CreateLogger();
</code></pre>
<p>After making all our changes, we can run the <code>AspirationalAbp.AppHost</code> project.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-dashboard.png" alt="aspire-dashboard" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-structured-logs.png" alt="aspire-structured-logs" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-traces.png" alt="aspire-traces" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2024-06-27-how-to-use-Aspire-with-ABP-framework/aspire-metrics.png" alt="aspire-metrics" /></p>
<h2>Conclusion</h2>
<p>Combining Aspire with the ABP framework creates a powerful setup for building robust, observable, and feature-rich applications. By integrating Aspire's observability and cloud capabilities with ABP's approach of focusing on your business without repeating yourself, you can develop feature-rich, scalable applications with enhanced monitoring and seamless cloud integration. This guide provides a clear path to set up and configure these technologies, ensuring your applications are well-structured, maintainable, and ready for modern cloud environments.</p>
<h2>See Also</h2>
<ul>
<li><a href="https://abp.io/community/articles/.net-aspire-vs-abp-studio-side-by-side-t1c73d1l">Aspire vs ABP Studio: Side by Side</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/every-programmer-should-know-2-optimistic-concurrency-control-sms9xs9n</guid>
      <link>https://abp.io/community/posts/every-programmer-should-know-2-optimistic-concurrency-control-sms9xs9n</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>concurrency</category>
      <title>Every Programmer Should Know #2: Optimistic Concurrency Control</title>
      <description>In the world of programming, there are many concepts that every developer should understand in order to build efficient and consistent systems. Among these, Optimistic Concurrency (also known as Optimistic Locking) stands out as a key mechanism for ensuring data consistency in the middle of concurrent changes.</description>
      <pubDate>Tue, 13 Feb 2024 20:14:04 Z</pubDate>
      <a10:updated>2026-04-13T18:49:58Z</a10:updated>
      <content:encoded><![CDATA[In the world of programming, there are many concepts that every developer should understand in order to build efficient and consistent systems. Among these, Optimistic Concurrency (also known as Optimistic Locking) stands out as a key mechanism for ensuring data consistency in the middle of concurrent changes.<br \><a href="https://www.berkansasmaz.com/every-programmer-should-know-optimistic-concurrency/" rel="nofollow noopener noreferrer" title="Go to the Post">Go to the Post</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a10b647-c65c-c18e-8513-1748ce964a47" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a10b647-c65c-c18e-8513-1748ce964a47" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/asp.net-8-whats-new-about-authentication-and-authorization-r24nro0j</guid>
      <link>https://abp.io/community/posts/asp.net-8-whats-new-about-authentication-and-authorization-r24nro0j</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>net8</category>
      <category>aspnet-core</category>
      <title>ASP.NET 8: What's New About Authentication and Authorization</title>
      <description>In ASP.NET 8, the concept of authentication and authorization is undergoing a transformation. Specifically, ASP.NET Core Identity is transitioning from a focus on traditional web pages to a more API-driven approach. We will see what the Identity API endpoints are, why we need them, and how to use them in detail. So, let's dive in.</description>
      <pubDate>Tue, 07 Nov 2023 07:08:37 Z</pubDate>
      <a10:updated>2026-09-26T00:26:17Z</a10:updated>
      <content:encoded><![CDATA[<h1>ASP.NET 8: What's New About Authentication and Authorization</h1>
<p>In ASP.NET 8, the concept of authentication and authorization is undergoing a transformation. Specifically, ASP.NET Core Identity is transitioning from a focus on traditional web pages to a more API-driven approach. We will see what the Identity API endpoints are, why we need them, and how to use them in detail. So, let's dive in.</p>
<h2>What is ASP.NET Core Identity?</h2>
<p>ASP.NET Core Identity is like an extra toolkit that comes with ASP.NET Core. It offers a bunch of services to help you deal with user accounts in your ASP.NET Core application. These services include both basic concepts and ready-made solutions for each task.</p>
<p>With ASP.NET Core Identity, you can save user accounts in your app, handle user information, add extra security with two-factor authentication, and even connect other log in options, like social media logins, to user accounts. In simple terms, it's all about keeping user accounts in your app and making it easy for users to log in.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png" alt="" /></p>
<p>Prior to ASP.NET 8, there were limitations on using ASP.NET Identity in Single Page Applications (SPAs). Because the ASP.NET Core Identity framework is primarily tailored for traditional server-rendered web applications, like ASP.NET Core MVC or Razor Pages apps. However, the Identity API endpoints that come with ASP.NET 8 aim to solve token-based authentication and authorization in SPA without external dependencies.</p>
<h2>Why do we need Identity APIs?</h2>
<p>The introduction of Identity endpoints in ASP.NET 8 aims to streamline the process of integrating ASP.NET Core Identity into both API server applications and front-end SPAs like those built with JavaScript or Blazor. To understand the importance of these endpoints, let's first examine the disadvantages of using ASP.NET Core Identity before ASP.NET 8.</p>
<p>Let's delve into a common scenario:</p>
<p>Imagine you have a straightforward application composed of an ASP.NET Core backend that exposes APIs. On the client side, you have a SPA application that communicates with these APIs. Now, you want to incorporate user accounts, complete with authentication and authorization, into your application.</p>
<p>Before the advent of the identity endpoints, you could add ASP.NET Core Identity and the default Razor Pages UI to your app by adding a few packages, updating your database schema, and registering some services. However, this approach had some disadvantages. From a user experience perspective, the full-page refreshes of Razor Pages can be a major drawback especially compared to the fluidity of SPAs. On the other hand, from a developer's perspective, if you want the default pages to be compatible with the rest of your application, you may need to update more than 30 pages. Moreover, the default Razor Pages UI uses traditional cookie-based authentication, not bearer tokens.</p>
<p>In ASP.NET 8, identity endpoints were introduced to simplify the process of adding user accounts to ASP.NET Core API apps used with SPAs or mobile. These endpoints streamline the user management process by providing an alternative to the traditional Razor Page Identity UI pages. Additionally, a new endpoint is included for retrieving bearer tokens, which can be used for authentication. These changes directly address the concerns mentioned earlier, making it easier to create a user-friendly and integrated experience within your app, with no full-page refreshes or styling conflicts.</p>
<h2>How to add Identity APIs?</h2>
<p>In this part, I'll create a demo application, add all of the required packages, and lastly map the Identity APIs.</p>
<p>Create a new ASP.NET Core app with the following command and change the directory to the created application folder:</p>
<pre><code class="language-bash">dotnet new webapi --name NewIdentityEndpoints -o IdentityDemo
cd IdentityDemo
</code></pre>
<p>After that, open the application in your favorite IDE and add the required packages:</p>
<pre><code class="language-csharp">&lt;PackageReference Include=&quot;Microsoft.AspNetCore.Identity.EntityFrameworkCore&quot; Version=&quot;8.0.0-rc.2.23480.2&quot; /&gt;  
&lt;PackageReference Include=&quot;Microsoft.EntityFrameworkCore.InMemory&quot; Version=&quot;8.0.0-rc.2.23480.1&quot; /&gt;
</code></pre>
<p><strong>Note:</strong> I used the <code>.NET 8.0.100-rc.2-**</code> SDK for everything in this post.</p>
<p>Then replace <code>Program.cs</code> as below:</p>
<pre><code class="language-csharp">using System.Security.Claims;  
using Microsoft.AspNetCore.Identity;  
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;  
using Microsoft.EntityFrameworkCore;  
  
var builder = WebApplication.CreateBuilder(args);  
  
builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme);  
builder.Services.AddAuthorizationBuilder();  
  
builder.Services.AddDbContext&lt;ApplicationDbContext&gt;(options =&gt; options.UseInMemoryDatabase(&quot;AppDb&quot;));  
  
builder.Services.AddIdentityCore&lt;MyCustomUser&gt;()  
.AddEntityFrameworkStores&lt;ApplicationDbContext&gt;()  
.AddApiEndpoints();  
  
builder.Services.AddEndpointsApiExplorer();  
builder.Services.AddSwaggerGen();  
  
var app = builder.Build();  
  
app.MapGroup(&quot;/my-identity-api&quot;).MapIdentityApi&lt;IdentityUser&gt;();  
  
app.MapGet(&quot;/&quot;, (ClaimsPrincipal user) =&gt; $&quot;Hello {user.Identity!.Name}&quot;).RequireAuthorization();  
  
if (app.Environment.IsDevelopment())  
{  
  app.UseSwagger();  
  app.UseSwaggerUI();  
}  
  
app.Run();  
  
class MyCustomUser : IdentityUser { }  
  
class ApplicationDbContext : IdentityDbContext&lt;MyCustomUser&gt;  
{  
  public ApplicationDbContext(DbContextOptions&lt;ApplicationDbContext&gt; options) : base(options) { }  
}

</code></pre>
<p>When you run the application, you will see a result as the following:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png" alt="" /></p>
<h2>Custom Authorization Policies</h2>
<p>Prior to ASP.NET 8, adding a parameterized authorization policy to an endpoint required writing a lot of code.</p>
<ul>
<li>Implementing an <code>AuthorizeAttribute</code> for each policy</li>
<li>Implementing an <code>AuthorizationPolicyProvider</code> to process a custom policy from a string-based contract</li>
<li>Implementing an <code>AuthorizationRequirement</code> for the policy</li>
<li>Implementing an <code>AuthorizationHandler</code> for each requirement</li>
</ul>
<p>However, implementing the <code>IAuthorizationRequirementData</code> interface that comes with ASP.NET 8, your application code should look like this:</p>
<pre><code class="language-csharp">class MinimumAgeAuthorizeAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData
{
  public MinimumAgeAuthorizeAttribute(int age) =&gt; Age =age;
  public int Age { get; }

  public IEnumerable&lt;IAuthorizationRequirement&gt; GetRequirements()
  {
    yield return this;
  }
}

class MinimumAgeAuthorizationHandler : AuthorizationHandler&lt;MinimumAgeAuthorizeAttribute&gt;
{
  protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeAuthorizeAttribute requirement) 
  {
    ...
  }
}
</code></pre>
<p>See <a href="https://gist.github.com/captainsafia/7c54e92d12df695ff0908e989fb8531f">here</a> for the complete code.</p>
<h2>Conclusion</h2>
<p>In this article, I've shown you the Identity APIs introduced with ASP.NET 8, why we need them, and how we can add them to our application, and finally I explained how to define a custom authorization policy with fewer lines of code.</p>
<h2>References</h2>
<ul>
<li>https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#authentication-and-authorization</li>
<li>https://auth0.com/blog/whats-new-dotnet8-authentication-authorization/</li>
<li>https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/</li>
<li>https://andrewlock.net/should-you-use-the-dotnet-8-identity-api-endpoints/</li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a0ebac9-7384-2ae3-5f97-6f75ba265040" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a0ebac9-7384-2ae3-5f97-6f75ba265040" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-export-data-to-excel-files-with-asp.net-core-minimal-api-79o45u3s</guid>
      <link>https://abp.io/community/posts/how-to-export-data-to-excel-files-with-asp.net-core-minimal-api-79o45u3s</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>minimal-api</category>
      <category>excel</category>
      <category>aspnet</category>
      <title>How to Export Data to Excel Files with ASP.NET Core Minimal API</title>
      <description>As a developer, you may need to work with file-based data sources, and exporting data to Excel files is a common requirement in many applications. In this article, we'll explore how to export data to Excel files in a ASP.NET Core application using the MiniExcel library. </description>
      <pubDate>Mon, 10 Apr 2023 15:43:43 Z</pubDate>
      <a10:updated>2026-04-21T05:42:18Z</a10:updated>
      <content:encoded><![CDATA[As a developer, you may need to work with file-based data sources, and exporting data to Excel files is a common requirement in many applications. In this article, we'll explore how to export data to Excel files in a ASP.NET Core application using the MiniExcel library. <br \><a href="https://www.berkansasmaz.com/how-to-export-data-to-excel-files-in-net/" rel="nofollow noopener noreferrer" title="Go to the Post">Go to the Post</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/rate-limiting-with-asp.net-core-7.0-co55vem3</guid>
      <link>https://abp.io/community/posts/rate-limiting-with-asp.net-core-7.0-co55vem3</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>dotnet7</category>
      <category>dotnet</category>
      <title>Rate Limiting with ASP.NET Core 7.0</title>
      <description>In this article, we will look at what rate limiting is, why we need to use it, how the different rate limiting algorithms provided with .NET 7.0 work, and the best practices for using rate limiting in your application.</description>
      <pubDate>Tue, 29 Nov 2022 09:15:09 Z</pubDate>
      <a10:updated>2026-09-26T01:17:11Z</a10:updated>
      <content:encoded><![CDATA[<h1>Rate Limiting with ASP.NET Core 7.0</h1>
<p>Rate limiting is a way of controlling the traffic that a web application or API receives. In other words, rate limiting helps you control the amount of traffic each user has access to at any given time. This is extremely useful when you want to manage the load on your server or services, avoid going over your monthly data transfer limit and allow the system to continue to function and meet service level agreements, even when an increase in demand places an extreme load on resources.</p>
<p>In this article, we will look at what rate limiting is, why we need to use it, how the different rate limiting algorithms provided with .NET 7.0 work, and best practices for using rate limiting in your application.</p>
<h2>What is rate limiting?</h2>
<p>Whether accidental or intentional, users may exhaust resources in a way that impacts others. When a number of requests are received on to resources for a long time, the server can run out of those resources. These resources can include memory, threads, connections, or anything else that is limited. To avoid this situation, set rate limiters. Rate limiters control the consumption of resources used by an instance of an application, a user, an individual tenant, or an entire service.</p>
<h2>Why do you need to use rate limiting?</h2>
<p>A rate limiting system is crucial in any application where you have to control or throttle user requests or traffic. This is especially true in applications running on a cloud hosting platform because the user’s traffic can affect the whole server where the application is hosted.</p>
<p>Why do you need to implement rate limiting? Here are a few reasons:</p>
<ul>
<li>To ensure that a system continues to meet service level agreements (SLA).</li>
<li>To prevent a single user, tenant, service, or so on from monopolizing the resources provided by an application.</li>
<li>To help cost-optimize a system by limiting the maximum resource levels needed to keep it functioning.</li>
</ul>
<h2>Rate limiter algorithms</h2>
<p>The <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptionsextensions"><code>RateLimiterOptionsExtensions</code></a> class provides the following extension methods for rate limiting:</p>
<ul>
<li><strong><a href="https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#fixed-window-limit">Fixed window</a></strong>: Fixed-window limits—such as 3,000 requests per hour or 10 requests per day—are easy to state, but they are subject to spikes at the edges of the window, as available quota resets. Consider, for example, a limit of 3,000 requests per hour, which still allows for a spike of all 3,000 requests to be made in the first minute of the hour, which might overwhelm the service.</li>
<li><a href="https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#sliding-window-limit"><strong>Sliding window</strong>:</a> Sliding windows have the benefits of a fixed window, but the rolling window of time smoothes out bursts. Systems such as Redis facilitate this technique with expiring keys.</li>
<li><a href="https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#token-bucket-limit"><strong>Token bucket</strong></a>: A token bucket maintains a rolling and accumulating budget of usage as a balance of tokens. A token bucket adds tokens at some rate. When a service request is made, the service attempts to withdraw a token (decrementing the token count) to fulfill the request. If there are no tokens in the bucket, the service has reached its limit and responds with backpressure.</li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&amp;view=aspnetcore-7.0#concurrency-limiter"><strong>Concurrency</strong></a>: A concurrency limiter is the simplest form of rate limiting. It doesn’t look at time, just at number of concurrent requests.</li>
</ul>
<p>In order to be a more realistic example, instead of making an example with each rate limiter algorithm, we will implement the following three algorithms in an <strong>ABP-based</strong> application.</p>
<ol>
<li>We will add a <code>SlidingWindowLimiter</code> with a partition for all anonymous users.</li>
<li>We will add a <code>TokenBucketRateLimiter</code> with a partition for each authenticated user.</li>
<li>We will add a <code>ConcurrencyLimiter</code> with a partition for each Tenant.</li>
</ol>
<p><strong>Note:</strong> The following sample isn't meant for production code but is an example of how to use the limiters in ABP-based applications.</p>
<h3>Limiter with <code>OnRejected</code>, <code>RetryAfter</code>, and <code>GlobalLimiter</code></h3>
<h4>Add rate limiter</h4>
<p>Let's create the following method in the <code>MyProjectNameWebModule.cs</code> class in the <code>MyProjectName.Web</code> project.</p>
<p><strong>Note:</strong> If the <code>**.Web</code> project is not in your application, you can do the same in the project where your application is hosted.</p>
<pre><code class="language-csharp">private void ConfigureRateLimiters(ServiceConfigurationContext context)
{
    context.Services.AddRateLimiter(limiterOptions =&gt;
    {
        limiterOptions.OnRejected = (context, cancellationToken) =&gt;
        {
            if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
            {
                context.HttpContext.Response.Headers.RetryAfter =
                    ((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
            }

            context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
            context.HttpContext.RequestServices.GetService&lt;ILoggerFactory&gt;()?
                .CreateLogger(&quot;Microsoft.AspNetCore.RateLimitingMiddleware&quot;)
                .LogWarning(&quot;OnRejected: {RequestPath}&quot;, context.HttpContext.Request.Path);

            return new ValueTask();
        };

        limiterOptions.AddPolicy(&quot;UserBasedRateLimiting&quot;, context =&gt;
        {
            var currentUser = context.RequestServices.GetService&lt;ICurrentUser&gt;();
            
            if (currentUser is not null &amp;&amp; currentUser.IsAuthenticated)
            {
                return RateLimitPartition.GetTokenBucketLimiter(currentUser.UserName, _ =&gt; new TokenBucketRateLimiterOptions
                {
                    TokenLimit = 10,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 3,
                    ReplenishmentPeriod = TimeSpan.FromMinutes(1),
                    TokensPerPeriod = 4,
                    AutoReplenishment = true
                });
            }

            return RateLimitPartition.GetSlidingWindowLimiter(&quot;anonymous-user&quot;,
                _ =&gt; new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = 2,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 1,
                    Window = TimeSpan.FromMinutes(1),
                    SegmentsPerWindow = 2
                });
        });

        limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create&lt;HttpContext, string&gt;(context =&gt;
        {
            var currentTenant = context.RequestServices.GetService&lt;ICurrentTenant&gt;();
            
            if (currentTenant is not null &amp;&amp; currentTenant.IsAvailable)
            {
                return RateLimitPartition.GetConcurrencyLimiter(currentTenant!.Name, _ =&gt; new ConcurrencyLimiterOptions
                {
                    PermitLimit = 5,
                    QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                    QueueLimit = 1
                });
            }

            return RateLimitPartition.GetNoLimiter(&quot;host&quot;);
        });
    });
}
</code></pre>
<p>In the above example, the <code>TokenBucketLimiter</code> is used for each authenticated user, while the <code>SlidingWindowLimiter</code> is used for all anonymous users. Additionally, as a global limiter, the <code>ConcurrencyLimiter</code> is used for each tenant, while rate limiting is disabled for the host(tenant is not available). Also, for requests that are rejected when the limit is reached, sets the response status code to <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429">429 Too Many Requests</a> and the response mentions when to retry (if available from the rate-limiting metadata).</p>
<p>Let's call the <code>ConfigureRateLimiters</code> method that we created in the <code>ConfigureServices</code> method.</p>
<p>The final version of the <code>ConfigureServices</code> method:</p>
<pre><code class="language-csharp">public override void ConfigureServices(ServiceConfigurationContext context)
{
    var hostingEnvironment = context.Services.GetHostingEnvironment();
    var configuration = context.Services.GetConfiguration();

    ConfigureBundles();
    ConfigureUrls(configuration);
    ConfigurePages(configuration);
    ConfigureAuthentication(context);
    ConfigureImpersonation(context, configuration);
    ConfigureAutoMapper();
    ConfigureVirtualFileSystem(hostingEnvironment);
    ConfigureNavigationServices();
    ConfigureAutoApiControllers();
    ConfigureSwaggerServices(context.Services);
    ConfigureExternalProviders(context);
    ConfigureHealthChecks(context);
    ConfigureCookieConsent(context);
    ConfigureTheme();

    Configure&lt;PermissionManagementOptions&gt;(options =&gt;
    {
        options.IsDynamicPermissionStoreEnabled = true;
    });
    
    ConfigureRateLimiters(context); // added
}
</code></pre>
<h4>Add RateLimiter middleware</h4>
<p>Add the following line just before the <code>app.UseConfiguredEndpoints(...)</code> line to add the <code>RateLimiter</code> middleware to your ASP.NET Core request pipeline:</p>
<pre><code class="language-csharp">app.UseRateLimiter();
</code></pre>
<h4>Use rate limiter for all controllers</h4>
<p>Let's edit the <code>ConfiguredEndpoints</code> middleware as follows:</p>
<pre><code class="language-csharp">app.UseConfiguredEndpoints(endpoints =&gt;
{
    endpoints.MapRazorPages()
        .DisableRateLimiting();

    endpoints.MapControllers()
        .RequireRateLimiting(&quot;UserBasedRateLimiting&quot;);
});
</code></pre>
<ul>
<li><strong>DisableRateLimiting:</strong> It is used to disable the <code>ConcurrencyLimiter</code> for razor pages, which we set globally when the tenant is available.</li>
<li><strong>RequireRateLimiting:</strong> We have enabled the rate limiter, which we define according to whether the user is authenticated or not, for all controllers.</li>
</ul>
<h2><code>EnableRateLimiting</code> and <code>DisableRateLimiting</code> attributes</h2>
<p>It's kind of unrealistic to always use rate limiting for all controllers or pages. Sometimes, we may want to throttle a particular endpoint or page. In such cases, we can use the <code>EnableRateLimiting</code> and <code>DisableRateLimiting</code> attributes. The <code>EnableRateLimiting</code> and <code>DisableRateLimiting</code> attributes can be applied to a controller, action method, or razor rage. Check <a href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&amp;view=aspnetcore-7.0#enableratelimiting-and-disableratelimiting-attributes">here</a> for more.</p>
<h2>Rate limit an HTTP handler</h2>
<p>Rate limiting when sending an HTTP request can be a good practice, especially in service-to-service communication. Because, resources are consumed by apps that rely on them, and when an app makes too many requests for a single resource, it can lead to <em>resource contention</em>. Resource contention occurs when a resource is consumed by too many clients, and the resource is unable to serve all of the apps that are requesting it. This can result in a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. Since there are similar codes, I will not mention an example in this article, but to avoid such situations, you can write your own HTTP handler as <a href="https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter#implement-a-delegatinghandler-subclass">here</a>.</p>
<h2>How does it work?</h2>
<p><a href="https://www.nuget.org/packages/System.Threading.RateLimiting">System.Threading.RateLimiting</a> provides the primitives for writing rate limiters as well as providing a few commonly used algorithms built-in. The main type is the abstract base class <a href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs">RateLimiter</a>.</p>
<pre><code class="language-csharp">public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
    public abstract int GetAvailablePermits();
    public abstract TimeSpan? IdleDuration { get; }

    public RateLimitLease Acquire(int permitCount = 1);
    public ValueTask&lt;RateLimitLease&gt; WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default);

    public void Dispose();
    public ValueTask DisposeAsync();
}
</code></pre>
<p><code>RateLimiter</code> contains <code>Acquire</code> and <code>WaitAsync</code> as the core methods for trying to gain permits for a resource that is being protected. Depending on the application, the protected resource may need to acquire more than 1 permits, so <code>Acquire</code> and <code>WaitAsync</code> both accept an optional <code>permitCount</code> parameter. <code>Acquire</code> is a synchronous method that will check if enough permits are available or not and return a <code>RateLimitLease</code> which contains information about whether you successfully acquired the permits or not. <code>WaitAsync</code> is similar to <code>Acquire</code> except that it can support queuing permit requests which can be de-queued at some point in the future when the permits become available, which is why it’s asynchronous and accepts an optional <code>CancellationToken</code> to allow canceling the queued request.</p>
<p><code>RateLimitLease</code> has an <code>IsAcquired</code> property which is used to see if the permits were acquired. Additionally, the <code>RateLimitLease</code> may contain metadata such as a suggested retry-after period if the lease failed. Finally, the <code>RateLimitLease</code> is disposable and should be disposed when the code is done using the protected resource. The disposal will let the <code>RateLimiter</code> know to update its limits based on how many permits were acquired.</p>
<h2>Limitations</h2>
<p>In most cases, the rate-limiting middleware provided with ASP.NET 7.0 will meet your requirements. However, if you would want to return statistics about your limits (e.g. <a href="https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limit-http-headers">the way GitHub does</a>), you’ll find out that the ASP.NET rate limiting middleware does not support this. You won’t have access to the “number of requests remaining” or other metadata. Not in <code>OnRejected</code>, and definitely not if you want to return this data as headers on every request.</p>
<h2>Best practices for rate limiting</h2>
<p>In order to use rate limiting properly, you need to have a solid understanding of the types of limiting available, as well as the data rate and data volume of your service. You also need to have a clear idea of how many users you expect to use your service as well as how they will interact with it. The best practices for rate limiting are as follows:</p>
<ul>
<li>Find the right rate limiter algorithm for your endpoint. I mean, the cost of an endpoint should be considered when selecting a limiter. The cost of an endpoint includes the resources used, for example, time, data access, CPU, and I/O.</li>
<li>Set realistic limits. Once you’ve figured out all the above, you need to set realistic limits for each service. Then, before deploying an app using rate limiting to production, stress test the app to validate the rate limiters and options used. For example, create a <a href="https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html">JMeter script</a> with a tool like <a href="https://guide.blazemeter.com/hc/articles/207421695-Writing-your-first-JMeter-script">BlazeMeter</a> or <a href="https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html">Apache JMeter HTTP(S) Test Script Recorder</a> and load the script to <a href="https://learn.microsoft.com/en-us/azure/load-testing/overview-what-is-azure-load-testing">Azure Load Testing</a>.</li>
<li>In response to rate-limiting, intermittent, or non-specific errors, a client should generally retry the request after a delay. It is a best practice for this delay to increase exponentially after each failed request, which is referred to as <em>exponential backoff</em>. When many clients might be making schedule-based requests (such as fetching results every hour), additional random time (<em>jitter</em>) should be applied to the request timing, the backoff period, or both of them to ensure that these multiple client instances don't become periodic <a href="https://www.wikiwand.com/en/Thundering_herd_problem">thundering herd</a>, and cause a form of DDoS themselves.</li>
</ul>
<h2>Conclusion</h2>
<p>In this article, we’ve covered what rate limiting is, why you need to use it and the best practices for doing so. We’ve also looked at how to use three rate-limiting algorithms that are provided with .NET 7.0 on ABP-based applications and how rate-limiting works. Now that you’re familiar with the concept of rate limiting, it’s time to start implementing rate limiting in your application. This will allow you to control the traffic and ensure that your application is running smoothly without any issues.</p>
<h2>References</h2>
<ul>
<li>https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&amp;view=aspnetcore-7.0</li>
<li>https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/</li>
<li>https://blog.maartenballiauw.be/post/2022/09/26/aspnet-core-rate-limiting-middleware.html</li>
<li>https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter</li>
<li>https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern</li>
<li>https://learn.microsoft.com/en-us/azure/architecture/patterns/throttling</li>
<li>https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet</li>
<li>https://cloud.google.com/architecture/rate-limiting-strategies-techniques</li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/b517a63c-3ed8-60d0-eff4-3a07d4d807ab" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/b517a63c-3ed8-60d0-eff4-3a07d4d807ab" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-add-custom-properties-to-the-user-entity-rixchoha</guid>
      <link>https://abp.io/community/posts/how-to-add-custom-properties-to-the-user-entity-rixchoha</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>extending-entities</category>
      <title>How to Add Custom Properties to the User Entity</title>
      <description>In this step-by-step article, I will explain how you can customize the user entity class, which is available in every web application you create using the ABP framework, according to your needs.</description>
      <pubDate>Fri, 19 Aug 2022 09:57:32 Z</pubDate>
      <a10:updated>2026-09-25T20:42:03Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Add Custom Properties to the User Entity</h1>
<blockquote>
<p><strong>Note:</strong> If your application is less than version 4.4.x, please follow <a href="https://community.abp.io/posts/how-to-add-custom-property-to-the-user-entity-6ggxiddr">this article</a>.</p>
</blockquote>
<h2>Introduction</h2>
<p>In this step-by-step article, I will explain how you can customize the user entity class, which is available in every web application you create using the ABP framework, according to your needs. When you read this article, you will learn how to override the services of built-in modules, extend the entities, extend data transfer objects and customize the user interface in the applications you develop using the ABP framework.</p>
<blockquote>
<p><strong>Note:</strong> This article is not about customizing the <code>Login</code> page. If you have such a need, please follow <a href="https://community.abp.io/posts/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd">this article</a>.</p>
</blockquote>
<p>You can see the screenshots below which we will reach at the end of the article.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-07-19-How-To-Add-Custom-Property-To-The-User-Entity/custom-identity-user-list.png" alt="custom-identity-user-list" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-07-19-How-To-Add-Custom-Property-To-The-User-Entity/new-user.png" alt="new-user" /></p>
<h2>Preparing the Project</h2>
<h3>Startup template and the initial run</h3>
<p>Abp Framework offers startup templates to get into the work faster. We can create a new startup template using Abp CLI:</p>
<p><code>abp new CustomizeUserDemo</code></p>
<blockquote>
<p>In this article, I will go through the MVC application, but it will work also in the <a href="https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&amp;DB=EF&amp;Tiered=No">Angular</a>, <a href="https://docs.abp.io/en/abp/latest/Getting-Started?UI=BlazorServer&amp;DB=EF&amp;Tiered=No">Blazor Server</a>, and <a href="https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor&amp;DB=EF&amp;Tiered=No">Blazor WebAssembly</a> application.</p>
</blockquote>
<p>After the download is finished, we can run <strong>CustomizeUserDemo.DbMigrator</strong> project to create the database migrations and seed the initial data (admin user, role, etc). Then we can run <code>CustomizeUserDemo.Web</code> to see that our application is working.</p>
<blockquote>
<p>Default admin username is <strong>admin</strong> and password is <strong>1q2w3E*</strong></p>
</blockquote>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-07-19-How-To-Add-Custom-Property-To-The-User-Entity/initial-project.png" alt="initial-project" /></p>
<p>In this article, we will go through a scenario together and find the solutions to our questions through this scenario. However, since the scenario is not a real-life scenario, it may be strange, please don't get too about this issue :)</p>
<h2>Step-1</h2>
<p>Create the Users folder in the <strong>CustomizeUserDemo.Domain.Shared</strong> project, create the class <code>UserConsts</code> inside the folder and update the class you created as below:</p>
<pre><code class="language-csharp">public static class UserConsts
{
    public const string TitlePropertyName = &quot;Title&quot;;

    public const string ReputationPropertyName = &quot;Reputation&quot;;

    public const int MaxTitleLength = 64;

    public const double MaxReputationValue = 1_000;

    public const double MinReputationValue = 1;
}
</code></pre>
<h2>Step-2</h2>
<p>Update the <code>CustomizeUserDemoEfCoreEntityExtensionMappings</code> class in the <strong>CustomizeUserDemo.EntityFramework</strong> project in the EntityFrameworkCore folder as below:</p>
<pre><code class="language-csharp">public static class CustomizeUserDemoEfCoreEntityExtensionMappings
{
    private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();

    public static void Configure()
    {
        CustomizeUserDemoGlobalFeatureConfigurator.Configure();
        CustomizeUserDemoModuleExtensionConfigurator.Configure();

        OneTimeRunner.Run(() =&gt;
        {
            ObjectExtensionManager.Instance
                .MapEfCoreProperty&lt;IdentityUser, string&gt;(
                    UserConsts.TitlePropertyName,
                    (_, propertyBuilder) =&gt;
                    {
                        propertyBuilder.HasDefaultValue(&quot;&quot;);
                        propertyBuilder.HasMaxLength(UserConsts.MaxTitleLength);
                    }
                ).MapEfCoreProperty&lt;IdentityUser, int&gt;(
                    UserConsts.ReputationPropertyName,
                    (_, propertyBuilder) =&gt;
                    {
                        propertyBuilder.HasDefaultValue(UserConsts.MinReputationValue);
                    }
                );
        });
    }
}
</code></pre>
<p>This class can be used to map these extra properties to table fields in the database. Please read <a href="https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities">this</a> article to improve your understanding of what we are doing.</p>
<p>So far, we have added our extra features to the <code>User</code> entity and matched these features with the <code>ef core</code>.</p>
<p>Now we need to add migration to see what has changed in our database. This for, open the Package Manager Console (PMC) under the menu Tools &gt; NuGet Package Manager.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-07-19-How-To-Add-Custom-Property-To-The-User-Entity/nuget-package-manager.png" alt="nuget-package-manager" /></p>
<p>Select the <strong>CustomizeUserDemo.EntityFramework</strong> as the <strong>default project</strong> and execute the following command:</p>
<pre><code class="language-bash">Add-Migration &quot;Updated-User-Entity&quot;
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-07-19-How-To-Add-Custom-Property-To-The-User-Entity/added-new-migration.png" alt="added-new-migration" /></p>
<p>This will create a new migration class inside the <code>Migrations</code> folder of the <strong>CustomizeUserDemo.EntityFrameworkCore</strong> project.</p>
<blockquote>
<p>If you are using another IDE than the Visual Studio, you can use <code>dotnet-ef</code> tool as <a href="https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration">documented here</a>.</p>
</blockquote>
<p>Finally, run the <strong>CustomizeUserDemo.DbMigrator</strong> project to update the database.</p>
<p>When we updated the database, you can see that the <code>Title</code> and <code>Reputation</code> columns are added to the <code>Users</code> table.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-07-19-How-To-Add-Custom-Property-To-The-User-Entity/user-table.png" alt="user-table" /></p>
<h2>Step-3</h2>
<p>Open the <code>CustomizeUserDemoModuleExtensionConfigurator</code> in the <strong>CustomizeUserDemo.Domain.Shared</strong> project, and change the contents of the <code>ConfigureExtraProperties</code> method as shown below:</p>
<pre><code class="language-csharp">private static void ConfigureExtraProperties()
{
    ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity =&gt;
    {
        identity.ConfigureUser(user =&gt;
        {
            user.AddOrUpdateProperty&lt;string&gt;(
                UserConsts.TitlePropertyName,
                options =&gt;
                {
                    options.Attributes.Add(new RequiredAttribute());
                    options.Attributes.Add(
                        new StringLengthAttribute(UserConsts.MaxTitleLength)
                    );
                }
            );
            user.AddOrUpdateProperty&lt;int&gt;(
                UserConsts.ReputationPropertyName,
                options =&gt;
                {
                    options.DefaultValue = UserConsts.MinReputationValue;
                    options.Attributes.Add(
                        new RangeAttribute(UserConsts.MinReputationValue, UserConsts.MaxReputationValue)
                    );
                }
            );
        });
    });
}
</code></pre>
<p>That's it. Now let's run the application and look at the Identity user page. You can also try to edit and recreate a record if you want, it will work even though we haven't done anything extra. Here is the magic code behind ABP framework.</p>
<p>If there is a situation you want to add, you can click the contribute button or make a comment. Also, if you like the article, don't forget to share it :)</p>
<p>Happy coding :)</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/f624716b-e1a6-a2e9-0aeb-3a05c7b62e39" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/f624716b-e1a6-a2e9-0aeb-3a05c7b62e39" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-design-multilingual-entity-8glnk6vu</guid>
      <link>https://abp.io/community/posts/how-to-design-multilingual-entity-8glnk6vu</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>multi-lingual-entity</category>
      <title>How to Design Multi-Lingual Entity</title>
      <description>With a multi-lingual application, you can expand your market share, but if not designed well, may your application will be unusable. So, I've tried to explain how to design a sustainable multi-lingual entity in this article.</description>
      <pubDate>Mon, 15 Aug 2022 08:54:56 Z</pubDate>
      <a10:updated>2026-09-25T22:50:41Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Design Multi-Lingual Entity</h1>
<h2>Introduction</h2>
<p>If you want to open up to the global market these days, end-to-end localization is a must. ABP provides an already established infrastructure for static texts. However, this may not be sufficient for many applications. You may need to fully customize your app for a particular language and region.</p>
<p>Let's take a look at a few quotes from Christian Arno's article &quot;<a href="https://www.mediapost.com/publications/article/155250/how-foreign-language-internet-strategies-boost-sal.html">How Foreign-Language Internet Strategies Boost Sales</a>&quot; to better understand the impact of this:</p>
<ul>
<li><p>82% of European consumers are less likely to buy online if the site is not in their native tongue (<a href="http://europa.eu/rapid/pressReleasesAction.do?reference=IP/11/556">Eurobarometer survey</a>).</p>
</li>
<li><p>72.4% of global consumers are more likely to buy a product if the information is available in their own language (<a href="http://www.commonsenseadvisory.com/">Common Sense Advisory</a>).</p>
</li>
<li><p>The English language currently only accounts for 31% of all online use, and more than half of all searches are in languages other than English.</p>
</li>
<li><p>Today, 42% of all Internet users are in Asia, while almost one-quarter are in Europe and just over 10% are in Latin America.</p>
</li>
<li><p>Foreign languages have experienced exponential growth in online usage in the past decade -- with Chinese now officially the <a href="http://english.peopledaily.com.cn/90001/90776/90882/7438489.html">second-most-prominent-language</a> on the Web. <a href="http://www.internetworldstats.com/stats7.htm">Arabic</a> has increased by a whopping 2500%, while English has only risen by 204%</p>
</li>
</ul>
<p>If you are looking for ways to expand your market share by fully customizing your application for a particular language and region, in this article I will explain how you can do it with ABP framework.</p>
<h3>Source Code</h3>
<p>You can find the source code of the application at <a href="https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreMultiLingual">abpframework/abp-samples</a>.</p>
<h3>Demo of the Final Application</h3>
<p>At the end of this article, we will have created an application same as in the gif below.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/15-08-2022-How-to-Design-Multi-Lingual-Entity/result.gif" alt="data-model" /></p>
<h2>Development</h2>
<p>In order to keep the article short and get rid of unrelated information in the article (like defining entities etc.), we'll be using the <a href="https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore">BookStore</a> example, which is used in the &quot;<a href="https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&amp;DB=EF">Web Application Development Tutorial</a>&quot; documentation of ABP Framework and we will make the Book entity as multi-lingual. If you do not want to finish this tutorial, you can find the application <a href="https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore">here</a>.</p>
<h3>Determining the data model</h3>
<p>We need a robust, maintainable, and efficient data model to store content in multiple languages.</p>
<blockquote>
<p>I read many articles to determine the data model correctly, and as a result, I decided to use one of the many approaches that suit us.
However, as in everything, there is a trade-off here. If you are wondering about the advantages and disadvantages of the model we will implement compared to other models, I recommend you to read <a href="https://vertabelo.com/blog/data-modeling-for-multiple-languages-how-to-design-a-localization-ready-system/">this article</a>.</p>
</blockquote>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/15-08-2022-How-to-Design-Multi-Lingual-Entity/data-model.png" alt="data-model" /></p>
<p>As a result of the tutorial, we already have the <code>Book</code> and <code>Author</code> entities, as an extra, we will just add the <code>BookTranslation</code>.</p>
<blockquote>
<p>In the article, we will make the Name property of the Book entity multi-lingual, but the article is independent of the Book entity, you can make the entity you want multi-lingual with similar codes according to your requirements.</p>
</blockquote>
<h4>Acme.BookStore.Domain.Shared</h4>
<p>Create a folder named <code>MultiLingualObjects</code> and create the following interfaces in its contents.</p>
<p>We will use the <code>IObjectTranslation</code> interface to mark the translation of a multi-lingual entity:</p>
<pre><code class="language-csharp">public interface IObjectTranslation
{
    string Language { get; set; }
}
</code></pre>
<p>We will use the <code>IMultiLingualObject&lt;TTranslation&gt;</code> interface to mark multi-lingual entities:</p>
<pre><code class="language-csharp">public interface IMultiLingualObject&lt;TTranslation&gt;
    where TTranslation : class, IObjectTranslation
{
    ICollection&lt;TTranslation&gt; Translations { get; set; }
}
</code></pre>
<h4>Acme.BookStore.Domain</h4>
<p>In the <code>Books</code> folder, create the <code>BookTranslation</code> class as follows:</p>
<pre><code class="language-csharp">public class BookTranslation : Entity, IObjectTranslation
{
    public Guid BookId { get; set; }

    public string Name { get; set; }

    public string Language { get; set; }

    public override object[] GetKeys()
    {
        return new object[] {BookId, Language};
    }
}
</code></pre>
<p><code>BookTranslation</code> contains the <code>Language</code> property, which contains a language code for translation and a reference to the multi-lingual entity. We also have the <code>BookId</code> foreign key to help us know which book is translated.</p>
<p>Implement <code>IMultiLingualObject</code> in the <code>Book</code> class as follows:</p>
<pre><code class="language-csharp">public class Book : AuditedAggregateRoot&lt;Guid&gt;, IMultiLingualObject&lt;BookTranslation&gt;
{
    public Guid AuthorId { get; set; }

    public string Name { get; set; }

    public BookType Type { get; set; }

    public DateTime PublishDate { get; set; }

    public float Price { get; set; }
    
    public ICollection&lt;BookTranslation&gt; Translations { get; set; }
}
</code></pre>
<p>Create a folder named <code>MultiLingualObjects</code> and add the following class inside of this folder:</p>
<pre><code class="language-csharp">public class MultiLingualObjectManager : ITransientDependency
{
    protected const int MaxCultureFallbackDepth = 5;

    public async Task&lt;TTranslation&gt; FindTranslationAsync&lt;TMultiLingual, TTranslation&gt;(
        TMultiLingual multiLingual,
        string culture = null,
        bool fallbackToParentCultures = true)
        where TMultiLingual : IMultiLingualObject&lt;TTranslation&gt;
        where TTranslation : class, IObjectTranslation
    {
        culture ??= CultureInfo.CurrentUICulture.Name;

        if (multiLingual.Translations.IsNullOrEmpty())
        {
            return null;
        }

        var translation = multiLingual.Translations.FirstOrDefault(pt =&gt; pt.Language == culture);
        if (translation != null)
        {
            return translation;
        }

        if (fallbackToParentCultures)
        {
            translation = GetTranslationBasedOnCulturalRecursive(
                CultureInfo.CurrentUICulture.Parent,
                multiLingual.Translations,
                0
            );

            if (translation != null)
            {
                return translation;
            }
        }
        
        return null;
    }

    protected TTranslation GetTranslationBasedOnCulturalRecursive&lt;TTranslation&gt;(
        CultureInfo culture, ICollection&lt;TTranslation&gt; translations, int currentDepth)
        where TTranslation : class, IObjectTranslation
    {
        if (culture == null ||
            culture.Name.IsNullOrWhiteSpace() ||
            translations.IsNullOrEmpty() ||
            currentDepth &gt; MaxCultureFallbackDepth)
        {
            return null;
        }

        var translation = translations.FirstOrDefault(pt =&gt; pt.Language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase));
        return translation ?? GetTranslationBasedOnCulturalRecursive(culture.Parent, translations, currentDepth + 1);
    }
}
</code></pre>
<p>With <code>MultiLingualObjectManager</code>'s <code>FindTranslationAsync</code> method, we get the translated version of the book according to <code>CurrentUICulture</code>. If no translation of culture is found, we return null.</p>
<blockquote>
<p>Every thread in .NET has <code>CurrentCulture</code> and <code>CurrentUICulture</code> objects.</p>
</blockquote>
<h4>Acme.BookStore.EntityFrameworkCore</h4>
<p>In the <code>OnModelCreating</code> method of the <code>BookStoreDbContext</code> class, configure the <code>BookTranslation</code> as follows:</p>
<pre><code class="language-csharp">builder.Entity&lt;BookTranslation&gt;(b =&gt;
{
    b.ToTable(BookStoreConsts.DbTablePrefix + &quot;BookTranslations&quot;,
        BookStoreConsts.DbSchema);

    b.ConfigureByConvention();

    b.HasKey(x =&gt; new {x.BookId, x.Language});
});
</code></pre>
<blockquote>
<p>I haven't explicitly set up a one-to-many relationship between <code>Book</code> and <code>BookTranslation</code> here, but the entity framework will do it for us.</p>
</blockquote>
<p>After that, you can just run the following command in a command-line terminal to add a new database migration (in the directory of the <code>EntityFrameworkCore</code> project):</p>
<pre><code class="language-bash">dotnet ef migrations add Added_BookTranslation
</code></pre>
<p>This will add a new migration class to your project. You can then run the following command (or run the <code>.DbMigrator</code> application) to apply changes to the database:</p>
<pre><code class="language-bash">dotnet ef database update
</code></pre>
<p>Add the following code to the <code>ConfigureServices</code> method of the <code>BookStoreEntityFrameworkCoreModule</code>:</p>
<pre><code class="language-csharp"> Configure&lt;AbpEntityOptions&gt;(options =&gt;
 {
     options.Entity&lt;Book&gt;(bookOptions =&gt;
     {
         bookOptions.DefaultWithDetailsFunc = query =&gt; query.Include(o =&gt; o.Translations);
     });
});
</code></pre>
<p>Now we can use <code>WithDetailsAsync</code> without any parameters on <code>BookAppService</code> knowing that <code>Translations</code> will be included.</p>
<h4>Acme.BookStore.Application.Contracts</h4>
<p>Implement <code>IObjectTranslation</code> in the <code>BookDto</code> class as follows:</p>
<pre><code class="language-csharp">public class BookDto : AuditedEntityDto&lt;Guid&gt;, IObjectTranslation
{
    public Guid AuthorId { get; set; }

    public string AuthorName { get; set; }

    public string Name { get; set; }

    public BookType Type { get; set; }

    public DateTime PublishDate { get; set; }

    public float Price { get; set; }
    
    public string Language { get; set; }
}
</code></pre>
<p><code>Language</code> property is required to understand which language the translated book name belongs to in the UI.</p>
<p>Create the <code>AddBookTranslationDto</code> class in the <code>Books</code> folder as follows:</p>
<pre><code class="language-csharp">public class AddBookTranslationDto : IObjectTranslation
{
    [Required]
    public string Language { get; set; }
    
    [Required]
    public string Name { get; set; }
}
</code></pre>
<p>Add the <code>AddTranslationsAsync</code> method to the <code>IBookAppService</code> as follows:</p>
<pre><code class="language-csharp">public interface IBookAppService :
    ICrudAppService&lt;
        BookDto,
        Guid,
        PagedAndSortedResultRequestDto,
        CreateUpdateBookDto&gt;
{
    Task&lt;ListResultDto&lt;AuthorLookupDto&gt;&gt; GetAuthorLookupAsync();
    
    Task AddTranslationsAsync(Guid id, AddBookTranslationDto input); // added this line
}
</code></pre>
<h4>Acme.BookStore.Application</h4>
<p>Now, we need to implement the <code>AddTranslationsAsync</code> method in <code>BookAppService</code> and include <code>Translations</code> in the <code>Book</code> entity, for this you can change the <code>BookAppService</code> as follows:</p>
<pre><code class="language-csharp">[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
    CrudAppService&lt;
        Book, //The Book entity
        BookDto, //Used to show books
        Guid, //Primary key of the book entity
        PagedAndSortedResultRequestDto, //Used for paging/sorting
        CreateUpdateBookDto&gt;, //Used to create/update a book
    IBookAppService //implement the IBookAppService
{
    private readonly IAuthorRepository _authorRepository;

    public BookAppService(
        IRepository&lt;Book, Guid&gt; repository,
        IAuthorRepository authorRepository)
        : base(repository)
    {
        _authorRepository = authorRepository;
        GetPolicyName = BookStorePermissions.Books.Default;
        GetListPolicyName = BookStorePermissions.Books.Default;
        CreatePolicyName = BookStorePermissions.Books.Create;
        UpdatePolicyName = BookStorePermissions.Books.Edit;
        DeletePolicyName = BookStorePermissions.Books.Create;
    }

    public override async Task&lt;BookDto&gt; GetAsync(Guid id)
    {
        //Get the IQueryable&lt;Book&gt; from the repository
        var queryable = await Repository.WithDetailsAsync(); // this line changed

        //Prepare a query to join books and authors
        var query = from book in queryable
            join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
            where book.Id == id
            select new { book, author };

        //Execute the query and get the book with author
        var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
        if (queryResult == null)
        {
            throw new EntityNotFoundException(typeof(Book), id);
        }

        var bookDto = ObjectMapper.Map&lt;Book, BookDto&gt;(queryResult.book);
        bookDto.AuthorName = queryResult.author.Name;
        return bookDto;
    }

    public override async Task&lt;PagedResultDto&lt;BookDto&gt;&gt; GetListAsync(PagedAndSortedResultRequestDto input)
    {
        //Get the IQueryable&lt;Book&gt; from the repository
        var queryable = await Repository.WithDetailsAsync(); // this line changed

        //Prepare a query to join books and authors
        var query = from book in queryable
            join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
            select new {book, author};

        //Paging
        query = query
            .OrderBy(NormalizeSorting(input.Sorting))
            .Skip(input.SkipCount)
            .Take(input.MaxResultCount);

        //Execute the query and get a list
        var queryResult = await AsyncExecuter.ToListAsync(query);

        //Convert the query result to a list of BookDto objects
        var bookDtos = queryResult.Select(x =&gt;
        {
            var bookDto = ObjectMapper.Map&lt;Book, BookDto&gt;(x.book);
            bookDto.AuthorName = x.author.Name;
            return bookDto;
        }).ToList();

        //Get the total count with another query
        var totalCount = await Repository.GetCountAsync();

        return new PagedResultDto&lt;BookDto&gt;(
            totalCount,
            bookDtos
        );
    }

    public async Task&lt;ListResultDto&lt;AuthorLookupDto&gt;&gt; GetAuthorLookupAsync()
    {
        var authors = await _authorRepository.GetListAsync();

        return new ListResultDto&lt;AuthorLookupDto&gt;(
            ObjectMapper.Map&lt;List&lt;Author&gt;, List&lt;AuthorLookupDto&gt;&gt;(authors)
        );
    }

    public async Task AddTranslationsAsync(Guid id, AddBookTranslationDto input)
    {
        var queryable = await Repository.WithDetailsAsync();
        
        var book = await AsyncExecuter.FirstOrDefaultAsync(queryable, x =&gt; x.Id == id);

        if (book.Translations.Any(x =&gt; x.Language == input.Language))
        {
            throw new UserFriendlyException($&quot;Translation already available for {input.Language}&quot;);
        }

        book.Translations.Add(new BookTranslation
        {
            BookId = book.Id,
            Name = input.Name,
            Language = input.Language
        });
        
        await Repository.UpdateAsync(book);
    }

    private static string NormalizeSorting(string sorting)
    {
        if (sorting.IsNullOrEmpty())
        {
            return $&quot;book.{nameof(Book.Name)}&quot;;
        }

        if (sorting.Contains(&quot;authorName&quot;, StringComparison.OrdinalIgnoreCase))
        {
            return sorting.Replace(
                &quot;authorName&quot;,
                &quot;author.Name&quot;,
                StringComparison.OrdinalIgnoreCase
            );
        }

        return $&quot;book.{sorting}&quot;;
    }
}
</code></pre>
<p>Create the <code>MultiLingualBookObjectMapper</code> class as follows:</p>
<pre><code class="language-csharp">public class MultiLingualBookObjectMapper : IObjectMapper&lt;Book, BookDto&gt;, ITransientDependency
{
    private readonly MultiLingualObjectManager _multiLingualObjectManager;

    private readonly ISettingProvider _settingProvider;

    public MultiLingualBookObjectMapper(
        MultiLingualObjectManager multiLingualObjectManager,
        ISettingProvider settingProvider)
    {
        _multiLingualObjectManager = multiLingualObjectManager;
        _settingProvider = settingProvider;
    }

    public BookDto Map(Book source)
    {
        var translation = AsyncHelper.RunSync(() =&gt;
            _multiLingualObjectManager.FindTranslationAsync&lt;Book, BookTranslation&gt;(source));

        return new BookDto
        {
            Id = source.Id,
            AuthorId = source.AuthorId,
            Type = source.Type,
            Name = translation?.Name ?? source.Name,
            PublishDate = source.PublishDate,
            Price = source.Price,
            Language = translation?.Language ?? AsyncHelper.RunSync(() =&gt; _settingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage)),
            CreationTime = source.CreationTime,
            CreatorId = source.CreatorId,
            LastModificationTime = source.LastModificationTime,
            LastModifierId = source.LastModifierId
        };
    }

    public BookDto Map(Book source, BookDto destination)
    {
        return default;
    }
}
</code></pre>
<p>To map the multi-lingual <code>Book</code> entity to <code>BookDto</code>, we implement custom mapping using the <code>IObjectMapper&lt;TSource, TDestination&gt;</code> interface. If no translation is found, default values are returned.</p>
<p>So far we have created the entire infrastructure. We don't need to change anything in the UI, if there is a translation according to the language chosen by the user, the list view will change. However, I want to create a simple modal where we can add new translations to an existing book in order to see what we have done.</p>
<h4>Acme.BookStore.Web</h4>
<p>Create a new razor page named <code>AddTranslationModal</code> in the <code>Books</code> folder as below.</p>
<p><strong>View</strong></p>
<pre><code class="language-html">@page
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model Acme.BookStore.Web.Pages.Books.AddTranslationModal

@{
    Layout = null;
}

&lt;form asp-page=&quot;/Books/AddTranslationModal&quot;&gt;
    &lt;abp-modal&gt;
        &lt;abp-modal-header&gt;Translations&lt;/abp-modal-header&gt;
        &lt;abp-modal-body&gt;
            &lt;abp-input asp-for=&quot;Id&quot;&gt;&lt;/abp-input&gt;
            &lt;abp-select asp-for=&quot;@Model.TranslationViewModel.Language&quot; asp-items=&quot;Model.Languages&quot; class=&quot;form-select&quot;&gt;
                &lt;option selected value=&quot;&quot;&gt;Pick a language&lt;/option&gt;
            &lt;/abp-select&gt;
            &lt;abp-input asp-for=&quot;TranslationViewModel.Name&quot;&gt;&lt;/abp-input&gt;
        &lt;/abp-modal-body&gt;
        &lt;abp-modal-footer buttons=&quot;@(AbpModalButtons.Cancel | AbpModalButtons.Save)&quot;&gt;&lt;/abp-modal-footer&gt;
    &lt;/abp-modal&gt;
&lt;/form&gt;
</code></pre>
<p><strong>Model</strong></p>
<pre><code class="language-csharp">public class AddTranslationModal : BookStorePageModel
{
    [HiddenInput]
    [BindProperty(SupportsGet = true)]
    public Guid Id { get; set; }
    
    public List&lt;SelectListItem&gt; Languages { get; set; }

    [BindProperty]
    public BookTranslationViewModel TranslationViewModel { get; set; }
    
    private readonly IBookAppService _bookAppService;
    private readonly ILanguageProvider _languageProvider;

    public AddTranslationModal(
        IBookAppService bookAppService,
        ILanguageProvider languageProvider)
    {
        _bookAppService = bookAppService;
        _languageProvider = languageProvider;
    }

    public async Task OnGetAsync()
    {
        Languages = await GetLanguagesSelectItem();

        TranslationViewModel = new BookTranslationViewModel();
    }
    
    public async Task&lt;IActionResult&gt; OnPostAsync()
    {
        await _bookAppService.AddTranslationsAsync(Id, ObjectMapper.Map&lt;BookTranslationViewModel, AddBookTranslationDto&gt;(TranslationViewModel));
        
        return NoContent();
    }
    
    private async Task&lt;List&lt;SelectListItem&gt;&gt; GetLanguagesSelectItem()
    {
        var result = await _languageProvider.GetLanguagesAsync();
            
        return result.Select(
            languageInfo =&gt; new SelectListItem
            {
                Value = languageInfo.CultureName,
                Text = languageInfo.DisplayName + &quot; (&quot; + languageInfo.CultureName + &quot;)&quot;
            }
        ).ToList();
    }

    public class BookTranslationViewModel
    {
        [Required]
        [SelectItems(nameof(Languages))]
        public string Language { get; set; }
        
        [Required]
        public string Name { get; set; }

    }
}
</code></pre>
<p>Then, we can open the <code>BookStoreWebAutoMapperProfile</code> class and define the required mapping as follows:</p>
<pre><code class="language-csharp">CreateMap&lt;AddTranslationModal.BookTranslationViewModel, AddBookTranslationDto&gt;();
</code></pre>
<p>Finally, change the content of <code>index.js</code> in the <code>Books</code> folder as follows:</p>
<pre><code class="language-javascript">$(function () {
    var l = abp.localization.getResource('BookStore');
    var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
    var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
    var addTranslationModal = new abp.ModalManager(abp.appPath + 'Books/AddTranslationModal'); // added this line
    
    var dataTable = $('#BooksTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, &quot;asc&quot;]],
            searching: false,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
            columnDefs: [
                {
                    title: l('Actions'),
                    rowAction: {
                        items:
                            [
                                {
                                    text: l('Edit'),
                                    visible: abp.auth.isGranted('BookStore.Books.Edit'),
                                    action: function (data) {
                                        editModal.open({ id: data.record.id });
                                    }
                                },
                                {
                                    text: l('Add Translation'), // added this action
                                    visible: abp.auth.isGranted('BookStore.Books.Edit'),
                                    action: function (data) {
                                        addTranslationModal.open({ id: data.record.id });
                                    }
                                },
                                {
                                    text: l('Delete'),
                                    visible: abp.auth.isGranted('BookStore.Books.Delete'),
                                    confirmMessage: function (data) {
                                        return l(
                                            'BookDeletionConfirmationMessage',
                                            data.record.name
                                        );
                                    },
                                    action: function (data) {
                                        acme.bookStore.books.book
                                            .delete(data.record.id)
                                            .then(function() {
                                                abp.notify.info(
                                                    l('SuccessfullyDeleted')
                                                );
                                                dataTable.ajax.reload();
                                            });
                                    }
                                }
                            ]
                    }
                },
                {
                    title: l('Name'),
                    data: &quot;name&quot;
                },
                {
                    title: l('Author'),
                    data: &quot;authorName&quot;
                },
                {
                    title: l('Type'),
                    data: &quot;type&quot;,
                    render: function (data) {
                        return l('Enum:BookType:' + data);
                    }
                },
                {
                    title: l('PublishDate'),
                    data: &quot;publishDate&quot;,
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString();
                    }
                },
                {
                    title: l('Price'),
                    data: &quot;price&quot;
                },
                {
                    title: l('CreationTime'),
                    data: &quot;creationTime&quot;,
                    render: function (data) {
                        return luxon
                            .DateTime
                            .fromISO(data, {
                                locale: abp.localization.currentCulture.name
                            }).toLocaleString(luxon.DateTime.DATETIME_SHORT);
                    }
                }
            ]
        })
    );

    createModal.onResult(function () {
        dataTable.ajax.reload();
    });

    editModal.onResult(function () {
        dataTable.ajax.reload();
    });

    $('#NewBookButton').click(function (e) {
        e.preventDefault();
        createModal.open();
    });
});
</code></pre>
<h2>Conclusion</h2>
<p>With a multi-lingual application, you can expand your market share, but if not designed well, may your application will be unusable. So, I've tried to explain how to design a sustainable multi-lingual entity in this article.</p>
<h3>Source Code</h3>
<p>You can find source code of the example solution used in this article <a href="https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreMultiLingual">here</a>.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/2aa284d0-5bcc-75ab-5510-3a05b2e36c9c" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/2aa284d0-5bcc-75ab-5510-3a05b2e36c9c" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-devexpress-blazor-ui-components-with-the-abp-framework-wrpoa8rw</guid>
      <link>https://abp.io/community/posts/using-devexpress-blazor-ui-components-with-the-abp-framework-wrpoa8rw</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>DevExpress</category>
      <title>Using DevExpress Blazor UI Components With the ABP Framework</title>
      <description> I will show you how to integrate DevExpress Blazor UI components into ABP Framework-based applications.</description>
      <pubDate>Tue, 15 Dec 2020 06:07:10 Z</pubDate>
      <a10:updated>2026-09-25T23:10:18Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using DevExpress Blazor UI Components With the ABP Framework</h1>
<p>Hi, in this step by step article, I will show you how to integrate <a href="https://demos.devexpress.com/blazor/">DevExpress</a> blazor UI components into ABP Framework-based applications.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/both-example-result.png" alt="both-example-result" /></p>
<p><em>(A screenshot from the example application developed in this article)</em></p>
<h2>Create the Project</h2>
<blockquote>
<p>ABP Framework offers startup templates to get into business faster.</p>
</blockquote>
<p>In this article, I will create a new startup template with EF Core as a database provider and Blazor for UI framework. But if you already have a project with Blazor UI, you don't need to create a new startup template, you can directly implement the following steps to your existing project.</p>
<blockquote>
<p>If you already have a project with the Blazor UI, you can skip this section.</p>
</blockquote>
<ul>
<li>Before starting to development, we will create a solution named <code>DevExpressSample</code> (or whatever you want). We will create a new startup template with EF Core as a database provider and Blazor for UI framework by using <a href="https://docs.abp.io/en/abp/latest/CLI">ABP CLI</a>:</li>
</ul>
<pre><code class="language-bash">abp new DevExpressSample -u blazor
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/initial-project.png" alt="initial-project" /></p>
<ul>
<li><p>Our project boilerplate will be ready after the download is finished. Then, we can open the solution in the Visual Studio (or any other IDE) and run the <code>DevExpressSample.DbMigrator</code> to create the database and seed initial data (which creates the admin user, admin role, permissions etc.)</p>
</li>
<li><p>After database and initial data created,</p>
</li>
<li><p>Run the <code>DevExpressSample.HttpApi.Host</code> to see our server side working and</p>
</li>
<li><p>Run the <code>DevExpressSample.Blazor</code> to see our UI working properly.</p>
</li>
</ul>
<blockquote>
<p><em>Default login credentials for admin: username is <strong>admin</strong> and password is <strong>1q2w3E*</strong></em></p>
</blockquote>
<h2>Install DevExpress</h2>
<p>You can follow <a href="https://docs.devexpress.com/Blazor/401986/getting-started/install-components-and-create-an-application/without-devexpress-installer/microsoft-templates">this documentation</a> to install DevExpress packages into your computer.</p>
<blockquote>
<p>Don't forget to add <em>&quot;DevExpress NuGet Feed&quot;</em> to your <strong>Nuget Package Sources</strong>.</p>
</blockquote>
<h3>Adding DevExpress NuGet Packages</h3>
<p>Add the <code>DevExpress.Blazor</code> NuGet package to the <code>DevExpressSample.Blazor</code> project.</p>
<pre><code>Install-Package DevExpress.Blazor
</code></pre>
<h3>Register DevExpress Resources</h3>
<ol>
<li><p>Add the following line to the HEAD section of the <code>wwwroot/index.html</code> file within the <code>DevExpressSample.Blazor</code> project:</p>
<pre><code class="language-Razor">&lt;head&gt;
    &lt;!--...--&gt;
    &lt;link href=&quot;_content/DevExpress.Blazor/dx-blazor.css&quot; rel=&quot;stylesheet&quot; /&gt;
&lt;/head&gt;
</code></pre>
</li>
<li><p>In the <code>DevExpressSampleBlazorModule</code> class, call the <code>AddDevExpressBlazor()</code> method from your project's <code>ConfigureServices()</code> method:</p>
<pre><code class="language-csharp">public override void ConfigureServices(ServiceConfigurationContext context)
{
    var environment = context.Services.GetSingletonInstance&lt;IWebAssemblyHostEnvironment&gt;();
    var builder = context.Services.GetSingletonInstance&lt;WebAssemblyHostBuilder&gt;();
   // ...
    builder.Services.AddDevExpressBlazor();
}
</code></pre>
</li>
<li><p>Register the <strong>DevExpressSample.Blazor</strong> namespace in the <code>_Imports.razor</code> file:</p>
<pre><code class="language-Razor">@using DevExpress.Blazor
</code></pre>
</li>
</ol>
<h3>Result</h3>
<p>The installation step was done. You can use any DevExpress Blazor UI component in your application:</p>
<p>Example:  A Scheduler:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/sample-appointment.gif" alt="sample-appointment" /></p>
<p>This example has been created by following <a href="https://demos.devexpress.com/blazor/SchedulerViewTypes">this documentation</a>.</p>
<h2>The Sample Application</h2>
<p>We have created a sample application with <a href="https://docs.devexpress.com/Blazor/DevExpress.Blazor.DxDataGrid-1">Data Grid</a> example.</p>
<h3>The Source Code</h3>
<p>You can download the source code from <a href="https://github.com/abpframework/abp-samples/tree/master/DevExpress-Blazor">here</a>.</p>
<p>The related files for this example are marked in the following screenshots.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-app-contract.png" alt="data-grid-app-contract" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-application.png" alt="data-grid-application" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-12-10-How-to-Integrate-the-DevExpress-Blazor-Component/data-grid-blazor.png" alt="data-grid-web" /></p>
<h3>Additional Notes</h3>
<h4>Data Storage</h4>
<p>I've used an in-memory list to store data for this example, instead of a real database. Because it is not related to DevExpress usage. There is a <code>SampleDataService.cs</code> file in <code>Data</code> folder at <code>DevExpressSample.Application.Contracts</code> project. All the data is stored here.</p>
<h2>Conclusion</h2>
<p>In this article, I've explained how to use <a href="https://www.devexpress.com/blazor/">DevExpress</a> components in your application. ABP Framework is designed so that it can work with any UI library/framework.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/how-to-add-custom-property-to-the-user-entity-6ggxiddr</guid>
      <link>https://abp.io/community/posts/how-to-add-custom-property-to-the-user-entity-6ggxiddr</link>
      <a10:author>
        <a10:name>berkansasmaz</a10:name>
        <a10:uri>https://abp.io/community/members/berkansasmaz</a10:uri>
      </a10:author>
      <category>extending-entities</category>
      <title>How to add custom property to the user entity</title>
      <description>I will explain how you can customize the user entity class, which is available in every web application you create using the ABP framework, according to your needs.</description>
      <pubDate>Thu, 08 Oct 2020 14:13:55 Z</pubDate>
      <a10:updated>2026-09-25T20:42:07Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to Add Custom Properties to the User Entity</h1>
<blockquote>
<p><strong>Note:</strong> If your application is greater than version 4.3.3, please follow <a href="https://community.abp.io/posts/how-to-add-custom-properties-to-the-user-entity-rixchoha">this article</a>.</p>
</blockquote>
<h2>Introduction</h2>
<p>In this step-by-step article, I will explain how you can customize the user entity class, which is available in every web application you create using the ABP framework, according to your needs. When you read this article, you will learn how to override the services of built-in modules, extend the entities, extend data transfer objects and customize the user interface in the applications you develop using the ABP framework.</p>
<blockquote>
<p><strong>Note:</strong> This article is not about customizing the <code>Login</code> page. If you have such a need, please follow <a href="https://community.abp.io/posts/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd">this article</a>.</p>
</blockquote>
<p>You can see the screenshots below which we will reach at the end of the article.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/custom-identity-user-list.png" alt="custom-identity-user-list" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/new-user.png" alt="new-user" /></p>
<h2>Preparing the Project</h2>
<h3>Startup template and the initial run</h3>
<p>Abp Framework offers startup templates to get into the work faster. We can create a new startup template using Abp CLI:</p>
<p><code>abp new CustomizeUserDemo</code></p>
<blockquote>
<p>In this article, I will go through the MVC application, but it will work also in the <a href="https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&amp;DB=EF&amp;Tiered=No">Angular</a>, <a href="https://docs.abp.io/en/abp/latest/Getting-Started?UI=BlazorServer&amp;DB=EF&amp;Tiered=No">Blazor Server</a>, and <a href="https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor&amp;DB=EF&amp;Tiered=No">Blazor WebAssembly</a> application.</p>
</blockquote>
<p>After the download is finished, we can run <strong>CustomizeUserDemo.DbMigrator</strong> project to create the database migrations and seed the initial data (admin user, role, etc). Then we can run <code>CustomizeUserDemo.Web</code> to see that our application is working.</p>
<blockquote>
<p>Default admin username is <strong>admin</strong> and password is <strong>1q2w3E*</strong></p>
</blockquote>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/initial-project.png" alt="initial-project" /></p>
<p>In this article, we will go through a scenario together and find the solutions to our questions through this scenario. However, since the scenario is not a real-life scenario, it may be strange, please don't get too about this issue :)</p>
<h2>Step-1</h2>
<p>Add two new properties to the <code>AppUser</code> in the Users folder of the <strong>CustomizeUserDemo.Domain</strong> project as follows:</p>
<pre><code class="language-csharp">public string Title { get; protected set; }

public int Reputation { get; protected set; }
</code></pre>
<h2>Step-2</h2>
<p>Create the Users folder in the <strong>CustomizeUserDemo.Domain.Shared</strong> project, create the class <code>UserConsts</code> inside the folder and update the class you created as below:</p>
<pre><code class="language-csharp">public static class UserConsts
{
    public const string TitlePropertyName = &quot;Title&quot;;

    public const string ReputationPropertyName = &quot;Reputation&quot;;

    public const int MaxTitleLength = 64;

    public const double MaxReputationValue = 1_000;

    public const double MinReputationValue = 1;
}
</code></pre>
<h2>Step-3</h2>
<p>Update the <code>CustomizeUserDemoEfCoreEntityExtensionMappings</code> class in the <strong>CustomizeUserDemo.EntityFramework</strong> project in the EntityFrameworkCore folder as below:</p>
<pre><code class="language-csharp">public static class CustomizeUserDemoEfCoreEntityExtensionMappings
{
    private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();

    public static void Configure()
    {
        CustomizeUserDemoGlobalFeatureConfigurator.Configure();
        CustomizeUserDemoModuleExtensionConfigurator.Configure();

        OneTimeRunner.Run(() =&gt;
        {
            ObjectExtensionManager.Instance
                .MapEfCoreProperty&lt;IdentityUser, string&gt;(
                    nameof(AppUser.Title),
                    (entityBuilder, propertyBuilder) =&gt;
                    {
                        propertyBuilder.HasDefaultValue(&quot;&quot;);
                        propertyBuilder.HasMaxLength(UserConsts.MaxTitleLength);
                    }
                ).MapEfCoreProperty&lt;IdentityUser, int&gt;(
                    nameof(AppUser.Reputation),
                    (entityBuilder, propertyBuilder) =&gt;
                    {
                        propertyBuilder.HasDefaultValue(UserConsts.MinReputationValue);
                    }
                );
        });
    }
}
</code></pre>
<p>This class can be used to map these extra properties to table fields in the database. Please read <a href="https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities">this</a> article to improve your understanding of what we are doing.</p>
<p>So far, we have added our extra features to the <code>User</code> entity and matched these features with the <code>ef core</code>.</p>
<p>Now we need to add migration to see what has changed in our database. This for, open the Package Manager Console (PMC) under the menu Tools &gt; NuGet Package Manager.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/nuget-package-manager.png" alt="nuget-package-manager" /></p>
<p>Select the <strong>CustomizeUserDemo.EntityFramework</strong> as the <strong>default project</strong> and execute the following command:</p>
<pre><code class="language-bash">Add-Migration &quot;Updated-User-Entity&quot;
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/added-new-migration.png" alt="added-new-migration" /></p>
<p>This will create a new migration class inside the <code>Migrations</code> folder of the <strong>CustomizeUserDemo.EntityFrameworkCore</strong> project.</p>
<blockquote>
<p>If you are using another IDE than the Visual Studio, you can use <code>dotnet-ef</code> tool as <a href="https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration">documented here</a>.</p>
</blockquote>
<p>Finally, run the <strong>CustomizeUserDemo.DbMigrator</strong> project to update the database.</p>
<p>When we updated the database, you can see that the <code>Title</code> and <code>Reputation</code> columns are added to the <code>Users</code> table.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/user-table.png" alt="user-table" /></p>
<h2>Step-4</h2>
<p>Open the <code>CustomizeUserDemoModuleExtensionConfigurator</code> in the <strong>CustomizeUserDemo.Domain.Shared</strong> project, and change the contents of the <code>ConfigureExtraProperties</code> method as shown below:</p>
<pre><code class="language-csharp">private static void ConfigureExtraProperties()
{
    ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity =&gt;
    {
        identity.ConfigureUser(user =&gt;
        {
            user.AddOrUpdateProperty&lt;string&gt;(
                UserConsts.TitlePropertyName,
                options =&gt;
                {
                    options.Attributes.Add(new RequiredAttribute());
                    options.Attributes.Add(
                        new StringLengthAttribute(UserConsts.MaxTitleLength)
                    );
                }
            );
            user.AddOrUpdateProperty&lt;int&gt;(
                UserConsts.ReputationPropertyName,
                options =&gt;
                {
                    options.DefaultValue = UserConsts.MinReputationValue;
                    options.Attributes.Add(
                        new RangeAttribute(UserConsts.MinReputationValue, UserConsts.MaxReputationValue)
                    );
                }
            );
        });
    });
}
</code></pre>
<p>That's it. Now let's run the application and look at the Identity user page. You can also try to edit and recreate a record if you want, it will work even though we haven't done anything extra. Here is the magic code behind ABP framework.</p>
<p>If there is a situation you want to add, you can click the contribute button or make a comment. Also, if you like the article, don't forget to share it :)</p>
<p>Happy coding :)</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/images/others/blank-cover-image-150_79.png" />
      <media:content url="https://abp.io/images/others/blank-cover-image-150_79.png" medium="image" />
    </item>
  </channel>
</rss>