<?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 12:36:30 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=enisn" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-6-abp-studio-git-integration-09tr41ec</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-6-abp-studio-git-integration-09tr41ec</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp-studio</category>
      <category>git</category>
      <category>ai</category>
      <title>Deep Dive on ABP AI Agent #6: ABP Studio Git Integration</title>
      <description>How git integration and AI Agent work together seamlessly in ABP Studio</description>
      <pubDate>Tue, 16 Jun 2026 09:25:54 Z</pubDate>
      <a10:updated>2026-09-26T06:18:01Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent #6: ABP Studio Git Integration</h1>
<p>When I use an AI coding agent, I do not only care about whether it can change files.</p>
<p>I care about what happens around those changes.</p>
<p>Which branch am I on? What exactly changed? Can I review the diff before I commit? Did I accidentally touch a file from another package? Is my branch behind the default branch? If a pull request already has feedback, can I bring that context back into the coding session without copying every comment by hand?</p>
<p>That is where Git integration in <strong>ABP Studio</strong> becomes important.</p>
<p>Git is not just the final step after the agent finishes. For me, it is the confidence layer around the whole workflow. It helps me keep AI-assisted work reviewable, recoverable, and connected to the same team process I already use every day.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/cover-image.png" alt="ABP Studio Git Integration cover" /></p>
<h2>Git In The AI Workflow</h2>
<p>Most development work is not a straight line from prompt to done.</p>
<p>I may ask ABP Agent to make a small change, then review the diff and adjust the direction. I may ask it to address feedback from a pull request. I may start from a GitHub issue, create a branch, let the agent investigate, and then decide which changes are ready to commit.</p>
<p>In all of those moments, Git gives me a practical boundary:</p>
<ul>
<li>this is the branch I am working on,</li>
<li>these are the files that changed,</li>
<li>this is the diff I need to review,</li>
<li>this is the commit I am about to create,</li>
<li>and this is the context I want to send back to the team.</li>
</ul>
<p>Without a Git-aware workflow, AI changes can feel a little too loose. The agent may be productive, but I still need a clean way to inspect, group, commit, push, and discuss the result.</p>
<p>ABP Studio Git Integration brings that loop into the same place where I already work with the solution and the agent.</p>
<h2>Initializing Git For A Solution</h2>
<p>The Git panel starts with the active solution.</p>
<p>If the solution is not a Git repository yet, ABP Studio does not pretend otherwise. It shows a simple empty state and lets me initialize Git from there. I can choose the initial branch name, create a <code>.gitignore</code>, and create the first commit.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-initialize-repository.png" alt="Initializing Git for an ABP solution in ABP Studio" /></p>
<p>That is useful for new ABP solutions because the first Git step is part of the project setup, not something I need to remember after the fact.</p>
<p>If I want to put the solution on GitHub, Studio can also help with that path. After connecting my GitHub account, I can publish the repository under my account or an organization, choose the repository name, add a description, and decide whether it should be private.</p>
<p>The small but important detail is that Git becomes part of the solution experience early. I do not need to move from ABP Studio to a separate Git tool just to create the repository before I start working with ABP Agent.</p>
<h2>Changed Files And Diff Review</h2>
<p>Once Git is active, the Git panel becomes the place I check after an agent session or a manual edit.</p>
<p>I can see the current branch, remote state, changed files, and the selected file diff. The changes are not only a flat list. In an ABP solution, they can be grouped in a way that follows the solution structure, so changes under different packages or solution areas are easier to scan.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-changes-and-diff.png" alt="Changed files and diff review in ABP Studio Git Integration" /></p>
<p>That grouping matters in real ABP work.</p>
<p>If I asked ABP Agent to adjust a public web page, but I see changes in an admin package, I immediately know to slow down and review why. If a change touches a contract package and a UI package, the grouping helps me understand that relationship before I commit.</p>
<p>The diff viewer is also part of the same loop. I do not need to leave the workspace just to answer the basic review question:</p>
<pre><code class="language-text">What did this task actually change?
</code></pre>
<p>That is the question I want to answer before a commit, especially when AI helped produce the diff.</p>
<h2>Selected Files And Commit Messages</h2>
<p>Committing is not only pressing a button.</p>
<p>I still want to choose which files belong together. I still want the commit message to match the change. I still want to avoid committing work on a protected branch by accident.</p>
<p>ABP Studio keeps that flow visible. I can select the files I want, write a summary and description, and commit to the current branch. When AI is enabled, Studio can generate a commit message from the selected diffs.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-ai-commit-message.gif" alt="Generating a commit message from selected changes" /></p>
<p>I like this because it keeps the AI help close to the actual diff.</p>
<p>A generic prompt like &quot;write a commit message&quot; depends on what I paste into the chat. In Studio, the commit message generator can work from the selected files. That makes the result more focused, and I still stay in control because the generated text lands in the commit fields before I use it.</p>
<p>The protected branch warning is another important part of the experience. If the current branch should not receive direct commits, Studio makes that visible and pushes me toward the safer workflow: create a branch, review the diff, then commit there.</p>
<p>That is the right kind of guardrail. It does not make Git complicated. It makes the normal team habit harder to miss.</p>
<h2>Branching, Stashing, And Syncing</h2>
<p>AI-assisted development often starts with a branch decision.</p>
<p>Sometimes I am starting fresh from the default branch. Sometimes I am building on work already in my current branch. Sometimes I have local changes and need to switch context without losing them.</p>
<p>ABP Studio exposes those decisions in the Git panel.</p>
<p>I can create a branch, switch branches, update from the default branch, fetch, pull, push, and see whether I am ahead or behind. If I switch branches while I have local changes, Studio asks what should happen to that work: leave it behind as a stash or bring it with me.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-branch-and-stash.png" alt="Branch switching and stashed changes in ABP Studio" /></p>
<p>That choice is more important than it looks.</p>
<p>When I am working with an agent, I do not want local changes to silently follow me into the wrong branch. I also do not want to lose half-finished work just because I need to inspect another issue. The stash flow turns that into an explicit decision.</p>
<p>The same idea applies to sync.</p>
<p>If my branch is behind, I can update before I continue. If I have local commits, I can push them. If a merge or pull produces conflicts, Studio shows the conflicted files and gives me a path to resolve, abort, continue, or send the conflict context to ABP Agent.</p>
<p>That keeps the Git workflow close to the coding workflow. I can move from change to review to sync without mentally switching tools.</p>
<h2>AI Review And Manual Diff Comments</h2>
<p>There is a moment before a commit where I often want a second look.</p>
<p>Not a full pull request review. Not a long architecture discussion. Just a focused pass over the files I selected:</p>
<pre><code class="language-text">Does this diff contain something suspicious?
Did the agent miss a small edge case?
Is there a line I should check again before committing?
</code></pre>
<p>ABP Studio supports that with AI review on selected Git changes.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-ai-review.png" alt="AI review suggestions on selected Git changes" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-ai-review-details.png" alt="AI review suggestions on selected Git changes" /></p>
<p>AI review is not the only way to leave notes on a diff. I can also write my own comments directly on changed lines while I am reviewing.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-6-abp-studio-git-integration/git-diff-comments.png" alt="Manual comments on Git diff in ABP Studio" /></p>
<p>The useful part is that the review is attached to the diff. Suggestions and notes appear near the changed lines, and if there is something I want the agent to handle, I can send those review notes to ABP Agent.</p>
<p>That changes the feel of the workflow.</p>
<p>Instead of asking the agent to code and then manually re-explaining my review comments, I can turn the review result back into a task. Whether a note comes from AI review or from something I wrote myself, the agent gets the file, line, and note context. I still review the result, but I spend less time copying context between places.</p>
<p>Git also helps with recovery. In a Git repository, ABP Studio can offer <strong>Back to this point</strong> in the agent conversation. For me, that is a comfort feature: if an agent turn takes the work in the wrong direction, I can return to an earlier point instead of manually untangling every changed file.</p>
<p>I still treat Git commits as the real checkpoints for team work. But during a live agent session, being able to go back to a previous point makes experimentation feel less risky.</p>
<h2>GitHub Issue Context</h2>
<p>Many tasks do not start as a prompt. They start as an issue.</p>
<p>The issue has the requirement, comments, labels, screenshots, and sometimes a conversation about what is expected. If that context stays only in the browser, I have to copy it into the agent manually.</p>
<p>ABP Studio can bring GitHub issues into the Git area.</p>
<p>I can filter issues, open one, read the description and comments, create a branch for that issue, and send the issue context to ABP Agent.</p>
<p>That makes the workflow feel natural:</p>
<ol>
<li>Pick the issue.</li>
<li>Create a branch for it.</li>
<li>Send the relevant context to ABP Agent.</li>
<li>Let the agent inspect the solution and implement the change.</li>
<li>Review the Git diff before committing.</li>
</ol>
<p>The important detail is that the agent starts from the same context I would start from as a developer. It sees the issue title, description, labels, included comments, and attached images when they are part of the selected context.</p>
<p>That is much better than writing a vague prompt that tries to summarize the issue from memory.</p>
<h2>Pull Request Feedback Context</h2>
<p>Pull request feedback is another place where Git integration helps the AI workflow.</p>
<p>When I open a pull request inside ABP Studio, I can see the PR title, branches, comments, reviews, and requested changes. If I am not on the PR branch, Studio can switch to it. Then I can choose which comments or requested changes should be included and send that context to ABP Agent.</p>
<p>This is the workflow I want when a reviewer asks for changes:</p>
<ul>
<li>read the feedback,</li>
<li>switch to the right branch,</li>
<li>include only the relevant comments,</li>
<li>send the request to ABP Agent,</li>
<li>review the resulting diff,</li>
<li>commit and push.</li>
</ul>
<p>The include and exclude controls matter here. Not every PR comment should become an agent instruction. Some comments are discussion, some are already resolved, and some are optional. I want to choose what becomes context.</p>
<p>That keeps the agent from treating the entire PR timeline as one undifferentiated command. I can shape the task before sending it.</p>
<h2>Git Integration In The Deep Dive Series</h2>
<p>In the earlier articles, we looked at modes, tools, MCP, scopes, and workflows.</p>
<p>Git integration connects those ideas to the normal development lifecycle.</p>
<ul>
<li><strong>Ask and Plan</strong> help me understand and shape the work.</li>
<li><strong>Agent mode</strong> can make the change.</li>
<li><strong>Tools</strong> help the agent use ABP Studio context.</li>
<li><strong>Scopes</strong> keep the working area focused.</li>
<li><strong>Workflows</strong> make repeated actions predictable.</li>
<li><strong>Git integration</strong> lets me review, recover, commit, push, and collaborate around the result.</li>
</ul>
<p>That last part is easy to underestimate.</p>
<p>The value of an AI coding agent is not only how quickly it can modify files. The value is whether I can bring those modifications into a professional development workflow without losing control.</p>
<p>Git is the structure that makes that possible.</p>
<h2>Conclusion</h2>
<p>ABP Studio Git Integration makes ABP Agent feel more grounded.</p>
<p>It gives me a clear path from issue to branch, from agent work to diff review, from selected files to commit, and from pull request feedback back into the agent.</p>
<p>For everyday work, that means fewer context switches. For AI-assisted work, it means more confidence.</p>
<p>I can let the agent help, but I do not have to accept the result blindly. I can inspect the diff, ask for a review, commit intentionally, push when ready, and keep the whole process connected to GitHub and the team workflow.</p>
<p>That is the part I value most: <strong>Git integration turns AI output into reviewable development work.</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21e1ed-426b-7055-69e5-73e0344656c0" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21e1ed-426b-7055-69e5-73e0344656c0" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-2-supported-ai-models-in-abp-studio-usage-recommendations-3krbc7yc</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-2-supported-ai-models-in-abp-studio-usage-recommendations-3krbc7yc</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp-studio</category>
      <category>ai</category>
      <category>models</category>
      <category>gpt</category>
      <category>claude</category>
      <title>Deep Dive on ABP AI Agent #2: Supported AI Models in ABP Studio + Usage Recommendations</title>
      <description>When I use ABP Studio AI, the model choice is part of the workflow. Some tasks need careful reasoning. Some need speed. Some need a large context window. Some need image support because the browser or a screenshot is involved. Some are small text-processing tasks where using the most capable model would only make the work slower and more expensive.</description>
      <pubDate>Wed, 10 Jun 2026 05:51:56 Z</pubDate>
      <a10:updated>2026-09-26T07:31:03Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent #2: Supported AI Models in ABP Studio + Usage Recommendations</h1>
<p>There is one question I ask almost as often as &quot;Which mode should I use?&quot;:</p>
<p><strong>Which model should do this work?</strong></p>
<p>At first, it is tempting to answer that question by always choosing the strongest model in the list. That feels safe. If a model is more capable, why not use it for everything?</p>
<p>In real work, I do not think about it that way.</p>
<p>When I use <strong>ABP Studio AI</strong>, the model choice is part of the workflow. Some tasks need careful reasoning. Some need speed. Some need a large context window. Some need image support because the browser or a screenshot is involved. Some are small text-processing tasks where using the most capable model would only make the work slower and more expensive.</p>
<p>So I treat model selection as a practical decision, not a trophy selection.</p>
<h2>What ABP Studio Supports Today</h2>
<p>ABP Studio gives me a curated model setup by default. It keeps the first experience simple, while still letting me choose from a broader model catalog when I want to tune the setup for a specific kind of work.</p>
<p>The important word here is <strong>focused</strong>.</p>
<p>ABP Studio does not treat every model as an equally good choice for agent work. A coding agent needs things like a useful context window, tool support, text output, and reliable behavior in repeated agent loops. Studio keeps the model experience closer to that reality.</p>
<p>The built-in model set currently includes:</p>
<p>| Model | How I think about it |
| --- | --- |
| Claude Sonnet 4.6 | The default main model for day-to-day Ask, Plan, and Agent work. |
| Claude Haiku 4.5 | A fast supporting model for research, browser work, and lightweight text processing. |
| Claude Opus 4.7 | A stronger option when the task needs deeper reasoning or more careful review. |
| GPT-5.5 | Another strong option for main conversations or review-style work. |
| GLM-5.1 | A text/code option for tasks that do not need image input. |</p>
<p>I do not read this list as a ranking. I read it as the default toolbox.</p>
<p>And it is not a closed box. If I need a different model for a specific task, I can open the Models settings, search the catalog, filter by category, and add more models to my selection.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-2-supported-ai-models-abp-studio-ai/abp-agent-model-catalog.png" alt="ABP Studio Models settings showing the selectable model catalog" /></p>
<p>That is an important distinction. The built-in models are there so I can start with sensible defaults. They are not there to force every team, every solution, or every workflow into the same model choices.</p>
<h2>The Main Model</h2>
<p>The main model is the one I feel most directly in the conversation.</p>
<p>It is used when I ask questions, create plans, or let ABP Agent work through an implementation. This is the model behind the normal flow of the chat.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-2-supported-ai-models-abp-studio-ai/abp-agent-model-selector.png" alt="ABP Agent model selector showing the current conversation model" /></p>
<p>For most work, I keep a balanced model as the main model. A Sonnet-style model is a good default because it is capable enough for real development tasks without making every small question feel heavy.</p>
<p>This is the model I use for:</p>
<ul>
<li>Understanding a module or package</li>
<li>Planning a feature before editing code</li>
<li>Applying a reviewed plan</li>
<li>Fixing ordinary build or test failures</li>
<li>Making changes where the expected result is easy to review</li>
</ul>
<p>When the task gets broader, I become more intentional.</p>
<p>If I am asking ABP Agent to reason across several modules, plan a risky refactor, review architecture, or inspect a subtle regression, I am more willing to switch to a stronger model. The extra capability is useful when the cost of a shallow answer is high.</p>
<p>For a quick localization change or a small DTO update, that same choice can be wasteful. The strongest model is not always the best model for the moment.</p>
<h2>Role-Based Models</h2>
<p>One detail I like in ABP Studio AI is that model selection is not only one global dropdown.</p>
<p>Studio separates the main conversation model from supporting model roles. That means I can keep the main model strong enough for the conversation while using lighter models for background work.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-2-supported-ai-models-abp-studio-ai/abp-agent-model-settings-agents.png" alt="ABP Agent model settings for main, research, browser, and text processor models" /></p>
<p>The roles are easier to understand if I describe them by how they feel in daily use.</p>
<p><strong>Main Model</strong> is the model I am actively talking to. It carries the normal Ask, Plan, and Agent experience.</p>
<p><strong>Research Model</strong> is for research and ABP documentation searcher work. I usually keep this lightweight because research often involves gathering, narrowing, and summarizing context before the main model decides what to do with it.</p>
<p><strong>Browser Model</strong> is used by the browser subagent in Agent mode. This role should stay fast and practical. When browser screenshots are involved, I choose a model that supports image input. A text-only model may be fine for code, but it is not the right fit when the work depends on seeing the UI.</p>
<p><strong>Text Processor Model</strong> is for smaller language tasks such as summarizing errors, generating commit messages, or consolidating learned lessons. This is exactly where I do not want to spend the most capable model every time.</p>
<p>The Git Review model is separate too.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-2-supported-ai-models-abp-studio-ai/abp-agent-model-settings-git-review.png" alt="ABP Agent model settings for Git Review model selection" /></p>
<p>For AI Review, I like the &quot;Ask me every time&quot; behavior. Some reviews are routine. Some reviews deserve a stronger model because the change is large, security-sensitive, or touches architecture. Asking each time keeps that decision close to the actual change.</p>
<p>If a team wants consistent review behavior, a fixed review model also makes sense. The key is that Git Review does not have to silently follow the same model I use for ordinary chat.</p>
<h2>How I Choose In Practice</h2>
<p>For quick questions, I use the default main model.</p>
<p>If I am asking &quot;Where is this permission defined?&quot; or &quot;Why does this module reference that package?&quot;, I do not need to overthink the model. I want a clear answer and maybe a few source references.</p>
<p>For planning larger work, I use a stronger main model when the decision matters.</p>
<p>Plan mode is where the model can save me from an expensive wrong turn. If the change crosses layers, modules, permissions, UI, or database behavior, I prefer a model that can hold more context and reason carefully. I still narrow the scope where possible, because a focused prompt usually beats a huge unfocused one.</p>
<p>For implementation, I care about reliability more than raw size.</p>
<p>Agent mode is not only about generating code. It is about reading the solution, editing files, running checks, seeing failures, and trying again. A good main model should follow instructions consistently and use tools well. For supporting roles, I usually keep the lighter defaults.</p>
<p>For UI and browser tasks, I check image support.</p>
<p>If the task involves screenshots, browser interaction, visual verification, or UI state, the browser model needs to be able to understand images. This is one reason I do not treat every text/code model as interchangeable.</p>
<p>For reviews, I choose based on risk.</p>
<p>A small formatting or localization change does not need the same review setup as a large change in authorization, multi-tenancy, persistence, or distributed behavior. For deeper reviews, I am willing to use a stronger model because the goal is not speed. The goal is to catch what I missed.</p>
<p>For cost and latency, I avoid using the strongest model everywhere.</p>
<p>This is not only about credits. It is also about pace. If every background task uses a heavy model, the development loop feels slower. Keeping lightweight models for lightweight jobs makes ABP Studio AI feel more responsive.</p>
<h2>A Simple Rule</h2>
<p>The model name matters less than the job.</p>
<p>When I choose a model in ABP Studio AI, I usually ask:</p>
<p>| Situation | Model choice I prefer |
| --- | --- |
| Normal Ask, Plan, or Agent work | Balanced main model |
| Broad planning or risky implementation | Stronger main model |
| Research and documentation lookup | Lightweight supporting model |
| Browser tasks with screenshots | Vision-capable browser model |
| Error summaries and commit messages | Lightweight text processor model |
| Important AI Review | Stronger model or ask each time |</p>
<p>That keeps the experience practical.</p>
<p>Modes decide how much action I want: Ask, Plan, or Agent.</p>
<p>Models decide which brain should handle the work.</p>
<p>When those two choices are made intentionally, ABP Studio AI feels less like one generic AI button and more like a set of tools I can tune for the task in front of me.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21c243-3829-d805-714d-c1e1dfd2b44e" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21c243-3829-d805-714d-c1e1dfd2b44e" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/deep-dive-on-abp-ai-agent-1-agent-plan-and-ask-modes-62wteg9t</guid>
      <link>https://abp.io/community/posts/deep-dive-on-abp-ai-agent-1-agent-plan-and-ask-modes-62wteg9t</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</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 #1: Agent, Plan and Ask Modes</title>
      <description>Deep dive into ABP AI Agent modes like Agent, Plan and Ask Modex.</description>
      <pubDate>Tue, 09 Jun 2026 07:52:20 Z</pubDate>
      <a10:updated>2026-09-26T09:30:22Z</a10:updated>
      <content:encoded><![CDATA[<h1>Deep Dive on ABP AI Agent #1: Agent, Plan and Ask Modes</h1>
<p>There is a small question I like to answer before I type anything into <strong>ABP Agent</strong>:</p>
<p><strong>Do I want an answer, a plan, or action?</strong></p>
<p>That question looks simple, but it changes the whole experience. Sometimes I am only trying to understand why a module is structured a certain way. Sometimes I already know the direction, but I want the implementation path checked before touching files. And sometimes the task is clear enough that I want ABP Agent to do the work, run the checks, and iterate with me.</p>
<p>That is where the three modes in ABP Studio AI become more than labels. They help me choose the right level of trust, risk, and action for the moment.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-1-agent-plan-ask-abp-studio-ai/abp-agent-mode-picker.png" alt="ABP Agent mode picker showing Agent, Plan, and Ask" /></p>
<h2>Ask Mode: When I Want To Understand</h2>
<p><strong>Ask</strong> is the mode I reach for when I want to stay in learning mode.</p>
<p>It is useful when I am reading a solution and want to ask questions like:</p>
<ul>
<li>What is this module responsible for?</li>
<li>Why is this permission checked here?</li>
<li>How does this application service relate to the domain layer?</li>
<li>What would happen if I changed this setting, dependency, or flow?</li>
<li>Which ABP concept should I use for this requirement?</li>
</ul>
<p>The important part is that Ask mode is read-only. I can explore the codebase, ABP concepts, architecture, or possible approaches without worrying that files will be changed as a side effect of the conversation.</p>
<p>That makes it a comfortable starting point. I do not need to prepare a perfect prompt. I can ask a rough question, follow up with more context, and slowly turn uncertainty into something clearer.</p>
<p>For me, Ask mode is especially helpful when I join a solution after some time away. Instead of jumping between files and trying to rebuild the story manually, I can ask ABP Agent to explain the shape of the solution in the language of ABP: modules, layers, permissions, application services, entities, settings, events, and runtime pieces.</p>
<h2>Plan Mode: When I Want To Think Before Changing Code</h2>
<p><strong>Plan</strong> is the mode I use when the next step is probably implementation, but I do not want to start editing yet.</p>
<p>This is the middle ground between a conversation and a code change. ABP Agent can inspect the solution in a read-only way, ask clarifying questions when the requirement is not clear enough, and produce a structured plan before any file is modified.</p>
<p>That changes the feeling of working with AI. Instead of saying &quot;go build this&quot; and reviewing only the result, I can review the approach first:</p>
<ul>
<li>Which files will likely be affected?</li>
<li>Which ABP layers are involved?</li>
<li>Does the implementation path match the existing solution style?</li>
<li>Are there missing decisions before the work starts?</li>
<li>Is this a small change, or is it actually a larger workflow?</li>
</ul>
<p>This is useful for changes that cross boundaries: adding a new entity, adjusting an application service, introducing a permission, changing a UI flow, or touching more than one module. Those are exactly the moments where I want a second pass before code starts moving.</p>
<p>When a plan is active, ABP Studio gives me clear actions around it. I can view the plan, detach it if it is no longer the right direction, or apply it with Agent mode when I am ready to move from planning to implementation.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-18-deep-dive-1-agent-plan-ask-abp-studio-ai/abp-agent-plan-actions.png" alt="ABP Agent plan actions for viewing, detaching, or applying a plan" /></p>
<p>The small detail I like here is that the plan does not disappear into the chat history. It becomes something I can review and intentionally carry into the next step.</p>
<h2>Agent Mode: When I Am Ready For Action</h2>
<p><strong>Agent</strong> is the mode I choose when I am ready to let ABP Agent work on the solution.</p>
<p>This is the action mode. ABP Agent can edit files, run commands, build projects, use ABP Studio tasks, and iterate when something fails. It is the right choice when the task is clear enough and I am comfortable letting the agent make changes that I will review afterward.</p>
<p>For small trusted tasks, I may go directly to Agent mode:</p>
<ul>
<li>Add a missing localization entry</li>
<li>Fix a straightforward build error</li>
<li>Update a simple DTO mapping</li>
<li>Add a validation rule that matches an existing pattern</li>
<li>Apply a plan that I have already reviewed</li>
</ul>
<p>For larger work, I prefer not to start here. Agent mode is powerful, and power is better when it is intentional. If I am not sure about the shape of the change, I usually start with Ask or Plan first.</p>
<h2>A Practical Workflow</h2>
<p>The modes are most useful when I treat them as a workflow, not as three disconnected buttons.</p>
<p>For larger changes, my usual flow is:</p>
<ol>
<li><strong>Ask</strong> to understand the area and the existing conventions.</li>
<li><strong>Plan</strong> to turn the requirement into a reviewable implementation path.</li>
<li><strong>Agent</strong> to apply the plan, build, and iterate.</li>
</ol>
<p>For learning, I often stay entirely in Ask mode. If I am trying to understand ABP multi-tenancy behavior, module dependencies, permission definitions, or why a solution is organized a certain way, there is no need to involve file changes.</p>
<p>For small tasks, I may go directly to Agent mode. The key is that I already know what I want, the risk is low, and the expected result is easy to review.</p>
<p>Here is the simple rule I keep in mind:</p>
<p>| Situation | Mode I Choose | Why |
| --- | --- | --- |
| I need an explanation | Ask | It keeps the conversation read-only. |
| I need a direction before implementation | Plan | It gives me a reviewable path before changes. |
| I am ready for ABP Agent to work | Agent | It can edit, build, run tasks, and iterate. |</p>
<h2>Why This Matters</h2>
<p>AI-assisted development can feel too fast when the tool moves from idea to code before I have decided what kind of help I actually need.</p>
<p>The three modes slow that moment down in a good way. They let me say:</p>
<ul>
<li>&quot;Just explain this.&quot;</li>
<li>&quot;Think through the change first.&quot;</li>
<li>&quot;Now implement it.&quot;</li>
</ul>
<p>That separation makes the work feel more intentional. It also makes the output easier to review, because the mode already tells me what kind of result I should expect.</p>
<p>Ask gives me understanding. Plan gives me a path. Agent gives me action.</p>
<p>Used together, they make ABP Studio AI feel less like a single big button and more like a development partner that can adapt to the level of confidence I have at each step.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21bd8b-164c-adfe-c35d-a9021cc8019e" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21bd8b-164c-adfe-c35d-a9021cc8019e" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-studio-is-now-available-on-linux-ge3bnnqa</guid>
      <link>https://abp.io/community/posts/abp-studio-is-now-available-on-linux-ge3bnnqa</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp-studio</category>
      <category>tool</category>
      <category>linux</category>
      <title>ABP Studio Is Now Available on Linux</title>
      <description>
We are excited to announce that ABP Studio, our cross-platform desktop application for ABP developers, is now available on Linux.</description>
      <pubDate>Mon, 08 Jun 2026 09:43:00 Z</pubDate>
      <a10:updated>2026-09-26T09:29:17Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP Studio Is Now Available on Linux</h1>
<p>We are excited to announce that <a href="https://abp.io/studio">ABP Studio</a>, our cross-platform desktop application for ABP developers, is now available on Linux.</p>
<p>With this release, Linux users can download and run ABP Studio as an <strong>x64 AppImage</strong>. This is an important step in making ABP Studio available wherever .NET and ABP developers prefer to work.</p>
<h2>What can you do with ABP Studio?</h2>
<p><a href="https://abp.io/studio">ABP Studio</a> is a desktop application designed to make ABP development faster, easier, and more comfortable. It offers:</p>
<ul>
<li>Easy creation of new solutions, from simple applications to distributed systems</li>
<li>Visual architecture management for modular monolith and microservice solutions</li>
<li>Solution exploration tools for entities, services, packages, and HTTP APIs</li>
<li>Simplified running, debugging, and monitoring of multi-application solutions</li>
<li>Kubernetes integration capabilities</li>
<li>Built-in access to ABP-specific tooling and workflows</li>
</ul>
<p>The screenshots below were captured from ABP Studio on Linux through the AppImage.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-08-abp-studio-is-now-available-on-linux/abp-studio-solution-system.png" alt="ABP Studio solution system selection" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-08-abp-studio-is-now-available-on-linux/abp-studio-solution-properties.png" alt="ABP Studio solution properties step" /></p>
<h2>Linux Support Has Arrived</h2>
<p>ABP Studio has already been supporting multiple desktop environments, and now Linux joins that list.</p>
<p>You can currently use ABP Studio on:</p>
<ul>
<li>Windows x64</li>
<li>Windows ARM</li>
<li>macOS Apple Silicon</li>
<li>macOS Intel</li>
<li>Linux x64 <strong>(New!)</strong></li>
</ul>
<p>On Linux, the current distribution format is <strong>AppImage</strong>, which provides a practical way to distribute a desktop application across different Linux distributions without requiring a distribution-specific installer package.</p>
<h2>What This Means for Developers</h2>
<p>Many ABP developers use Linux as their daily development environment. Until now, they needed to switch to another operating system to use ABP Studio. With Linux support, developers can now stay on their preferred platform and still benefit from ABP Studio's solution creation, architecture design, solution runner, monitoring, and integrated development experience.</p>
<p>This is especially valuable for teams that already build and run their backend services on Linux-based environments and want to keep their development workflow aligned with that ecosystem.</p>
<h2>AI Agent Is Available on Linux Too</h2>
<p>Another common question from the community has been whether ABP Studio AI Agent can be used on Linux machines. With this release, the answer is yes.</p>
<p>Linux users can now use ABP Studio AI Agent in their own development environment. You can ask questions about your solution, plan implementation steps before changing code, and let the agent help with coding tasks while ABP Studio understands your ABP solution structure, build flow, and runtime context.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-08-abp-studio-is-now-available-on-linux/abp-studio-ai-agent.png" alt="abp studio ai agent on linux" />
For a deeper look at the AI Agent experience, see the original announcement: <a href="https://abp.io/community/announcements/introducing-abp-studio-ai-agent-o1ni0toc">Introducing ABP Studio AI Agent</a>.</p>
<h2>Getting Started</h2>
<p>Downloading and running ABP Studio on Linux is simple:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-06-08-abp-studio-is-now-available-on-linux/abp-studio-download-linux.png" alt="abp studio download on linux" /></p>
<ol>
<li>Go to <a href="https://abp.io/studio">abp.io/studio</a></li>
<li>Download the <strong>Linux x64 AppImage</strong></li>
<li>Open a terminal in the folder where the file was downloaded</li>
<li>Make the AppImage executable and run it</li>
</ol>
<pre><code class="language-bash">chmod +x ./AbpStudio-stable.AppImage
./AbpStudio-stable.AppImage
</code></pre>
<p>Once launched, you can start using ABP Studio just like on the other supported platforms.</p>
<h2>If the AppImage Does Not Run Directly</h2>
<p>Some Linux distributions may require additional runtime support for direct AppImage execution.</p>
<p>For example, on some Ubuntu and Debian-based systems, you may need <code>libfuse2</code>:</p>
<pre><code class="language-bash">sudo apt update
sudo apt install libfuse2
</code></pre>
<p>If FUSE is not available on your machine, you can still extract and run the AppImage manually:</p>
<pre><code class="language-bash">./AbpStudio-stable.AppImage --appimage-extract
./squashfs-root/AppRun
</code></pre>
<p>This fallback can be useful for testing or for environments where AppImage mounting is restricted.</p>
<h2>Current Scope and Limitations</h2>
<p>This first Linux release is intentionally focused so we can deliver a reliable experience quickly.</p>
<p>Here is the current scope:</p>
<ul>
<li>Linux distribution is currently provided as an <strong>x64 AppImage</strong></li>
<li><strong>Linux ARM builds are not published yet</strong></li>
<li>Depending on your Linux distribution, some native desktop or browser-related libraries may need to be installed</li>
<li>When ABP Studio can detect a known native dependency problem, it tries to show guidance in the UI instead of leaving you with an unclear failure</li>
</ul>
<p>This means Linux support is ready to use today, and we will continue to improve the Linux experience in future releases.</p>
<h2>A Better Cross-Platform Experience</h2>
<p>ABP Studio has always aimed to be the default way to start and develop ABP solutions. Linux support brings us closer to that goal by making the Studio experience more accessible across major desktop platforms.</p>
<p>Whether you are creating a new solution, exploring packages and services, running multiple applications together, or monitoring runtime behavior, you can now do that on Linux too.</p>
<h2>Conclusion</h2>
<p>We are happy to finally make ABP Studio available on Linux.</p>
<p>This first release focuses on a practical and reliable target: <strong>Linux x64 through AppImage</strong>. It already opens the door for many developers who prefer Linux as their primary development environment, and it gives us a strong foundation to improve the Linux experience further.</p>
<p>Please download it, try it in your daily workflow, and share your feedback with us. If you encounter a problem or want to request additional Linux targets like ARM, feel free to open an issue and let us know.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21b8ca-0b03-78af-96b6-42da3572d13a" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21b8ca-0b03-78af-96b6-42da3572d13a" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/react-ui-for-abp-framework-is-finally-here-7rfmgb2v</guid>
      <link>https://abp.io/community/posts/react-ui-for-abp-framework-is-finally-here-7rfmgb2v</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>abp</category>
      <category>react</category>
      <category>react-template</category>
      <title>React UI for ABP Framework Is Finally Here</title>
      <description>React in ABP is no longer just something people ask about, hope for, or imagine as the next step. You can now create it, run it, and explore it today</description>
      <pubDate>Thu, 07 May 2026 13:32:09 Z</pubDate>
      <a10:updated>2026-09-26T06:06:35Z</a10:updated>
      <content:encoded><![CDATA[<h1>React UI for ABP Framework Is Finally Here</h1>
<p>If you have followed ABP for a while, you probably know that React support has been one of the most requested topics in the community.</p>
<p>With <strong>ABP 10.4.0-rc.1</strong>, that wait ends. React in ABP is no longer just something people ask about, hope for, or imagine as the next step. You can now create it, run it, and explore it today as a beta/preview experience in the modern template system.</p>
<p>As part of the ABP Framework team, and as one of the developers working on this React effort, I am genuinely happy to finally share it. This RC gives the community an early chance to try it, share feedback, and help us polish the final details before <strong>ABP 10.4 stable</strong>, where we plan to make the React UI generally available.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.4/docs/en/Community-Articles/2026-03-12-official-react-ui-for-abp-framework/images/abp-studio-project-creation-react.png" alt="abp-studio-project-creation-react" /></p>
<h2>Why this matters</h2>
<p>ABP Framework has always been about helping teams build modern, maintainable, production-ready applications faster. With the new React UI, we are extending that same vision to teams who want ABP on the backend and React on the frontend without losing the built-in application features that make ABP productive from day one.</p>
<p>This is not another empty starter. The goal is a <strong>first-class UI option</strong> that fits into the ABP application startup experience and works naturally with familiar ABP concepts such as authentication, authorization, localization, multi-tenancy, modularity, runtime configuration, and deployment.</p>
<p>There is one important detail: the React UI belongs to ABP's <strong>modern template system</strong>. You create it with the <code>--modern</code> flag in the ABP CLI or by selecting the modern template flow in ABP Studio. You can find the technical documentation here: <a href="https://abp.io/docs/10.4/framework/ui/react">React UI documentation</a>.</p>
<h2>A quick look at the architecture</h2>
<p>The final shape is clearer now: a modern React solution gives you a real React application in the solution, plus the ABP administration experience.</p>
<p>First, there is <strong>your React application</strong>. In the modern templates, this lives directly in the solution as a real app under <code>react/</code> or <code>apps/react/</code>. It contains the frontend code you work with every day, including pages, components, routing, API integration, runtime configuration, and authentication setup.</p>
<p>Second, there is the <strong>ABP Admin Console</strong>. The Admin Console is a pre-built React application that provides the standard ABP module management pages. It is delivered through the <code>Volo.Abp.AdminConsole</code> NuGet package, so it can evolve with ABP package updates while your own React application stays focused on your product's business features.</p>
<p>For layered and single-layer modern applications, the Admin Console is hosted by the backend and served under <code>/admin-console/*</code>. For microservice solutions, it runs as a separate React app under <code>apps/react-admin-console/</code>, with its own runtime configuration and the same <code>/admin-console/</code> base path. In both cases, the main React app can link users into the Admin Console when they need full administrative screens.</p>
<p>This split is a practical design choice. Your business UI stays yours, while administration capabilities remain available, consistent, and upgradeable.
<img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.4/docs/en/Community-Articles/2026-03-12-official-react-ui-for-abp-framework/images/react-ui-and-admin-console.png" alt="react-ui-and-admin-console" /></p>
<h2>A different frontend philosophy</h2>
<p>One of the most important things to understand is that this React UI is <strong>not</strong> being shaped with exactly the same architecture as some previous UI options.</p>
<p>We are not trying to ship the whole frontend experience as a closed set of page implementations coming from npm packages. Instead, the generated solution includes the actual page code inside the app itself. You can open it, understand it, refactor it, redesign it, and adapt it without fighting against a packaged black box.</p>
<p>The Admin Console covers ABP's standard module administration pages. Your own React application remains intentionally open and direct. That gives teams a good balance: built-in administrative power from ABP, and full ownership of the product-facing frontend.</p>
<h2>Built for AI-driven development</h2>
<p>The new React UI is also shaped for the era of <strong>AI-assisted development</strong>.</p>
<p>React, TypeScript, Vite, TanStack Router, TanStack Query, Axios, Zod, React Hook Form, and shadcn/ui are technologies that modern coding assistants understand very well. Just as importantly, the generated application contains real frontend code in the solution. That gives AI tools and coding agents concrete project context to read, extend, and refactor.</p>
<p>This direction also fits the broader ABP AI story. ABP Studio already includes an AI assistant experience, and the new <strong>ABP AI Agent</strong> is being introduced to bring code generation, project understanding, issue fixing, and natural-language application evolution directly into the ABP workflow. You can follow that work here: <a href="https://abp.io/community/events/community-talks/the-future-of-abp-studio-ai-agent-code-generation-live-fekeoyjr">The Future of ABP Studio: AI Agent + Code Generation</a>. For the wider toolset, see the <a href="https://abp.io/ai/toolkit">ABP AI Toolkit</a>.</p>
<h2>What the React experience looks like</h2>
<p>The current template already points to the kind of experience React developers expect from a modern application:</p>
<ul>
<li>A Vite-powered React + TypeScript frontend</li>
<li>TanStack Router for client-side routing</li>
<li>TanStack Query for server state and data fetching</li>
<li>OIDC authentication against the ABP Auth Server</li>
<li>Axios-based HTTP client integration</li>
<li>Runtime configuration through <code>dynamic-env.json</code></li>
<li>Localization and permission-aware behavior integrated with ABP application configuration</li>
<li>Tailwind CSS and shadcn/ui components that live in your project and can be customized directly</li>
<li>Zod and React Hook Form for form handling and validation</li>
<li>Vitest for frontend tests</li>
<li>A dedicated Admin Console for ABP module administration</li>
</ul>
<p>Even in its current form, the React UI already feels like a real ABP solution experience, not just a login page plus a few demo screens.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.4/docs/en/Community-Articles/2026-03-12-official-react-ui-for-abp-framework/images/abp-react-app-screenshot.png" alt="abp-react-app-screenshot.png" /></p>
<h2>More than a hello world</h2>
<p>The generated React app is intentionally small enough to understand, but it is not empty.</p>
<p>Out of the box, you already get the kind of foundation most teams expect: login, registration, forgot-password and reset-password flows, runtime configuration, localization, permission-aware routing, API proxy generation, and a simple users page that can deep-link into the Admin Console when full user management is needed.</p>
<p>Depending on the selected options, it can also include a sample Books CRUD page that demonstrates how to build a full create/read/update/delete flow against an ABP backend.</p>
<p>The Admin Console provides the standard management experience for ABP modules, including identity management, roles, organization units, settings, audit logs, OpenIddict administration, language management, text templates, GDPR, SaaS and tenant management, and other module pages depending on your solution configuration.</p>
<p>That is the core value: developers get a clean React application to build their product, while ABP continues to provide the administrative capabilities expected from a production-ready application platform.</p>
<h2>Try it with ABP 10.4 RC</h2>
<p>During the RC period, you can create a modern React solution with ABP 10.4.0-rc.1:</p>
<pre><code class="language-bash">abp new Acme.BookStore --template app --modern
</code></pre>
<p>The React UI is the default UI option when <code>--modern</code> is used, but you can also pass it explicitly:</p>
<pre><code class="language-bash">abp new Acme.BookStore --template app --modern --ui-framework react
</code></pre>
<p>For a single-layer application:</p>
<pre><code class="language-bash">abp new Acme.BookStore --template app-nolayers --modern
</code></pre>
<p>For a microservice solution:</p>
<pre><code class="language-bash">abp new Acme.BookStore --template microservice --modern
</code></pre>
<p>Once ABP 10.4 stable is released, the same modern React experience is planned to become generally available without needing to target the RC version explicitly.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.4/docs/en/Community-Articles/2026-03-12-official-react-ui-for-abp-framework/images/abp-react-ui-modern-template-demo.gif" alt="ABP Framework React UI Modern Template Demo" /></p>
<h2>What's next</h2>
<p>The React UI is now real in ABP 10.4 RC, and the final polishing work continues toward the stable release. If you have been waiting for a real React path in ABP, this is the point where it stops being a wish and starts becoming something you can actually build with.</p>
<p>For me, one of the nicest parts of this RC is that we can finally stop talking about React support in ABP as a future idea and start improving something real together.</p>
<p>Try it, explore it, and share feedback with us while we keep polishing it for <strong>ABP 10.4 stable</strong>.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a2114d0-5518-38a8-d9b3-ab5100b587a4" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a2114d0-5518-38a8-d9b3-ab5100b587a4" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-frameworks-hidden-magic-things-that-just-work-without-you-knowing-vw6osmyt</guid>
      <link>https://abp.io/community/posts/abp-frameworks-hidden-magic-things-that-just-work-without-you-knowing-vw6osmyt</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp-framework</category>
      <category>exploring-abp</category>
      <category>abp-features</category>
      <category>Convention-over-Configuration</category>
      <title>ABP Framework's Hidden Magic: Things That Just Work Without You Knowing</title>
      <description>The ABP Framework is famous for its Convention-over-Configuration approach, which means a lot of things work automatically without explicit configuration. In this article, I'll uncover these "hidden magics" that make ABP so powerful but often go unnoticed by developers.</description>
      <pubDate>Thu, 19 Feb 2026 14:24:38 Z</pubDate>
      <a10:updated>2026-09-26T05:28:40Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP Framework's Hidden Magic: Things That Just Work Without You Knowing</h1>
<p>The ABP Framework is famous for its Convention-over-Configuration approach, which means a lot of things work automatically without explicit configuration. In this article, I'll uncover these &quot;hidden magics&quot; that make ABP so powerful but often go unnoticed by developers.</p>
<hr />
<h2>1. Automatic Service Registration Without Any Attributes</h2>
<p><strong>The Magic:</strong> Any class implementing <code>ITransientDependency</code>, <code>ISingletonDependency</code>, or <code>IScopedDependency</code> is automatically registered with the corresponding lifetime.</p>
<pre><code class="language-csharp">// This is automatically registered as Transient - no configuration needed!
public class MyService : IMyService, ITransientDependency
{
    public void DoSomething() { }
}
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs</code></p>
<p>The framework scans all assemblies and automatically determines service lifetime from class hierarchy. This is why you rarely need to manually register services in ABP.</p>
<hr />
<h2>2. All Interfaces Are Exposed By Default</h2>
<p><strong>The Magic:</strong> When you register a service, it's automatically registered as both itself AND all its implemented interfaces.</p>
<pre><code class="language-csharp">public class UserService : IUserService, IValidationInterceptor
{
    // Registered as both IUserService AND IValidationInterceptor
    // No ExposeServices attribute needed!
}
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Core/DependencyInjection/ExposedServiceExplorer.cs:9-14</code></p>
<pre><code class="language-csharp">private static readonly ExposeServicesAttribute DefaultExposeServicesAttribute =
    new ExposeServicesAttribute
    {
        IncludeDefaults = true,
        IncludeSelf = true
    };
</code></pre>
<hr />
<h2>3. Automatic Validation on Every Method</h2>
<p><strong>The Magic:</strong> Every application service method parameters are automatically validated - you don't need to add <code>[Validate]</code> attributes.</p>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Validation/ValidationInterceptorRegistrar.cs</code></p>
<p>The <code>ValidationInterceptor</code> is automatically added to the interceptor pipeline for all services. Every method call triggers automatic validation of input parameters.</p>
<hr />
<h2>4. Automatic Unit of Work Management</h2>
<p><strong>The Magic:</strong> Every database operation is automatically wrapped in a transaction. You don't need to explicitly configure unit of work for most scenarios.</p>
<p><strong>Where it happens:</strong> The <code>UnitOfWorkInterceptor</code> is auto-registered and automatically:</p>
<ul>
<li>Begins transaction before method execution</li>
<li>Commits on success</li>
<li>Rolls back on exception</li>
</ul>
<hr />
<h2>5. Auditing Is Enabled By Default</h2>
<p><strong>The Magic:</strong> Auditing is <strong>ON</strong> by default, even for anonymous users!</p>
<pre><code class="language-csharp">public class AbpAuditingOptions
{
    public AbpAuditingOptions()
    {
        IsEnabled = true;                    // Enabled by default!
        IsEnabledForAnonymousUsers = true;   // Anonymous users are audited!
        HideErrors = true;                   // Errors are silently hidden
        AlwaysLogOnException = true;         // Exceptions always logged
    }
}
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Auditing/AbpAuditingOptions.cs:73-91</code></p>
<p>This means every entity change and service call is logged automatically unless explicitly disabled.</p>
<hr />
<h2>6. Security Logging Is Always On</h2>
<p><strong>The Magic:</strong> Security logging is enabled by default in ABP!</p>
<pre><code class="language-csharp">public AbpSecurityLogOptions()
{
    IsEnabled = true;  // Hidden: ON by default!
}
</code></pre>
<p>Every authentication attempt, authorization failure, and security-relevant action is logged automatically.</p>
<hr />
<h2>7. Data Filters Are Enabled By Default</h2>
<p><strong>The Magic:</strong> <code>ISoftDelete</code> and <code>IMultiTenant</code> filters are <strong>enabled by default</strong>.</p>
<pre><code class="language-csharp">// In DataFilter.cs - Line 103
_filter.Value = _options.DefaultStates.GetOrDefault(typeof(TFilter))?.Clone() 
    ?? new DataFilterState(true);  // true = enabled!
</code></pre>
<p>This means:</p>
<ul>
<li>Deleted entities are automatically filtered out</li>
<li>Multi-tenant data is automatically isolated</li>
</ul>
<p>You must explicitly <strong>disable</strong> these filters when you need to access all data:</p>
<pre><code class="language-csharp">using (_dataFilter.Disable&lt;IMultiTenant&gt;())
{
    // Query all tenants
}
</code></pre>
<hr />
<h2>8. Object Mapping (Mapperly - The New Standard)</h2>
<p><strong>The Magic:</strong> Starting with <strong>ABP v9.0</strong>, new project templates use <strong>Mapperly</strong> instead of AutoMapper. Any class using Mapperly attributes is automatically configured.</p>
<pre><code class="language-csharp">// Starting with ABP v10.0, new projects use Mapperly instead of AutoMapper

// Inherit from MapperBase - automatically registered with IObjectMapper
public partial class UserMapper : MapperBase&lt;User, UserDto&gt;
{
    public override partial UserDto Map(User source);
}

// For two-way mapping
public partial class UserTwoWayMapper : TwoWayMapperBase&lt;User, UserDto&gt;
{
    public override partial UserDto Map(User source);
    public override partial User ReverseMap(UserDto source);
}
</code></pre>
<p>The mapping is done at <strong>compile-time</strong> (no reflection overhead), and it's automatically registered with ABP's <code>IObjectMapper</code>.</p>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Mapperly/AbpMapperlyConventionalRegistrar.cs</code></p>
<pre><code class="language-csharp">// Automatically discovers and configures all Mapperly mappers
context.Services.OnRegistered(context =&gt;
{
    if (typeof(MapperBase).IsAssignableFrom(context.ImplementationType))
    {
        // Register the mapper
    }
});
</code></pre>
<hr />
<h2>9. Automatic Data Seed Contributor Discovery</h2>
<p><strong>The Magic:</strong> Any class implementing <code>IDataSeedContributor</code> is automatically discovered and executed on application startup.</p>
<pre><code class="language-csharp">// Automatically discovered and run on startup!
public class MyDataSeeder : IDataSeedContributor
{
    public Task SeedAsync(DataSeedContext context)
    {
        // Seed data here
    }
}
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Data/AbpDataModule.cs:40-56</code></p>
<hr />
<h2>10. Automatic Definition Provider Discovery</h2>
<p><strong>The Magic:</strong> These are all auto-discovered without any configuration:</p>
<ul>
<li><code>ISettingDefinitionProvider</code> - Settings</li>
<li><code>IPermissionDefinitionProvider</code> - Permissions</li>
<li><code>IFeatureDefinitionProvider</code> - Features</li>
<li><code>INavigationProvider</code> - Navigation items</li>
</ul>
<hr />
<h2>11. Automatic Widget Discovery</h2>
<p><strong>The Magic:</strong> Any class implementing <code>IWidget</code> is automatically registered and can be rendered in pages.</p>
<p><strong>Where it happens:</strong> <code>Volo.Abp.AspNetCore.Mvc.UI.Widgets/AbpAspNetCoreMvcUiWidgetsModule.cs</code></p>
<hr />
<h2>12. Remote Services Are Enabled By Default</h2>
<p><strong>The Magic:</strong> All API controllers have remote service functionality enabled by default:</p>
<pre><code class="language-csharp">public class RemoteServiceAttribute : Attribute
{
    public bool IsEnabled { get; set; } = true;  // Enabled by default!
}
</code></pre>
<hr />
<h2>13. Auto API Controllers - Application Services Become REST APIs Automatically</h2>
<p><strong>The Magic:</strong> When you create an application service (class implementing an interface or inheriting from <code>ApplicationService</code>), ABP <strong>automatically</strong> creates REST API endpoints for it - no manual controller needed!</p>
<pre><code class="language-csharp">// This interface is automatically exposed as /api/app/product
public interface IProductAppService
{
    Task&lt;List&lt;ProductDto&gt;&gt; GetListAsync();
    Task&lt;ProductDto&gt; CreateAsync(CreateProductDto input);
    Task DeleteAsync(Guid id);
}

// The implementation automatically becomes an API Controller
public class ProductAppService : ApplicationService, IProductAppService
{
    public Task&lt;List&lt;ProductDto&gt;&gt; GetListAsync() { ... }
    public Task&lt;ProductDto&gt; CreateAsync(CreateProductDto input) { ... }
    public Task DeleteAsync(Guid id) { ... }
}

// Available endpoints (auto-generated):
// GET  /api/app/product
// POST /api/app/product
// DELETE /api/app/product/{id}
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.AspNetCore.Mvc/AbpServiceConvention.cs</code></p>
<p>The framework:</p>
<ul>
<li>Converts camelCase method names to kebab-case routes</li>
<li>Maps HTTP methods automatically (Get→GET, Create→POST, Delete→DELETE)</li>
<li>Generates proper DTOs from parameters and return types</li>
<li>Handles serialization/deserialization</li>
</ul>
<hr />
<h2>14. Dynamic Client Proxies - Client-Side Code Generated Automatically</h2>
<p><strong>The Magic:</strong> On the client side, you don't need to write HTTP client code. ABP automatically generates <strong>Dynamic JavaScript Proxies</strong> and <strong>Dynamic C# Proxies</strong> that let you call your APIs as if they were local method calls!</p>
<p><strong>JavaScript (MVC/Razor Pages):</strong></p>
<pre><code class="language-javascript">// Just call it like a local function!
var products = await productAppService.getList();
await productAppService.create({ name: &quot;New Product&quot; });
await productAppService.delete(id);
</code></pre>
<p><strong>C# (Blazor/Console Apps):</strong></p>
<pre><code class="language-csharp">// Inject and use like local method calls!
public class ProductListModel : PageModel
{
    private readonly IProductAppService _productAppService;
    
    public async Task OnGetAsync()
    {
        // Actually makes HTTP call to the server!
        var products = await _productAppService.GetListAsync();
    }
}
</code></pre>
<p><strong>Where it happens:</strong></p>
<ul>
<li>JavaScript: <code>Volo.Abp.AspNetCore.Mvc.UI</code> - Dynamic JavaScript proxies</li>
<li>C#: <code>Volo.Abp.AspNetCore.Mvc.Client</code> - Dynamic C# HTTP clients</li>
</ul>
<p>This is why you can inject application service interfaces directly in Blazor and call them like local methods!</p>
<hr />
<h2>15. Permission Checks</h2>
<p>By default, all application service methods and controllers are <strong>public</strong> and accessible. Add <code>[Authorize]</code> or <code>[AbpAuthorize]</code> to restrict access:</p>
<pre><code class="language-csharp">[Authorize]
public async Task CreateAsync(CreateDto input) { }

[AbpAuthorize(&quot;MyApp.Permissions.CanCreate&quot;)]
public async Task CreateAsync(CreateDto input) { }
</code></pre>
<p>The <code>AuthorizationInterceptor</code> is added only when <code>[Authorize]</code> attribute is present on the class or method.</p>
<hr />
<h2>16. Background Workers Auto-Registration</h2>
<p><strong>The Magic:</strong> Background workers are enabled by default, and any class implementing <code>IBackgroundWorker</code> or <code>IQuartzBackgroundWorker</code> is auto-registered.</p>
<hr />
<h2>17. Entity ID Generation</h2>
<p><strong>The Magic:</strong> ABP automatically detects the best ID generation strategy based on the entity type:</p>
<ul>
<li><code>Guid</code> → Auto-generates GUID</li>
<li><code>int</code>/<code>long</code> → Database identity</li>
<li><code>string</code> → No auto-generation (must provide)</li>
</ul>
<p><strong>Where it happens:</strong> <code>Volo.Abp.Ddd.Domain/Entities/EntityHelper.cs</code></p>
<hr />
<h2>18. Anti-Forgery Token Magic</h2>
<p><strong>The Magic:</strong> ABP automatically handles CSRF protection with these hardcoded values:</p>
<pre><code class="language-csharp">// Blazor Client
private const string AntiForgeryCookieName = &quot;XSRF-TOKEN&quot;;
private const string AntiForgeryHeaderName = &quot;RequestVerificationToken&quot;;
</code></pre>
<hr />
<h2>19. Automatic Event Handler Discovery</h2>
<p><strong>The Magic:</strong> Any class implementing <code>IEventHandler&lt;TEvent&gt;</code> is automatically subscribed to handle events - no manual registration needed!</p>
<pre><code class="language-csharp">// This handler is automatically registered when the assembly loads!
public class OrderCreatedHandler : IEventHandler&lt;OrderCreatedEvent&gt;
{
    public Task HandleEventAsync(OrderCreatedEvent eventData)
    {
        // Handle the event - automatically subscribed!
    }
}
</code></pre>
<hr />
<h2>20. Unit of Work Events - Automatic Save</h2>
<p><strong>The Magic:</strong> Events are not fired immediately - they're collected during the unit of work and fired at the end when everything succeeds!</p>
<pre><code class="language-csharp">// In UnitOfWorkEventPublisher.cs
// Events are queued and published only when UOW successfully completes
await _localEventBus.PublishAsync(
    entityChangeEvent,
    onUnitOfWorkComplete: true  // Wait for UOW to complete!
);
</code></pre>
<p>This ensures transactional consistency - if your UOW fails, no events are fired.</p>
<hr />
<h2>21. Distributed Event Bus - Outbox Pattern</h2>
<p><strong>The Magic:</strong> ABP implements the Outbox Pattern automatically for distributed events, ensuring no events are lost!</p>
<pre><code class="language-csharp">// In DistributedEventBusBase.cs
// Events are stored in outbox table and processed reliably
foreach (var outboxConfig in AbpDistributedEventBusOptions.Outboxes.Values.OrderBy(x =&gt; x.Selector is null))
{
    // Outbox processing happens automatically
}
</code></pre>
<hr />
<h2>22. Automatic Object Extension Properties</h2>
<p><strong>The Magic:</strong> Any property decorated with <code>[DisableAuditing]</code> is automatically excluded from audit logs without any configuration!</p>
<pre><code class="language-csharp">// This property is automatically excluded from auditing
[DisableAuditing]
public string SecretData { get; set; }
</code></pre>
<hr />
<h2>23. Virtual File System</h2>
<p><strong>The Magic:</strong> ABP provides a virtual file system that merges embedded resources from all modules into a single virtual path!</p>
<pre><code class="language-csharp">// Any file embedded as &quot;EmbeddedResource&quot; is accessible virtually
// No configuration needed for module authors!
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.VirtualFileSystem/AbpVirtualFileSystemModule.cs</code></p>
<p>This is how ABP modules include static files (CSS, JS, images) that work without copying to wwwroot.</p>
<hr />
<h2>24. Automatic JSON Serialization Settings</h2>
<p><strong>The Magic:</strong> ABP pre-configures JSON serialization with:</p>
<ul>
<li>Camel case property naming</li>
<li>Null value handling</li>
<li>Reference loop handling</li>
<li>Custom converters for common types</li>
</ul>
<p>All configured automatically.</p>
<hr />
<h2>26. Localization Automatic Discovery</h2>
<p><strong>The Magic:</strong> All <code>.json</code> localization files in the application are automatically discovered and loaded:</p>
<pre><code>/Localization/MyApp/
  en.json
  tr.json
  de.json
</code></pre>
<p>No explicit registration needed - just add files and they're available!</p>
<hr />
<h2>27. Feature Checks</h2>
<p>Add <code>[RequiresFeature]</code> to restrict access based on feature flags:</p>
<pre><code class="language-csharp">[RequiresFeature(&quot;MyApp.Features.SomeFeature&quot;)]
public async Task DoSomethingAsync()
{
}
</code></pre>
<p>The <code>FeatureInterceptor</code> is added only when <code>[RequiresFeature]</code> attribute is present on the class or method.</p>
<hr />
<h2>28. API Versioning Convention</h2>
<p><strong>The Magic:</strong> ABP automatically handles API versioning with sensible defaults:</p>
<ul>
<li>Default version: <code>1.0</code></li>
<li>Version from URL path: <code>/api/v1/...</code></li>
<li>Version from header: <code>Accept: application/json;v=1.0</code></li>
</ul>
<p>All configured automatically unless overridden.</p>
<hr />
<h2>29. Health Check Endpoints</h2>
<p><strong>The Magic:</strong> Health check endpoints are auto-registered:</p>
<ul>
<li><code>/health</code> - Overall health status</li>
<li><code>/health/ready</code> - Readiness check</li>
<li><code>/health/live</code> - Liveness check</li>
</ul>
<p>Includes automatic checks for:</p>
<ul>
<li>Database connectivity</li>
<li>Cache availability</li>
<li>External services</li>
</ul>
<hr />
<h2>30. Swagger/OpenAPI Auto-Configuration</h2>
<p><strong>The Magic:</strong> If you reference <code>Volo.Abp.AspNetCore.Mvc.UI.Swagger</code>, Swagger UI is automatically generated with:</p>
<ul>
<li>All API endpoints documented</li>
<li>Authorization support</li>
<li>Versioning support</li>
<li>XML documentation</li>
</ul>
<p>No configuration needed beyond the package reference!</p>
<hr />
<h2>31. Background Job Queue Magic</h2>
<p><strong>The Magic:</strong> Background jobs are automatically retried with exponential backoff:</p>
<pre><code class="language-csharp">// Jobs are automatically:
// - Queued when published
// - Retried on failure (3 times default)
// - Delayed with exponential backoff
</code></pre>
<p><strong>Where it happens:</strong> <code>Volo.Abp.BackgroundJobs/AbpBackgroundJobOptions.cs</code></p>
<hr />
<h2>Summary Table</h2>
<p>| # | Feature | Default Behavior | You Need to Know |
|---|---------|-----------------|------------------|
| 1 | <strong>Service Registration</strong> | Auto by interface | Implement <code>ITransientDependency</code> |
| 2 | <strong>Service Exposure</strong> | Self + all interfaces | Default is generous |
| 3 | <strong>Validation</strong> | All methods validated | Happens automatically |
| 4 | <strong>Unit of Work</strong> | Transactional by default | Auto-commits/rollbacks |
| 5 | <strong>Auditing</strong> | Enabled + anonymous users | Can disable per entity/method |
| 6 | <strong>Security Log</strong> | Always on | Can configure what to log |
| 7 | <strong>Soft Delete Filter</strong> | Enabled by default | Must disable to query deleted |
| 8 | <strong>Multi-Tenancy Filter</strong> | Enabled by default | Must disable for host data |
| 9 | <strong>Object Mapping</strong> | Mapperly (compile-time) | Inherit from <code>MapperBase</code> |
| 10 | <strong>Data Seeds</strong> | Auto-discovery | Implement <code>IDataSeedContributor</code> |
| 11 | <strong>Remote Services</strong> | Enabled by default | Can disable per service/method |
| 12 | <strong>Auto API Controllers</strong> | App services → REST APIs | No manual controller needed |
| 13 | <strong>Dynamic Client Proxies</strong> | Auto-generated | Call APIs like local methods |
| 14 | <strong>Permissions</strong> | NOT automatic | Must add <code>[Authorize]</code> |
| 15 | <strong>Settings</strong> | Auto-discovery | Define via <code>ISettingDefinitionProvider</code> |
| 16 | <strong>Features</strong> | NOT automatic | Must add <code>[RequiresFeature]</code> |
| 17 | <strong>Background Workers</strong> | Auto-registration | Implement <code>IBackgroundWorker</code> |
| 18 | <strong>Entity ID Generation</strong> | Auto by type | Guid, int, string strategies |
| 19 | <strong>Anti-Forgery</strong> | Auto-enabled | Token cookie/header handling |
| 20 | <strong>Event Handlers</strong> | Auto-discovery | Implement <code>IEventHandler&lt;TEvent&gt;</code> |
| 21 | <strong>UOW Events</strong> | Deferred execution | Transactional consistency |
| 22 | <strong>Distributed Events</strong> | Outbox pattern | Reliable messaging |
| 23 | <strong>Virtual Files</strong> | Module merging | Embedded resources as virtual |
| 24 | <strong>JSON Settings</strong> | Pre-configured | CamelCase, null handling |
| 25 | <strong>Tenant Resolution</strong> | Multi-source chain | Route → Query → Header → Cookie → Subdomain |
| 26 | <strong>Localization</strong> | Auto-discovery | JSON files in /Localization |
| 27 | <strong>API Versioning</strong> | Default v1.0 | URL, header, query support |
| 28 | <strong>Health Checks</strong> | Auto-registered | /health, /health/ready, /health/live |
| 29 | <strong>Swagger</strong> | Auto-generated | With authorization support |
| 30 | <strong>Background Job Queue</strong> | Auto with backoff | 3 retries default |
| 31 | <strong>Widgets</strong> | Auto-discovery | Implement <code>IWidget</code> |</p>
<hr />
<h2>Conclusion</h2>
<p>ABP Framework's hidden magic is what makes it so productive to use. These conventions allow developers to focus on business logic rather than boilerplate configuration. However, understanding these defaults is crucial for:</p>
<ol>
<li><strong>Debugging</strong> - Knowing why certain behaviors happen</li>
<li><strong>Optimization</strong> - Disabling what you don't need</li>
<li><strong>Security</strong> - Understanding what's logged/audited by default</li>
<li><strong>Architecture</strong> - Following the intended patterns</li>
</ol>
<p>The next time something &quot;just works&quot; in ABP, there's likely a hidden convention behind it!</p>
<hr />
<p><em>What hidden ABP magic have you discovered? Share your findings in the comments!</em></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1f8876-b529-910e-27b1-60edf39c8877" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1f8876-b529-910e-27b1-60edf39c8877" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/implementing-multiple-global-query-filters-with-entity-framework-core-ugnsmf6i</guid>
      <link>https://abp.io/community/posts/implementing-multiple-global-query-filters-with-entity-framework-core-ugnsmf6i</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>EfCore</category>
      <category>entity-framework-core</category>
      <category>authentication</category>
      <category>abp-framework</category>
      <category>api</category>
      <title>Implementing Multiple Global Query Filters with Entity Framework Core</title>
      <description>Learn how to implement API key authentication in ABP Framework applications. This comprehensive guide covers what API keys are, when to use them over OAuth2/JWT, real-world use cases for mobile apps and microservices, and a complete implementation with user-based key management, SHA-256 hashing, permission delegation, and built-in UI.
</description>
      <pubDate>Fri, 13 Feb 2026 11:18:41 Z</pubDate>
      <a10:updated>2026-09-26T09:32:34Z</a10:updated>
      <content:encoded><![CDATA[<h1>Implementing Multiple Global Query Filters with Entity Framework Core</h1>
<p>Global query filters are one of Entity Framework Core's most powerful features for automatically filtering data based on certain conditions. They allow you to define filter criteria at the entity level that are automatically applied to all LINQ queries, making it impossible for developers to accidentally forget to include important filtering logic. In this article, we'll explore how to implement multiple global query filters in ABP Framework, covering built-in filters, custom filters, and performance optimization techniques.</p>
<p>By the end of this guide, you'll understand how ABP Framework's data filtering system works, how to create custom global query filters for your specific business requirements, how to combine multiple filters effectively, and how to optimize filter performance using user-defined functions.</p>
<h2>Understanding Global Query Filters in EF Core</h2>
<p>Global query filters were introduced in EF Core 2.0 and allow you to automatically append LINQ predicates to queries generated for an entity type. This is particularly useful for scenarios like multi-tenancy, soft delete, data isolation, and row-level security.</p>
<p>In traditional applications, developers must remember to add filter conditions manually to every query:</p>
<pre><code class="language-csharp">// Manual filtering - error-prone and tedious
var activeBooks = await _bookRepository
    .GetListAsync(b =&gt; b.IsDeleted == false &amp;&amp; b.TenantId == currentTenantId);
</code></pre>
<p>With global query filters, this logic is applied automatically:</p>
<pre><code class="language-csharp">// Filter is applied automatically - no manual filtering needed
var activeBooks = await _bookRepository.GetListAsync();
</code></pre>
<p>ABP Framework provides a sophisticated data filtering system built on top of EF Core's global query filters, with built-in support for soft delete, multi-tenancy, and the ability to easily create custom filters.</p>
<h3>Important: Plain EF Core vs ABP Composition</h3>
<p>In plain EF Core, calling <code>HasQueryFilter</code> multiple times for the same entity does <strong>not</strong> create multiple active filters. The last call replaces the previous one (unless you use newer named-filter APIs in recent EF Core versions).</p>
<p>ABP provides <code>HasAbpQueryFilter</code> to compose query filters safely. This method combines your custom filter with ABP's built-in filters (such as <code>ISoftDelete</code> and <code>IMultiTenant</code>) and with other <code>HasAbpQueryFilter</code> calls.</p>
<h2>ABP Framework's Data Filtering System</h2>
<p>ABP's data filtering system is defined in the <code>Volo.Abp.Data</code> namespace and provides a consistent way to manage filters across your application. The core interface is <code>IDataFilter&lt;TFilter&gt;</code>, which allows you to enable or disable filters programmatically.</p>
<h3>Built-in Filters</h3>
<p>ABP Framework comes with several built-in filters:</p>
<ol>
<li><strong>ISoftDelete</strong>: Automatically filters out soft-deleted entities</li>
<li><strong>IMultiTenant</strong>: Automatically filters entities by current tenant (for SaaS applications)</li>
<li><strong>IIsActive</strong>: Filters entities based on active status</li>
</ol>
<p>Let's look at how these are implemented in the ABP framework:</p>
<p>The <code>ISoftDelete</code> interface is straightforward:</p>
<pre><code class="language-csharp">namespace Volo.Abp;

public interface ISoftDelete
{
    bool IsDeleted { get; }
}
</code></pre>
<p>Any entity implementing this interface will automatically have deleted records filtered out of queries.</p>
<h3>Enabling and Disabling Filters</h3>
<p>ABP provides the <code>IDataFilter&lt;TFilter&gt;</code> service to control filter behavior at runtime:</p>
<pre><code class="language-csharp">public class BookAppService : ApplicationService
{
    private readonly IDataFilter&lt;ISoftDelete&gt; _softDeleteFilter;
    private readonly IRepository&lt;Book, Guid&gt; _bookRepository;

    public BookAppService(
        IDataFilter&lt;ISoftDelete&gt; softDeleteFilter,
        IRepository&lt;Book, Guid&gt; bookRepository)
    {
        _softDeleteFilter = softDeleteFilter;
        _bookRepository = bookRepository;
    }

    public async Task&lt;List&lt;Book&gt;&gt; GetAllBooksIncludingDeletedAsync()
    {
        // Temporarily disable the soft delete filter
        using (_softDeleteFilter.Disable())
        {
            return await _bookRepository.GetListAsync();
        }
    }

    public async Task&lt;List&lt;Book&gt;&gt; GetActiveBooksAsync()
    {
        // Filter is enabled by default - soft-deleted items are excluded
        return await _bookRepository.GetListAsync();
    }
}
</code></pre>
<p>You can also check if a filter is enabled and enable/disable it programmatically:</p>
<pre><code class="language-csharp">public async Task ProcessBooksAsync()
{
    // Check if filter is enabled
    if (_softDeleteFilter.IsEnabled)
    {
        // Enable or disable explicitly
        _softDeleteFilter.Enable();
        // or
        _softDeleteFilter.Disable();
    }
}
</code></pre>
<h2>Creating Custom Global Query Filters</h2>
<p>Now let's create custom global query filters for a real-world scenario. Imagine we have a library management system where we need to filter books based on:</p>
<ol>
<li><strong>Publication Status</strong>: Only show published books in public areas</li>
<li><strong>User's Department</strong>: Users can only see books from their department</li>
<li><strong>Approval Status</strong>: Only show approved content</li>
</ol>
<h3>Step 1: Define Filter Interfaces</h3>
<p>First, create the filter interfaces. You can define them in the same file as your entity or in separate files:</p>
<pre><code class="language-csharp">// Can be placed in the same file as Book entity or in separate files
namespace Library;

public interface IPublishable
{
    bool IsPublished { get; }
    DateTime PublishDate { get; set; }
}

public interface IDepartmentRestricted
{
    Guid DepartmentId { get; }
}

public interface IApproveable
{
    bool IsApproved { get; }
}

public interface IPublishedFilter
{
}

public interface IApprovedFilter
{
}
</code></pre>
<p><code>IPublishable</code> / <code>IApproveable</code> are implemented by entities and define entity properties.
<code>IPublishedFilter</code> / <code>IApprovedFilter</code> are filter-state interfaces used with <code>IDataFilter</code> so you can enable/disable those filters at runtime.</p>
<h3>Step 2: Add Filter Expressions to DbContext</h3>
<p>Now let's add the filter expressions to your existing DbContext. First, here's how to use <code>HasAbpQueryFilter</code> to create <strong>always-on</strong> filters (they cannot be toggled at runtime):</p>
<pre><code class="language-csharp">// MyProjectDbContext.cs
using Microsoft.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.GlobalFeatures;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Authorization;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore.Modeling;

namespace Library;

public class LibraryDbContext : AbpDbContext&lt;LibraryDbContext&gt;
{
    public DbSet&lt;Book&gt; Books { get; set; }
    public DbSet&lt;Department&gt; Departments { get; set; }
    public DbSet&lt;Author&gt; Authors { get; set; }

    public LibraryDbContext(DbContextOptions&lt;LibraryDbContext&gt; options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        builder.Entity&lt;Book&gt;(b =&gt;
        {
            b.ToTable(&quot;Books&quot;);
            b.ConfigureByConvention();

            // HasAbpQueryFilter creates ALWAYS-ACTIVE filters
            // These cannot be toggled at runtime via IDataFilter
            b.HasAbpQueryFilter(book =&gt;
                book.IsPublished &amp;&amp;
                book.PublishDate &lt;= DateTime.UtcNow);

            b.HasAbpQueryFilter(book =&gt; book.IsApproved);
        });

        builder.Entity&lt;Department&gt;(b =&gt;
        {
            b.ToTable(&quot;Departments&quot;);
            b.ConfigureByConvention();
        });
    }
}
</code></pre>
<blockquote>
<p><strong>Note:</strong> Using <code>HasAbpQueryFilter</code> alone creates filters that are always active and cannot be toggled at runtime. This approach is simpler but less flexible. For toggleable filters, see Step 3 below.</p>
</blockquote>
<h3>Step 3: Make Filters Toggleable (Optional)</h3>
<p>If you need filters that can be enabled/disabled at runtime via <code>IDataFilter&lt;T&gt;</code>, override <code>ShouldFilterEntity</code> and <code>CreateFilterExpression</code> instead of (or in addition to) <code>HasAbpQueryFilter</code>:</p>
<pre><code class="language-csharp">// MyProjectDbContext.cs
using System;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Volo.Abp.EntityFrameworkCore;

namespace Library;

public class LibraryDbContext : AbpDbContext&lt;LibraryDbContext&gt;
{
    protected bool IsPublishedFilterEnabled =&gt; DataFilter?.IsEnabled&lt;IPublishedFilter&gt;() ?? false;
    protected bool IsApprovedFilterEnabled =&gt; DataFilter?.IsEnabled&lt;IApprovedFilter&gt;() ?? false;

    protected override bool ShouldFilterEntity&lt;TEntity&gt;(IMutableEntityType entityType)
    {
        if (typeof(IPublishable).IsAssignableFrom(typeof(TEntity)))
        {
            return true;
        }

        if (typeof(IApproveable).IsAssignableFrom(typeof(TEntity)))
        {
            return true;
        }

        return base.ShouldFilterEntity&lt;TEntity&gt;(entityType);
    }

    protected override Expression&lt;Func&lt;TEntity, bool&gt;&gt;? CreateFilterExpression&lt;TEntity&gt;(
        ModelBuilder modelBuilder,
        EntityTypeBuilder&lt;TEntity&gt; entityTypeBuilder)
        where TEntity : class
    {
        var expression = base.CreateFilterExpression&lt;TEntity&gt;(modelBuilder, entityTypeBuilder);

        if (typeof(IPublishable).IsAssignableFrom(typeof(TEntity)))
        {
            Expression&lt;Func&lt;TEntity, bool&gt;&gt; publishFilter = e =&gt;
                !IsPublishedFilterEnabled ||
                (
                    EF.Property&lt;bool&gt;(e, nameof(IPublishable.IsPublished)) &amp;&amp;
                    EF.Property&lt;DateTime&gt;(e, nameof(IPublishable.PublishDate)) &lt;= DateTime.UtcNow
                );

            expression = expression == null
                ? publishFilter
                : QueryFilterExpressionHelper.CombineExpressions(expression, publishFilter);
        }

        if (typeof(IApproveable).IsAssignableFrom(typeof(TEntity)))
        {
            Expression&lt;Func&lt;TEntity, bool&gt;&gt; approvalFilter = e =&gt;
                !IsApprovedFilterEnabled || EF.Property&lt;bool&gt;(e, nameof(IApproveable.IsApproved));

            expression = expression == null
                ? approvalFilter
                : QueryFilterExpressionHelper.CombineExpressions(expression, approvalFilter);
        }

        return expression;
    }
}
</code></pre>
<p>This mapping step is what connects <code>IDataFilter&lt;IPublishedFilter&gt;</code> and <code>IDataFilter&lt;IApprovedFilter&gt;</code> to entity-level predicates. Without this step, <code>HasAbpQueryFilter</code> expressions remain always active.</p>
<blockquote>
<p><strong>Important:</strong> Note that we use <code>DateTime</code> (not <code>DateTime?</code>) in the filter expression to match the entity property type. Adjust accordingly if your entity uses nullable <code>DateTime?</code>.</p>
</blockquote>
<h3>Step 4: Disable Custom Filters with IDataFilter</h3>
<p>Once custom filters are mapped to the ABP data-filter pipeline, you can disable them just like built-in filters:</p>
<pre><code class="language-csharp">public class BookAppService : ApplicationService
{
    private readonly IRepository&lt;Book, Guid&gt; _bookRepository;
    private readonly IDataFilter&lt;IPublishedFilter&gt; _publishedFilter;
    private readonly IDataFilter&lt;IApprovedFilter&gt; _approvedFilter;

    public BookAppService(
        IRepository&lt;Book, Guid&gt; bookRepository,
        IDataFilter&lt;IPublishedFilter&gt; publishedFilter,
        IDataFilter&lt;IApprovedFilter&gt; approvedFilter)
    {
        _bookRepository = bookRepository;
        _publishedFilter = publishedFilter;
        _approvedFilter = approvedFilter;
    }

    public async Task&lt;List&lt;Book&gt;&gt; GetIncludingUnpublishedAndUnapprovedAsync()
    {
        using (_publishedFilter.Disable())
        using (_approvedFilter.Disable())
        {
            return await _bookRepository.GetListAsync();
        }
    }
}
</code></pre>
<h2>Advanced: Multiple Filters with User-Defined Functions</h2>
<p>Starting from ABP v8.3, you can use user-defined function (UDF) mapping for better performance. This approach generates more efficient SQL and allows EF Core to create better execution plans.</p>
<h3>Step 1: Enable UDF Mapping</h3>
<p>First, configure your module to use UDF mapping:</p>
<pre><code class="language-csharp">// MyProjectModule.cs
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.GlobalFilters;
using Microsoft.Extensions.DependencyInjection;

namespace Library;

[DependsOn(
    typeof(AbpEntityFrameworkCoreModule),
    typeof(AbpDddDomainModule)
)]
public class LibraryModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure&lt;AbpEfCoreGlobalFilterOptions&gt;(options =&gt;
        {
            options.UseDbFunction = true; // Enable UDF mapping
        });
    }
}
</code></pre>
<h3>Step 2: Define DbFunctions</h3>
<p>Create static methods that EF Core will map to database functions:</p>
<pre><code class="language-csharp">// LibraryDbFunctions.cs
using Microsoft.EntityFrameworkCore;

namespace Library;

public static class LibraryDbFunctions
{
    public static bool IsPublishedFilter(bool isPublished, DateTime? publishDate)
    {
        return isPublished &amp;&amp; (publishDate == null || publishDate &lt;= DateTime.UtcNow);
    }

    public static bool IsApprovedFilter(bool isApproved)
    {
        return isApproved;
    }

    public static bool DepartmentFilter(Guid entityDepartmentId, Guid userDepartmentId)
    {
        return entityDepartmentId == userDepartmentId;
    }
}
</code></pre>
<h3>Step 4: Apply UDF Filters</h3>
<p>Update your DbContext to use the UDF-based filters:</p>
<pre><code class="language-csharp">// MyProjectDbContext.cs
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    // Map CLR methods to SQL scalar functions.
    // Create matching SQL functions in a migration.
    var isPublishedMethod = typeof(LibraryDbFunctions).GetMethod(
        nameof(LibraryDbFunctions.IsPublishedFilter),
        new[] { typeof(bool), typeof(DateTime?) })!;
    builder.HasDbFunction(isPublishedMethod);

    var isApprovedMethod = typeof(LibraryDbFunctions).GetMethod(
        nameof(LibraryDbFunctions.IsApprovedFilter),
        new[] { typeof(bool) })!;
    builder.HasDbFunction(isApprovedMethod);

    builder.Entity&lt;Book&gt;(b =&gt;
    {
        b.ToTable(&quot;Books&quot;);
        b.ConfigureByConvention();

        // ABP way: define separate filters. HasAbpQueryFilter composes them.
        b.HasAbpQueryFilter(book =&gt;
            LibraryDbFunctions.IsPublishedFilter(book.IsPublished, book.PublishDate));

        b.HasAbpQueryFilter(book =&gt;
            LibraryDbFunctions.IsApprovedFilter(book.IsApproved));
    });
}
</code></pre>
<p>This approach generates cleaner SQL and improves query performance, especially in complex scenarios with multiple filters.</p>
<h2>Working with Complex Filter Combinations</h2>
<p>When combining multiple filters, it's important to understand how they interact. Let's explore some common scenarios.</p>
<h3>Combining Tenant and Department Filters</h3>
<p>In a multi-tenant application, you might need to combine tenant isolation with department-level access control:</p>
<pre><code class="language-csharp">public class BookAppService : ApplicationService
{
    private readonly IRepository&lt;Book, Guid&gt; _bookRepository;
    private readonly IDataFilter&lt;IMultiTenant&gt; _tenantFilter;
    private readonly ICurrentUser _currentUser;

    public BookAppService(
        IRepository&lt;Book, Guid&gt; bookRepository,
        IDataFilter&lt;IMultiTenant&gt; tenantFilter,
        ICurrentUser currentUser)
    {
        _bookRepository = bookRepository;
        _tenantFilter = tenantFilter;
        _currentUser = currentUser;
    }

    public async Task&lt;List&lt;BookDto&gt;&gt; GetMyDepartmentBooksAsync()
    {
        var currentUser = _currentUser;
        var userDepartmentId = GetUserDepartmentId(currentUser);

        // Get all books without department filter, then filter in memory
        // (for scenarios where you need custom filter logic)
        using (_tenantFilter.Disable()) // Optional: disable tenant filter if needed
        {
            var allBooks = await _bookRepository.GetListAsync();
            
            // Apply department filter in memory (custom logic)
            var departmentBooks = allBooks
                .Where(b =&gt; b.DepartmentId == userDepartmentId)
                .ToList();

            return ObjectMapper.Map&lt;List&lt;Book&gt;, List&lt;BookDto&gt;&gt;(departmentBooks);
        }
    }

    private Guid GetUserDepartmentId(ICurrentUser currentUser)
    {
        // Get user's department from claims or database
        var departmentClaim = currentUser.FindClaim(&quot;DepartmentId&quot;);
        return Guid.Parse(departmentClaim.Value);
    }
}
</code></pre>
<h3>Filter Priority and Override</h3>
<p>Sometimes you need to override filters in specific scenarios. ABP provides a flexible way to handle this:</p>
<pre><code class="language-csharp">public async Task&lt;Book&gt; GetBookForEditingAsync(Guid id)
{
    // Disable soft delete filter to get deleted records for restoration
    using (DataFilter.Disable&lt;ISoftDelete&gt;())
    {
        return await _bookRepository.GetAsync(id);
    }
}

public async Task&lt;Book&gt; GetBookIncludingUnpublishedAsync(Guid id)
{
    // Use GetQueryableAsync to customize the query
    var query = await _bookRepository.GetQueryableAsync();
    
    // Manually apply or bypass filters
    var book = await query
        .FirstOrDefaultAsync(b =&gt; b.Id == id);

    return book;
}
</code></pre>
<h2>Best Practices for Multiple Global Query Filters</h2>
<p>When implementing multiple global query filters, consider these best practices:</p>
<h3>1. Keep Filters Simple</h3>
<p>Complex filter expressions can significantly impact query performance. Keep each condition focused on a single concern. In ABP, you can define them separately with <code>HasAbpQueryFilter</code>, which composes with ABP's built-in filters:</p>
<pre><code class="language-csharp">// Good (ABP): separate, focused filters composed by HasAbpQueryFilter
b.HasAbpQueryFilter(b =&gt; b.IsPublished);
b.HasAbpQueryFilter(b =&gt; b.IsApproved);
b.HasAbpQueryFilter(b =&gt; b.DepartmentId == userDeptId);

// Avoid: calling HasQueryFilter multiple times for the same entity
// in plain EF Core (the last call replaces the previous one)
b.HasQueryFilter(b =&gt; b.IsPublished);
b.HasQueryFilter(b =&gt; b.IsApproved);
</code></pre>
<h3>2. Use Indexing</h3>
<p>Ensure your database has appropriate indexes for filtered columns:</p>
<pre><code class="language-csharp">builder.Entity&lt;Book&gt;(b =&gt;
{
    b.HasIndex(b =&gt; b.IsPublished);
    b.HasIndex(b =&gt; b.IsApproved);
    b.HasIndex(b =&gt; b.DepartmentId);
    b.HasIndex(b =&gt; new { b.IsPublished, b.PublishDate });
});
</code></pre>
<h3>3. Consider Performance Impact</h3>
<p>Use UDF mapping for better performance with complex filters. Profile your queries and analyze execution plans.</p>
<h3>4. Document Filter Behavior</h3>
<p>Clearly document which filters are applied to each entity to help developers understand the behavior:</p>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// Book entity with the following global query filters:
/// - ISoftDelete: Automatically excludes soft-deleted books
/// - IMultiTenant: Automatically filters by current tenant
/// - IPublishable: Excludes unpublished books (based on IsPublished and PublishDate)
/// - IApproveable: Excludes unapproved books (based on IsApproved)
/// &lt;/summary&gt;
/// &lt;remarks&gt;
/// Filter interfaces (IPublishable, IApproveable, IPublishedFilter, IApprovedFilter)
/// are defined in Step 1: Define Filter Interfaces
/// &lt;/remarks&gt;
public class Book : AuditedAggregateRoot&lt;Guid&gt;, ISoftDelete, IMultiTenant, IPublishable, IApproveable
{
    public string Name { get; set; }

    public BookType Type { get; set; }

    public DateTime PublishDate { get; set; }

    public float Price { get; set; }

    public bool IsPublished { get; set; }

    public bool IsApproved { get; set; }

    public Guid? TenantId { get; set; }

    public bool IsDeleted { get; set; }

    public Guid DepartmentId { get; set; }
}
</code></pre>
<h2>Testing Global Query Filters</h2>
<p>Testing with global query filters can be challenging. Here's how to do it effectively:</p>
<h3>Unit Testing Filters</h3>
<pre><code class="language-csharp">[Fact]
public void Book_QueryFilter_Should_Filter_Unpublished()
{
    var options = new DbContextOptionsBuilder&lt;BookStoreDbContext&gt;()
        .UseInMemoryDatabase(databaseName: &quot;TestDb&quot;)
        .Options;

    using (var context = new BookStoreDbContext(options))
    {
        context.Books.Add(new Book { Name = &quot;Published Book&quot;, IsPublished = true });
        context.Books.Add(new Book { Name = &quot;Unpublished Book&quot;, IsPublished = false });
        context.SaveChanges();
    }

    using (var context = new BookStoreDbContext(options))
    {
        // Query with filter enabled (default)
        var publishedBooks = context.Books.ToList();
        Assert.Single(publishedBooks);
        Assert.Equal(&quot;Published Book&quot;, publishedBooks[0].Name);
    }
}
</code></pre>
<h3>Integration Testing with Filter Control</h3>
<pre><code class="language-csharp">[Fact]
public async Task Should_Get_Deleted_Book_When_Filter_Disabled()
{
    var dataFilter = GetRequiredService&lt;IDataFilter&gt;();

    // Arrange
    var book = await _bookRepository.InsertAsync(
        new Book { Name = &quot;Test Book&quot; },
        autoSave: true
    );

    await _bookRepository.DeleteAsync(book);

    // Act - with filter disabled
    using (dataFilter.Disable&lt;ISoftDelete&gt;())
    {
        var deletedBook = await _bookRepository
            .FirstOrDefaultAsync(b =&gt; b.Id == book.Id);

        deletedBook.ShouldNotBeNull();
        deletedBook.IsDeleted.ShouldBeTrue();
    }
}
</code></pre>
<h3>Testing Custom Global Query Filters</h3>
<p>Here's a complete example of testing custom toggleable filters:</p>
<pre><code class="language-csharp">[Fact]
public async Task Should_Filter_Unpublished_Books_By_Default()
{
    // Default: filters are enabled
    var result = await WithUnitOfWorkAsync(async () =&gt;
    {
        var bookRepository = GetRequiredService&lt;IRepository&lt;Book, Guid&gt;&gt;();
        return await bookRepository.GetListAsync();
    });

    // Only published and approved books should be returned
    result.All(b =&gt; b.IsPublished).ShouldBeTrue();
    result.All(b =&gt; b.IsApproved).ShouldBeTrue();
}

[Fact]
public async Task Should_Return_All_Books_When_Filter_Disabled()
{
    var result = await WithUnitOfWorkAsync(async () =&gt;
    {
        // Disable the published filter to see unpublished books
        using (_publishedFilter.Disable())
        {
            var bookRepository = GetRequiredService&lt;IRepository&lt;Book, Guid&gt;&gt;();
            return await bookRepository.GetListAsync();
        }
    });

    // Should include unpublished books
    result.Any(b =&gt; b.Name == &quot;Unpublished Book&quot;).ShouldBeTrue();
}

[Fact]
public async Task Should_Combine_Filters_Correctly()
{
    // Test combining multiple filter disables
    using (_publishedFilter.Disable())
    using (_approvedFilter.Disable())
    {
        var bookRepository = GetRequiredService&lt;IRepository&lt;Book, Guid&gt;&gt;();
        var allBooks = await bookRepository.GetListAsync();
        
        // All books should be visible
        allBooks.Count.ShouldBe(5);
    }
}
</code></pre>
<blockquote>
<p><strong>Tip:</strong> When using ABP's test base, inject <code>IDataFilter&lt;IPublishedFilter&gt;</code> and <code>IDataFilter&lt;IApprovedFilter&gt;</code> to control filters in your tests.</p>
</blockquote>
<h2>Key Takeaways</h2>
<p>✅ <strong>Global query filters automatically apply filter criteria to all queries</strong>, reducing developer error and ensuring consistent data filtering across your application.</p>
<p>✅ <strong>ABP Framework provides a sophisticated data filtering system</strong> with built-in support for soft delete (<code>ISoftDelete</code>) and multi-tenancy (<code>IMultiTenant</code>), plus the ability to create custom filters.</p>
<p>✅ <strong>Use <code>IDataFilter&lt;TFilter&gt;</code> to control filters at runtime</strong>, enabling or disabling filters as needed for specific operations.</p>
<p>✅ <strong>To make custom filters toggleable, override <code>ShouldFilterEntity</code> and <code>CreateFilterExpression</code></strong> in your DbContext. Using only <code>HasAbpQueryFilter</code> creates filters that are always active.</p>
<p>✅ <strong>Combine multiple filters carefully</strong> and consider performance implications, especially with complex filter expressions.</p>
<p>✅ <strong>Leverage user-defined function (UDF) mapping</strong> for better SQL generation and query performance, available since ABP v8.3.</p>
<p>✅ <strong>Always test filter behavior</strong> to ensure filters work as expected in different scenarios, including edge cases.</p>
<h2>Conclusion</h2>
<p>Global query filters are essential for building secure, well-isolated applications. ABP Framework's data filtering system provides a robust foundation that builds on EF Core's capabilities while adding convenient features like runtime filter control and UDF mapping optimization.</p>
<p>By implementing multiple global query filters strategically, you can ensure data isolation, simplify your query logic, and reduce the risk of accidentally exposing unauthorized data. Remember to keep filters simple, add appropriate database indexes, and test thoroughly to maintain optimal performance.</p>
<p>Start implementing global query filters in your ABP applications today to leverage automatic data filtering across all your repositories and queries.</p>
<h3>See Also</h3>
<ul>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/data-filtering">ABP Data Filtering Documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/querying/filters">EF Core Global Query Filters</a></li>
<li><a href="https://abp.io/docs/latest/framework/fundamentals/multi-tenancy">ABP Multi-Tenancy Documentation</a></li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/data-filtering#using-user-defined-function-mapping-for-global-filters">Using User-defined function mapping for global filters</a></li>
</ul>
<hr />
<h2>References</h2>
<ul>
<li><a href="https://docs.abp.io">ABP Framework Documentation</a></li>
<li><a href="https://docs.microsoft.com/en-us/ef/core/">Entity Framework Core Documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/querying/filters">EF Core Global Query Filters</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping">User-defined Function Mapping</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1f68e6-4f92-0de8-a65f-8115ff438b58" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1f68e6-4f92-0de8-a65f-8115ff438b58" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/introducing-the-ai-management-module-nz9404a9</guid>
      <link>https://abp.io/community/posts/introducing-the-ai-management-module-nz9404a9</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>application-configuration</category>
      <category>abp</category>
      <category>module</category>
      <category>ai</category>
      <category>abpplatform</category>
      <title>Introducing the AI Management Module</title>
      <description>We are excited to announce the AI Management Module, a powerful new module to the ABP Platform that makes managing AI capabilities in your applications easier. No need to redeploy your application, now you can configure, test, and manage your AI integrations on the fly through an intuitive user interface!</description>
      <pubDate>Thu, 18 Dec 2025 17:18:59 Z</pubDate>
      <a10:updated>2026-09-26T09:24:08Z</a10:updated>
      <content:encoded><![CDATA[<h1>Introducing the AI Management Module: Manage AI Integration Dynamically</h1>
<p>We are excited to announce the <strong>AI Management Module</strong>, a powerful new module to the ABP Platform that makes managing AI capabilities in your applications easier. No need to redeploy your application, now you can configure, test, and manage your AI integrations on the fly through an intuitive user interface!</p>
<h2>What is the AI Management Module?</h2>
<p>Built on top of the <a href="https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence">ABP Framework's AI infrastructure</a>, the AI Management Module allows you to manage AI workspaces dynamically without touching your code. Whether you're building a customer support chatbot, adding AI-powered search, or creating intelligent automation workflows, this module provides everything you need to manage AI integrations through a user-friendly interface.</p>
<blockquote>
<p><strong>Note</strong>: The AI Management Module is currently in <strong>preview</strong> and available to ABP Team or higher license holders.</p>
</blockquote>
<h2>What it offers?</h2>
<h3>Manage AI Without Redeployment</h3>
<p>Create, configure, and update AI workspaces directly from the UI. Switch between different AI providers (OpenAI, Azure OpenAI, Ollama, etc.), change models, adjust prompts, and test configurations, all without restarting your application or deploying new code.</p>
<h3>Built-In Chat Interface</h3>
<p>Test your AI workspaces immediately with the included chat interface in playground pages. Verify your configurations work correctly before using them in production. Perfect for experimenting with different models, prompts, and settings.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-playground.png" alt="AI Management Playground" /></p>
<h3>Flexible for Any Architecture</h3>
<p>Whether you're building a monolith, microservices, or something in between, the module adapts to your needs:</p>
<ul>
<li>Host AI management directly in your application with full UI and database</li>
<li>Deploy a centralized AI service that multiple applications can consume</li>
<li>Use it as an API gateway pattern for your microservices</li>
</ul>
<h3>Works with Any AI Provider</h3>
<p>Even AI Management module doesn't implement all the providers by default, it provides extensibility options with a good abstraction for other providers like Azure, Anthropic Claude, Google Gemini, and more. Or you can directly use the OpenAI adapter with LLMs that support OpenAI API.</p>
<ul>
<li><p>Example of using Gemini as an OpenAI provider:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/aimanagement-workspace-geminiasopenai.png" alt="Using Gemini as an OpenAI provider" /></p>
</li>
</ul>
<p>You can even add your own custom AI providers: <a href="https://abp.io/docs/latest/modules/ai-management#implementing-custom-ai-provider-factories">learn how to implement a custom AI provider factory in the documentation</a>.</p>
<h3>Ready to Use Chat Widget</h3>
<p>Drop a compact, pre-built chat widget into any page with minimal code. It includes streaming support, conversation history, and API integration for customization.</p>
<ul>
<li><p>Simple to use with minimal code</p>
<pre><code class="language-cs">@await Component.InvokeAsync(typeof(ChatClientChatViewComponent), new ChatClientChatViewModel
{
    WorkspaceName = &quot;StoryTeller&quot;,
})
</code></pre>
</li>
<li><p>And result is a working, pre-integrated widget</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/ai-management-workspace-widget.png" alt="AI Management Chat Widget" /></p>
</li>
<li><p><a href="https://abp.io/docs/latest/modules/ai-management#client-usage-mvc-ui">See the widget documentation</a> for details and all parameters for customization.</p>
</li>
</ul>
<h3>Security</h3>
<p>Control who can manage and use AI workspaces with permission-based access control. Isolate your AI configurations by using workspaces with different permissions. Also, resource based authorization on workspaces is on the way and will be available in the next versions. It'll allow you to manage access to specific workspaces by a user or role.</p>
<h2>Getting Started</h2>
<p>Installation is straightforward using the <a href="https://abp.io/studio">ABP Studio</a>. You can just enable <strong>AI Management</strong> module while creating a new project with ABP Studio and configure your preferred AI provider and model in the solution creation wizard.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-12-18-Announcement-AIMAnagement/images/abp-studio-ai-management.png" alt="ABP Studio AI Management Solution Creation Wizard" /></p>
<h2>Roadmap</h2>
<h3>v10.0 ✅</h3>
<ul>
<li>Workspace Management</li>
<li>MVC UI</li>
<li>Playground
<ul>
<li>Chat History <em>(Client-Side)</em></li>
</ul>
</li>
<li>Client Components</li>
<li>Integration to Startup Templates</li>
</ul>
<h3>v10.1 ✅</h3>
<ul>
<li>Blazor UI</li>
<li>Angular UI</li>
<li>Resource based authorization on Workspaces</li>
<li>Agent-Framework compatibility examples</li>
</ul>
<h3>Future Goals</h3>
<ul>
<li>Microservice templates</li>
<li>MCP Support</li>
<li>RAG with file upload <em>(md, pdf, txt)</em></li>
<li>Chat History <em>(Server-Side Conversations)</em></li>
<li>OpenAI Compatible Endpoints</li>
<li>Tenant-Based Configuration</li>
<li>Extended RAG capabilities, <em>(ie. providing application data as tools)</em></li>
</ul>
<h2>Ready to Get Started?</h2>
<p>The AI Management Module is available now for ABP Team and higher license holders.</p>
<p><strong>Learn More:</strong></p>
<ul>
<li><a href="https://abp.io/docs/latest/modules/ai-management">AI Management Module Documentation</a> - All features, scenarios, and technical details.</li>
<li><a href="https://abp.io/docs/latest/framework/infrastructure/artificial-intelligence">AI Infrastructure Documentation</a> - Understanding AI workspaces in the framework.</li>
<li><a href="https://abp.io/docs/latest/modules/ai-management#usage-scenarios">Usage Scenarios</a> - Examples for different architectures.</li>
</ul>
<hr />
<p><em>The AI Management Module is currently in preview. We're excited to hear your feedback as we continue to improve and add new features!</em></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1e44a5-b224-3390-2b06-acd1dfc5eb6c" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1e44a5-b224-3390-2b06-acd1dfc5eb6c" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp.io-platform-10.0-final-has-been-released-spknn925</guid>
      <link>https://abp.io/community/posts/abp.io-platform-10.0-final-has-been-released-spknn925</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>release</category>
      <category>abp-io</category>
      <category>abpframework</category>
      <category>abpplatform</category>
      <title>ABP.IO Platform 10.0 Final Has Been Released!</title>
      <description>We are glad to announce that ABP 10.0 stable version has been released today.</description>
      <pubDate>Wed, 19 Nov 2025 10:56:04 Z</pubDate>
      <a10:updated>2026-09-26T09:34:08Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP.IO Platform 10.0 Final Has Been Released!</h1>
<p>We are glad to announce that <a href="https://abp.io/">ABP</a> 10.0 stable version has been released today.</p>
<h2>What's New With Version 10.0?</h2>
<p>All the new features were explained in detail in the <a href="https://abp.io/community/announcements/announcing-abp-10-0-release-candidate-86lrnyox">10.0 RC Announcement Post</a>, so there is no need to review them again. You can check it out for more details.</p>
<h2>Getting Started with 10.0</h2>
<h3>How to Upgrade an Existing Solution</h3>
<p>You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained:</p>
<h3>Upgrading via ABP Studio</h3>
<p>If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See <a href="https://abp.io/docs/latest/studio/installation#upgrading">the documentation</a> for more info.</p>
<p>After upgrading the ABP Studio, then you can open your solution in the application, and simply click the <strong>Upgrade ABP Packages</strong> action button to instantly upgrade your solution:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-08-08%20v10_0_Release_Stable/upgrade-abp-packages.png" alt="" /></p>
<h3>Upgrading via ABP CLI</h3>
<p>Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version.</p>
<p>If you haven't installed it yet, you can run the following command:</p>
<pre><code class="language-bash">dotnet tool install -g Volo.Abp.Studio.Cli
</code></pre>
<p>Or to update the existing CLI, you can run the following command:</p>
<pre><code class="language-bash">dotnet tool update -g Volo.Abp.Studio.Cli
</code></pre>
<p>After installing/updating the ABP CLI, you can use the <a href="https://abp.io/docs/latest/CLI#update"><code>update</code> command</a> to update all the ABP related NuGet and NPM packages in your solution as follows:</p>
<pre><code class="language-bash">abp update
</code></pre>
<p>You can run this command in the root folder of your solution to update all ABP related packages.</p>
<h2>Migration Guides</h2>
<p>There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v9.x: <a href="https://abp.io/docs/10.0/release-info/migration-guides/abp-10-0">ABP Version 10.0 Migration Guide</a></p>
<h2>Community News</h2>
<h3>New ABP Community Articles</h3>
<p>As always, exciting articles have been contributed by the ABP community. I will highlight some of them here:</p>
<ul>
<li><a href="https://abp.io/community/members/alper">Alper Ebiçoğlu</a>
<ul>
<li><a href="https://abp.io/community/articles/optimize-your-dotnet-app-for-production-for-any-.net-app-wa24j28e">Optimize your .NET app for production Part 1</a></li>
<li><a href="https://abp.io/community/articles/optimize-your-dotnet-app-for-production-for-any-.net-app-2-78xgncpi">Optimize your .NET app for production Part 2</a></li>
<li><a href="https://abp.io/community/articles/return-code-vs-exceptions-which-one-is-better-1rwcu9yi">Return Code vs Exceptions: Which One is Better?</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/sumeyye.kurtulus">Sumeyye Kurtulus</a>
<ul>
<li><a href="https://abp.io/community/articles/building-scalable-angular-apps-with-reusable-ui-components-b9npiff3">Building Scalable Angular Apps with Reusable UI Components</a></li>
<li><a href="https://abp.io/community/articles/angular-library-linking-made-easy-paths-workspaces-and-5z2ate6e">Angular Library Linking Made Easy: Paths, Workspaces and Symlinks</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/erdem.caygor">erdem çaygör</a>
<ul>
<li><a href="https://abp.io/community/articles/building-dynamic-forms-in-angular-for-enterprise-6r3ewpxt">Building Dynamic Forms in Angular for Enterprise</a></li>
<li><a href="https://abp.io/community/articles/from-server-to-browser-angular-transferstate-explained-m99zf8oh">From Server to Browser: Angular TransferState Explained</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/mansur.besleney">Mansur Besleney</a>
<ul>
<li><a href="https://abp.io/community/articles/top-10-exception-handling-mistakes-in-net-jhm8wzvg">Top 10 Exception Handling Mistakes in .NET</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/berkansasmaz">Berkan Şaşmaz</a>
<ul>
<li><a href="https://abp.io/community/articles/how-to-dynamically-set-the-connection-string-in-ef-core-30k87fpj">How to Dynamically Set the Connection String in EF Core</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/oguzhan.agir">Oğuzhan Ağır</a>
<ul>
<li><a href="https://abp.io/community/articles/the-asp.net-core-dependency-injection-system-3vbsdhq8">The ASP.NET Core Dependency Injection System</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/selmankoc">Selman Koç</a>
<ul>
<li><a href="https://abp.io/community/articles/5-things-keep-in-mind-when-deploying-clustered-environment-i9byusnv">5 Things Keep in Mind When Deploying Clustered Environment</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/m.aliozkaya">Muhammet Ali ÖZKAYA</a>
<ul>
<li><a href="https://abp.io/community/articles/repository-pattern-in-asp.net-core-2dudlg3j">Repository Pattern in ASP.NET Core</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/armagan">Armağan Ünlü</a>
<ul>
<li><a href="https://abp.io/community/articles/UI-UX-Trends-That-Will-Shape-2026-bx4c2kow">UI/UX Trends That Will Shape 2026</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/salih">Salih</a>
<ul>
<li><a href="https://abp.io/community/articles/what-is-that-domain-service-in-ddd-for-.net-developers-uqnpwjja">What is That Domain Service in DDD for .NET Developers?</a></li>
<li><a href="https://abp.io/community/articles/building-an-api-key-management-system-with-abp-framework-28gn4efw">Building an API Key Management System with ABP Framework</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/fahrigedik">Fahri Gedik</a>
<ul>
<li><a href="https://abp.io/community/articles/signal-based-forms-in-angular-21-9qentsqs">Signal-Based Forms in Angular</a></li>
</ul>
</li>
</ul>
<p>Thanks to the ABP Community for all the content they have published. You can also <a href="https://abp.io/community/posts/create">post your ABP related (text or video) content</a> to the ABP Community.</p>
<h2>About the Next Version</h2>
<p>The next feature version will be 10.1. You can follow the <a href="https://github.com/abpframework/abp/milestones">release planning here</a>. Please <a href="https://github.com/abpframework/abp/issues/new">submit an issue</a> if you have any problems with this version.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1dadee-b260-baef-18b4-637146bdc9ed" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1dadee-b260-baef-18b4-637146bdc9ed" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/you-may-have-trouble-with-guids-generating-sequential-guids-in-.net-xx4a3mc6</guid>
      <link>https://abp.io/community/posts/you-may-have-trouble-with-guids-generating-sequential-guids-in-.net-xx4a3mc6</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>sql</category>
      <category>entity-framework-core</category>
      <category>dotnet</category>
      <title>You May Have Trouble with GUIDs: Generating Sequential GUIDs in .NET</title>
      <description>If you’ve ever shoved a bunch of Guid.NewGuid() values into a SQL Server table with a clustered index on the PK, you’ve probably felt the pain: Index fragmentation so bad you could use it as modern art. Inserts slow down, page splits go wild, and your DBA starts sending you passive-aggressive Slack messages.</description>
      <pubDate>Fri, 03 Oct 2025 13:46:46 Z</pubDate>
      <a10:updated>2026-09-26T09:29:35Z</a10:updated>
      <content:encoded><![CDATA[<h1>You May Have Trouble with GUIDs: Generating Sequential GUIDs in .NET</h1>
<p>If you’ve ever shoved a bunch of <code>Guid.NewGuid()</code> values into a SQL Server table with a clustered index on the PK, you’ve probably felt the pain: <strong>Index fragmentation so bad you could use it as modern art.</strong> Inserts slow down, page splits go wild, and your DBA starts sending you passive-aggressive Slack messages.</p>
<p>And yet… we keep doing it. Why? Because GUIDs are <em>easy</em>. They’re globally unique, they don’t need a round trip to the DB, and they make distributed systems happy. But here’s the catch: <strong>random GUIDs are absolute chaos for ordered indexes</strong>.</p>
<h2>The Problem with Vanilla GUIDs</h2>
<ul>
<li><p><strong>Randomness kills order</strong> — clustered indexes thrive on sequential inserts; random GUIDs force constant reordering.</p>
</li>
<li><p><strong>Performance hit</strong> — every insert can trigger page splits and index reshuffling.</p>
</li>
<li><p><strong>Storage bloat</strong> — fragmentation means wasted space and slower reads.</p>
</li>
</ul>
<p>Sure, you could switch to int or long identity columns, but then you lose the distributed generation magic and security benefits (predictable IDs are guessable).</p>
<h2>Sequential GUIDs to the Rescue</h2>
<p>Sequential GUIDs keep the uniqueness but add a predictable ordering component, usually by embedding a timestamp in part of the GUID. This means:</p>
<ul>
<li><p>Inserts happen at the “end” of the index, not all over the place.</p>
</li>
<li><p>Fragmentation drops dramatically.</p>
</li>
<li><p>You still get globally unique IDs without DB trips.</p>
</li>
</ul>
<p>Think of it as <strong>GUIDs with manners</strong>.</p>
<h2>ABP Framework’s Secret Sauce</h2>
<p>Here’s where ABP Framework flexes: it <strong>uses sequential GUIDs by default</strong> for entity IDs. No ceremony, no “remember to call this helper method”, it’s baked in.</p>
<p>Under the hood:</p>
<ul>
<li><p>ABP ships with IGuidGenerator (default: SequentialGuidGenerator).</p>
</li>
<li><p>It picks the right sequential strategy for your DB provider:</p>
<ul>
<li><p><strong>SequentialAtEnd</strong> → SQL Server</p>
</li>
<li><p><strong>SequentialAsString</strong> → MySQL/PostgreSQL</p>
</li>
<li><p><strong>SequentialAsBinary</strong> → Oracle</p>
</li>
</ul>
</li>
<li><p>EF Core integration packages auto-configure this, so you rarely need to touch it.</p>
</li>
</ul>
<p>Example in ABP:</p>
<pre><code class="language-csharp">public class MyProductService : ITransientDependency
{
    private readonly IRepository&lt;Product, Guid&gt; _productRepository;
    private readonly IGuidGenerator _guidGenerator;


    public MyProductService(
        IRepository&lt;Product, Guid&gt; productRepository,
        IGuidGenerator guidGenerator)
    {
        _productRepository = productRepository;
        _guidGenerator = guidGenerator;
    }


    public async Task CreateAsync(string productName)
    {
        var product = new Product(_guidGenerator.Create(), productName);
        await _productRepository.InsertAsync(product);
    }
}
</code></pre>
<p>No <code>Guid.NewGuid()</code> here, <code>_guidGenerator.Create()</code> gives you a sequential GUID every time.</p>
<h2>Benefits of Sequential GUIDs</h2>
<p>Let’s say you’re inserting 1M rows into a table with a clustered primary key:</p>
<ul>
<li><p><strong>Random GUIDs</strong> → fragmentation ~99%, insert throughput tanks.</p>
</li>
<li><p><strong>Sequential GUIDs</strong> → fragmentation stays low, inserts fly.</p>
</li>
</ul>
<p>In high-volume systems, this difference is <strong>not</strong> academic, it’s the difference between smooth scaling and spending weekends rebuilding indexes.</p>
<h2>When to Use Sequential GUIDs</h2>
<ul>
<li><p><strong>Distributed systems</strong> that still want DB-friendly inserts.</p>
</li>
<li><p><strong>High-write workloads</strong> with clustered indexes on GUID PKs.</p>
</li>
<li><p><strong>Multi-tenant apps</strong> where IDs need to be unique across tenants.</p>
</li>
</ul>
<h2>When Random GUIDs Still Make Sense</h2>
<ul>
<li><p>Security through obscurity, if you don’t want IDs to hint at creation order.</p>
</li>
<li><p>Non-indexed identifiers, fragmentation isn’t a concern.</p>
</li>
</ul>
<h2>The Final Take</h2>
<p>ABP’s default sequential GUID generation is one of those “<strong>small but huge</strong>” features. It’s the kind of thing you don’t notice until you benchmark, and then you wonder why you ever lived without it.</p>
<h2>Links</h2>
<p>You may want to check the following references to learn more about sequential GUIDs:</p>
<ul>
<li><a href="https://docs.abp.io/en/abp/latest/Guid-Generation">ABP Framework Documentation: Sequential GUIDs</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1cbc80-1749-58d8-cd3a-eeb77cac2799" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1cbc80-1749-58d8-cd3a-eeb77cac2799" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/abp-platform-10.0-rc-has-been-released-86lrnyox</guid>
      <link>https://abp.io/community/posts/abp-platform-10.0-rc-has-been-released-86lrnyox</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>release</category>
      <title>ABP Platform 10.0 RC Has Been Released</title>
      <description>We are happy to release ABP version 10.0 RC (Release Candidate). This blog post introduces the new features and important changes in this new version.</description>
      <pubDate>Fri, 03 Oct 2025 07:45:11 Z</pubDate>
      <a10:updated>2026-09-26T09:29:29Z</a10:updated>
      <content:encoded><![CDATA[<h1>ABP Platform 10.0 RC Has Been Released</h1>
<p>We are happy to release <a href="https://abp.io">ABP</a> version <strong>10.0 RC</strong> (Release Candidate). This blog post introduces the new features and important changes in this new version.</p>
<p>Try this version and provide feedback for a more stable version of ABP v10.0! Thanks to you in advance.</p>
<h2>Get Started with the 10.0 RC</h2>
<p>You can check the <a href="https://abp.io/get-started">Get Started page</a> to see how to get started with ABP. You can either download <a href="https://abp.io/get-started#abp-studio-tab">ABP Studio</a> (<strong>recommended</strong>, if you prefer a user-friendly GUI application - desktop application) or use the <a href="https://abp.io/docs/latest/cli">ABP CLI</a>.</p>
<p>By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-10-02%20v10_0_Preview/studio-switch-to-preview.png" alt="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-10-02%20v10_0_Preview/studio-switch-to-preview.png" /></p>
<h2>Migration Guide</h2>
<p>There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v9.3 or earlier: <a href="https://abp.io/docs/10.0/release-info/migration-guides/abp-10-0">ABP Version 10.0 Migration Guide</a>.</p>
<h2>What's New with ABP v10.0?</h2>
<p>In this section, I will introduce some major features released in this version.
Here is a brief list of titles explained in the next sections:</p>
<ul>
<li>Upgraded to .NET 10.0</li>
<li>Upgraded to Blazorise 1.8.2</li>
<li>New Module: <strong>Workflow (Elsa)</strong></li>
<li>New Object Mapper: <strong>Mapperly</strong></li>
<li>Localization: Nested object support in JSON files</li>
<li>Support EF Core Shared Entity Types on Repositories</li>
<li>Add failure retry policy to InboxProcessor</li>
<li>Migrate to New Esbuild-based Angular Builder</li>
<li>Angular SSR support</li>
</ul>
<h3>Upgraded to .NET 10.0</h3>
<p>We've upgraded ABP to .NET 10.0, so you need to move your solutions to .NET 10.0 if you want to use ABP 10.0.</p>
<blockquote>
<p>Since the stable version of .NET 10 hasn't been released yet, we upgraded ABP to .NET v10.0-rc.1. Stable NET 10 is scheduled to launch as a <strong>Long-Term Support (LTS)</strong> release during .NET Conf 2025, which takes place November 11-13, 2025. We'll update the ABP Platform to the .NET 10 as soon as possible official .NET 10 release is completed.</p>
</blockquote>
<h3>Upgraded to Blazorise v1.8.2</h3>
<p>Upgraded the <a href="https://blazorise.com/">Blazorise</a> library to v1.8.2 for Blazor UI. If you are upgrading your project to v10.0 RC, please ensure that all the Blazorise-related packages are using v1.8.2 in your application. Otherwise, you might get errors due to incompatible versions.</p>
<blockquote>
<p>See <a href="https://github.com/abpframework/abp/issues/23717">#23717</a> for the updated NuGet packages.</p>
</blockquote>
<h3>New Module: <strong>Workflow (Elsa)</strong></h3>
<p>ABP now ships a Workflow module that integrates <a href="https://github.com/elsa-workflows/elsa-core">Elsa Workflows</a> to build visual, long-running, event-driven workflows in your ABP solutions (monolith or microservices). It provides seamless integration with ABP authentication/authorization, distributed event bus, persistence, background processing and includes support for hybrid UIs via Elsa Studio.</p>
<p>For a hands-on reference showcasing an end-to-end order/payment workflow across services, see the sample: <a href="https://abp.io/docs/10.0/samples/elsa-workflows-demo">Elsa Workflows - Sample Workflow Demo</a>. For capabilities, installation and configuration details (activities, storage, hosting, dashboard), see the module docs: <a href="https://abp.io/docs/10.0/modules/elsa-pro">Workflow (Elsa) module</a>.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-10-02%20v10_0_Preview/elsa-workflow-instances.png" alt="Workflow (Elsa) module" /></p>
<h3>New Object Mapper: <strong>Mapperly</strong></h3>
<p>ABP modules now use Mapperly as the default object-to-object mapper. Mapperly is a compile-time, source generator–based mapper that removes runtime reflection and offers better performance with simpler maintenance. For background and implementation details, see the planning issue and the PR: <a href="https://github.com/abpframework/abp/issues/23243">Switch to another object mapping library</a> and <a href="https://github.com/abpframework/abp/pull/23277">Use Mapperly to replace AutoMapper in all modules</a>.</p>
<p>The <code>Volo.Abp.AutoMapper</code> package remains available for backward compatibility. You can keep using AutoMapper in your solutions, but you are responsible for obtaining and managing its license if needed. For upgrade guidance and practical steps, follow the migration guide: <a href="https://abp.io/docs/10.0/release-info/migration-guides/AutoMapper-To-Mapperly">AutoMapper to Mapperly</a>.</p>
<h3>Localization: Nested object support in JSON files</h3>
<p>ABP now supports nested objects (and arrays) in JSON localization files, allowing you to organize translations hierarchically and access them using the double underscore (<code>__</code>) separator. This improves maintainability for larger resource files and aligns lookups with familiar key paths.</p>
<blockquote>
<p>See the PR for details: <a href="https://github.com/abpframework/abp/pull/23701">feat(l8n): add support for nested objects in localization files</a>.</p>
</blockquote>
<p><strong>Declaration (nested objects)</strong>:</p>
<pre><code class="language-json">{
  &quot;culture&quot;: &quot;en&quot;,
  &quot;texts&quot;: {
    &quot;MyNestedTranslation&quot;: {
      &quot;SomeKey&quot;: &quot;Some nested value&quot;,
      &quot;SomeOtherKey&quot;: &quot;Some other nested value&quot;
    }
  }
}
</code></pre>
<p><strong>Usage</strong>:</p>
<pre><code class="language-csharp">L[&quot;MyNestedTranslation__SomeKey&quot;];
L[&quot;MyNestedTranslation__SomeOtherKey&quot;];
</code></pre>
<p><strong>Declaration (arrays)</strong>:</p>
<pre><code class="language-json">{
  &quot;culture&quot;: &quot;en&quot;,
  &quot;texts&quot;: {
    &quot;Menu&quot;: {
      &quot;Items&quot;: [&quot;Home&quot;, &quot;About&quot;, &quot;Contact&quot;]
    }
  }
}
</code></pre>
<p><strong>Usage</strong>:</p>
<pre><code class="language-csharp">L[&quot;Menu__Items__0&quot;]; // Home
L[&quot;Menu__Items__2&quot;]; // Contact
</code></pre>
<h3>Support EF Core Shared Entity Types on Repositories</h3>
<p>ABP repositories now support EF Core <strong>shared-type entity</strong> types by allowing a custom entity name to be set on a repository before performing operations. Internally, this uses EF Core's <code>DbContext.Set&lt;T&gt;(string name)</code> to target the correct <code>DbSet</code>/table for the same CLR type, enabling scenarios like per-tenant tables, archives, or partitioning, and you can switch the target at runtime. See the PR: <a href="https://github.com/abpframework/abp/pull/23588">Support EF Core Shared Entity Types on Repositories</a> and the EF Core documentation on <a href="https://learn.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=data-annotations#shared-type-entity-types">shared-type entity types</a>.</p>
<p><strong>Example</strong>:</p>
<pre><code class="language-csharp">// Set the shared entity name so repository operations target that table
var repo = serviceProvider.GetRequiredService&lt;IRepository&lt;MyEntity, Guid&gt;&gt;();
repo.SetCustomEntityName(&quot;MyEntity_TenantA&quot;);
var list = await repo.GetListAsync();

// Switch to another shared name later on the same instance
repo.SetCustomEntityName(&quot;MyEntity_Archive&quot;);
await repo.InsertAsync(new MyEntity { /* ... */ });
</code></pre>
<h3>Add failure retry policy to InboxProcessor</h3>
<p><code>InboxProcessor</code> now supports configurable failure handling strategies per event: <strong>Retry</strong> (default; reprocess in the next cycle), <strong>RetryLater</strong> (skip the failing event and retry it later with exponential backoff; the backoff factor and maximum retries are configurable), and <strong>Discard</strong> (drop the failing event). This prevents a single failing handler from blocking subsequent events and improves resiliency.</p>
<blockquote>
<p><strong>Note</strong>: This is a breaking change because <code>IncomingEvent</code> entity properties were updated. See the PR for details: <a href="https://github.com/abpframework/abp/pull/23563">Add failure retry policy to InboxProcessor</a>.</p>
</blockquote>
<h3>Migrate to New Esbuild-based Angular Builder</h3>
<p>We've migrated ABP Angular templates and packages to Angular's new esbuild-based build system (introduced in Angular 17+ and fully supported in Angular 20) to deliver faster builds, modern ESM support, built-in SSR/prerender capabilities, and a better development experience. This change is non-breaking for existing apps. See the tracking issue and PR: <a href="https://github.com/abpframework/abp/issues/23242">Angular - Migrate to New Esbuild-based Angular Builder</a>, <a href="https://github.com/abpframework/abp/pull/23363">feat: Update Angular templates to Angular 20 new build system</a>.</p>
<p><strong>Key updates in templates/config</strong>:</p>
<ul>
<li>Builder switched from <code>@angular-devkit/build-angular:browser</code> to <code>@angular-devkit/build-angular:application</code>.</li>
<li><code>main</code> option replaced by <code>browser</code>; <code>polyfills</code> moved to array form.</li>
<li>TypeScript updated to <code>es2020</code> with <code>esModuleInterop: true</code>; module target <code>esnext</code>.</li>
</ul>
<p><strong>More Angular updates</strong>:</p>
<ul>
<li>Unit tests have been updated for the new builder and configuration: <a href="https://github.com/abpframework/abp/pull/23460">#23460</a>.</li>
</ul>
<p><strong>Warnings</strong>:</p>
<ul>
<li>Constructor injections migrated to Angular's <code>inject()</code> function. If you extend a class and previously called <code>super(...)</code> with injected params, remove those parameters. See: <a href="https://angular.dev/reference/migrations/inject-function">Angular inject() migration</a>.</li>
<li><code>provideLogo</code> and <code>withEnvironmentOptions</code> have moved from LeptonX packages to <code>@abp/ng.theme-shared</code>.</li>
<li>If you use the new application builder and have <code>tsconfig.json</code> path mappings that point into <code>node_modules</code>, remove those mappings and prefer symlinks instead. See a symlink reference: <a href="https://hostman.com/tutorials/creating-symbolic-links-in-linux/">Creating symbolic links</a>.</li>
</ul>
<h3>Angular SSR support</h3>
<p>ABP Angular templates now support Server-Side Rendering (SSR) with the Angular Application Builder, enabling hybrid rendering (SSR + CSR) for improved first paint, SEO and perceived performance. This includes SSR-safe platform checks (no direct <code>window</code>/<code>location</code>/<code>localStorage</code>), OIDC auth compatibility via cookie-backed storage, and <code>TransferState</code> to prevent duplicate HTTP GETs during hydration. For implementation highlights and usage (including how to run the SSR dev server and the <code>transferStateInterceptor</code>), see the issue and PR: <a href="https://github.com/abpframework/abp/issues/23055">Angular SSR</a>, <a href="https://github.com/abpframework/abp/pull/23416">Hybrid Rendering &amp; Application Builder</a>.</p>
<blockquote>
<p>See Angular's official guide for details on hybrid rendering (prerender + SSR + CSR): <a href="https://angular.dev/guide/ssr">Angular SSR</a> and on the builder migration: <a href="https://angular.dev/tools/cli/build-system-migration">Angular build system migration</a>.</p>
</blockquote>
<h2>Community News</h2>
<h3>Recent Events</h3>
<p>We recently hosted two sessions of ABP Community Talks:</p>
<h4>Community Talks 2025.06: Microservices with ABP Template</h4>
<p>The Easiest Way to Get Started with Microservices on .NET Using ABP Microservice Solution Template: a deep dive into ABP’s microservice template, showing how ABP Studio streamlines creating, running, and scaling distributed systems. See the event page: <a href="https://abp.io/community/events/community-talks/the-easiest-way-to-get-started-with-microservices-on-.net-using-abp-microservice-solution-template-fd2comfn">Community Talks: Microservices with ABP Template</a>.</p>
<img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-10-02%20v10_0_Preview/community-talk-2025-06.png" alt="ABP Community Talks: Microservices with ABP Template" width="360">
<h4>Community Talks 2025.07: Developer-Friendly CMS for .NET</h4>
<p>Beyond WordPress: A Developer-Friendly CMS for .NET: an overview of building custom web apps with ABP CMS Kit, integrating content management with your application code. Learn more: <a href="https://abp.io/community/events/community-talks/beyond-wordpress-a-developerfriendly-cms-for-.net-mubtips6">Community Talks: Developer-Friendly CMS for .NET</a>.</p>
<img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-10-02%20v10_0_Preview/community-talk-2025-07.png" alt="ABP Community Talks: Developer-Friendly CMS for .NET" width="360">
<h3>Weekly Webinar: Discover ABP Platform</h3>
<p>We’ve started a <strong>weekly live webinar series</strong> designed for developers who want to get the most out of the <strong>ABP Platform</strong>. This event is designed for those new to ABP to help you understand its core features, capabilities, and licensing models.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/rel-10.0/docs/en/Blog-Posts/2025-10-02%20v10_0_Preview/abp-webinar.png" alt="ABP Weekly Webinar - Discover ABP Platform" /></p>
<p>Every webinar features live coding demos, practical examples, and an open Q&amp;A segment where you can get your questions answered directly by the ABP team. Whether you’re just starting with ABP or looking to explore advanced scenarios, these sessions will help you build better apps faster.</p>
<p><a href="https://abp.io/webinars/discover-abp-platform">👉 Register here to join an upcoming session!</a></p>
<h3>New ABP Community Articles</h3>
<p>There are exciting articles contributed by the ABP community as always. I will highlight some of them here:</p>
<ul>
<li><a href="https://abp.io/community/members/alper">Alper Ebiçoğlu</a>:
<ul>
<li><a href="https://abp.io/community/articles/high-performance-net-libraries-you-did-not-know-nu5t88sz">High-Performance .NET Libraries You Didn’t Know You Needed</a></li>
<li><a href="https://abp.io/community/articles/net-10-preview-features-breaking-changes-enhancements-xennnnky">.NET 10: What You Need to Know (LTS Release, Coming November 2025)</a></li>
<li><a href="https://abp.io/community/articles/best-free-alternatives-to-automapper-in-net-l9f5ii8s">Best Free Alternatives to AutoMapper in .NET — Why We Moved to Mapperly</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/maliming">Liming Ma</a>:
<ul>
<li><a href="https://abp.io/community/articles/keep-track-of-your-users-in-an-asp.net-core-application-jlt1fxvb">Keep Track of Your Users in an ASP.NET Core Application</a></li>
<li><a href="https://abp.io/community/articles/app-services-vs-domain-services-4dvau41u">App Services vs Domain Services</a></li>
<li><a href="https://abp.io/community/articles/using-hangfire-dashboard-in-abp-api-website--r32ox497">Using Hangfire Dashboard in ABP API Website</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/fahrigedik">Fahri Gedik</a>:
<ul>
<li><a href="https://abp.io/community/articles/backward-compatible-rest-apis-dotnet-microservices-9rzlb4q6">Backward‑Compatible REST APIs in .NET Microservices</a></li>
<li><a href="https://abp.io/community/articles/best-practices-for-designing-backward%E2%80%91compatible-rest-apis-in-a-microservice-solution-for-.net-developers-t1m4kzfa">Best Practices for Designing Backward‑Compatible REST APIs in a Microservice Solution for .NET Developers</a></li>
<li><a href="https://abp.io/community/articles/stepbystep-aws-secrets-manager-integration-in-abp-3dcblyix">Stepbystep AWS Secrets Manager Integration in ABP</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/engincanv">Engincan Veske</a>:
<ul>
<li><a href="https://abp.io/community/articles/building-a-permission-based-authorization-system-for-asp-net-owyszy0b">Building a Permission-Based Authorization System for ASP.NET Core</a></li>
<li><a href="https://abp.io/community/articles/where-and-how-to-store-your-blob-objects-in-dotnet-r2r1vjjd">Where and How to Store Your Blob Objects in .NET</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/salih">Salih Özkara</a>:
<ul>
<li><a href="https://abp.io/community/articles/truly-layering-a-net-application-based-on-ddd-principles-428jhn3a">Truly Layering a .NET Application Based on DDD Principles</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/kfrancis@clinicalsupportsystems.com">Kori Francis</a>:
<ul>
<li><a href="https://abp.io/community/articles/abp-postmark-email-integration-templated-emails-gvgc6pfj">ABP Postmark Email Integration, Templated Emails</a></li>
<li><a href="https://abp.io/community/articles/universal-redis-configuration-abp-aspire-deployment-qp90c7u4">Universal Redis Configuration in ABP Aspire Deployment</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/connect">Sajankumar Vijayan</a>:
<ul>
<li><a href="https://abp.io/community/articles/multi-tenant%20SaaS%20apps,%20Cloudflare%20DNs-dar977al">Multi-tenant SaaS apps, Cloudflare DNS</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/selmankoc">Selman Koc</a>:
<ul>
<li><a href="https://abp.io/community/articles/Azure%20DevOps,%20CI%2FCD%20pipelines,%20Azure%20DevOps%20best%20practices,-wiguy1ew">Azure DevOps, CI/CD pipelines, Azure DevOps best practices</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/sumeyye.kurtulus">Sümeyye Kurtuluş</a>:
<ul>
<li><a href="https://abp.io/community/articles/abp-now-supports-angular-standalone-applications-zzi2rr2z">ABP Now Supports Angular Standalone Applications</a></li>
<li><a href="https://abp.io/community/articles/supercharge-your-angular-app-a-developers-guide-to-unlock-0dmu7tkr">Supercharge your Angular app: A developer's guide to unlock</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/mansur.besleney">Mansur Besleney</a>:
<ul>
<li><a href="https://abp.io/community/articles/demystified-aggregates-in-ddd-and-dotnet-2becl93q">Demystified Aggregates in DDD &amp; .NET: From Theory to Practice</a></li>
<li><a href="https://abp.io/community/articles/how-to-build-persistent-background-jobs-with-abp-framework-n9aloh93">How to Build Persistent Background Jobs with ABP Framework and Quartz</a></li>
<li><a href="https://abp.io/community/articles/angular-application-builder-transitioning-from-webpack-to-3yzhzfl0">Angular Application Builder: Transitioning from Webpack to Esbuild</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/m.aliozkaya">Muhammet Ali Özkaya</a>:
<ul>
<li><a href="https://abp.io/community/articles/implementing-unit-of-work-with-asp.net-core-lv4v2tyf">Implementing Unit of Work with ASP.NET Core</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/enisn">Enis Necipoğlu</a>:
<ul>
<li><a href="https://abp.io/community/articles/integration-services-explained-what-they-are-when-to-use-lienmsy8">Integration Services Explained: What They Are &amp; When to Use</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/berkansasmaz">Berkan Şaşmaz</a>:
<ul>
<li><a href="https://abp.io/community/articles/how-to-dynamically-set-the-connection-string-in-ef-core-30k87fpj">How to Dynamically Set the Connection String in EF Core</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/emre.kara">Emre Kara</a>:
<ul>
<li><a href="https://abp.io/community/articles/a-developers-guide-to-distributed-event-buses-in-.net-oehl23kb">A Developer's Guide to Distributed Event Buses in .NET</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/oguzhan.agir">Oğuzhan Ağır</a>:
<ul>
<li><a href="https://abp.io/community/articles/in-memory-background-job-queue-aspnet-core-pai2zmtr">In-Memory Background Job Queue in ASP.NET Core</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/alperen.samurlu">Alperen Samurlu</a>:
<ul>
<li><a href="https://abp.io/community/articles/how-can-we-apply-the-dry-principle-in-a-better-way-pmc4eao2">How Can We Apply the DRY Principle in a Better Way?</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/ahmet.celik">Ahmet Çelik</a>:
<ul>
<li><a href="https://abp.io/community/articles/best-practices-guide-for-rest-api-design-oexc1euj">Best Practices Guide for REST API Design</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/s.elanuroguz">Elanur Oğuz</a>:
<ul>
<li><a href="https://abp.io/community/articles/web-design-basics-for-graphic-designers-who-dont-code-0c2jgt2v">Web Design Basics for Graphic Designers Who Don't Code</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/seda.sen">Seda Şen</a>:
<ul>
<li><a href="https://abp.io/community/articles/color-psychology-in-web-design-z383jph8">Color Psychology in Web Design</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/halimekarayay">Halime Karayay</a>:
<ul>
<li><a href="https://abp.io/community/articles/10-modern-html-css-techniques-every-designer-should-know-zxnwilf4">10 Modern HTML CSS Techniques Every Designer Should Know</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/antosubash">Anto Subash</a>:
<ul>
<li><a href="https://abp.io/community/articles/abp-react-cms-module-building-dynamic-pages-with-puck-auxvrwgf">ABP React CMS Module: Building Dynamic Pages with Puck</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/yagmur.celik">Yağmur Çelik</a>:
<ul>
<li><a href="https://abp.io/community/articles/integration-testing-best-practices-for-building-a-robust-udcwef71">Integration Testing Best Practices for Building a Robust API</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/suhaib-mousa">Suhaib Mousa</a>:
<ul>
<li><a href="https://abp.io/community/articles/visual-studio-2026-e4s5hed7">Visual Studio 2026 - What's New and Why I'm Excited About It</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/alex.maiereanu@3sstudio.com">Alex Maiereanu</a>:
<ul>
<li><a href="https://abp.io/community/articles/abphangfireazurepostgresql-s1jnf3yg">ABP-Hangfire-AzurePostgreSQL</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/jfistelmann">Jack Fistelmann</a>:
<ul>
<li><a href="https://abp.io/community/articles/abp-and-maildev-gy13cr1p">ABP and maildev</a></li>
</ul>
</li>
<li>Tarık Özdemir:
<ul>
<li><a href="https://abp.io/community/articles/AI-First%20Architecture%20for%20.NET%20Projects%3A%20A%20Modern%20Blueprint-h2wgcoq3">AI-First Architecture for .NET Projects: A Modern Blueprint</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/prabhjot">Prabhjot Singh</a>:
<ul>
<li><a href="https://abp.io/community/articles/switching-from-project-references-to-package-references-ql16qwx0">Switching from Project References to Package References</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/yekalkan">Yunus Emre Kalkan</a>:
<ul>
<li><a href="https://abp.io/community/articles/abp-studio-docker-container-management-ex7r27y8">New in ABP Studio: Docker Container Management</a></li>
</ul>
</li>
<li><a href="https://abp.io/community/members/burakdemir">Burak Demir</a>:
<ul>
<li><a href="https://abp.io/community/articles/solving-mongodb-guid-issues-after-an-abp-framework-upgrade-tv8waw1n">Solving MongoDB GUID Issues After an ABP Framework Upgrade</a></li>
</ul>
</li>
</ul>
<p>Thanks to the ABP Community for all the content they have published. You can also <a href="https://abp.io/community/posts/create">post your ABP-related (text or video) content</a> to the ABP Community.</p>
<h2>Conclusion</h2>
<p>This version comes with some new features and a lot of enhancements to the existing features. You can see the <a href="https://abp.io/docs/10.0/release-info/road-map">Road Map</a> documentation to learn about the release schedule and planned features for the next releases. Please try ABP v10.0 RC and provide feedback to help us release a more stable version.</p>
<p>Thanks for being a part of this community!</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1cbb35-0b64-2f43-01ba-c749757a13a5" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1cbb35-0b64-2f43-01ba-c749757a13a5" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/integration-services-explained-what-they-are-when-to-use-them-and-how-they-behave-lienmsy8</guid>
      <link>https://abp.io/community/posts/integration-services-explained-what-they-are-when-to-use-them-and-how-they-behave-lienmsy8</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <title>Integration Services Explained — What they are, when to use them, and how they behave</title>
      <description>Discover how ABP’s hidden superpower can make your systems talk to each other faster, safer, and smarter than ever. This guide unveils a feature many overlook—yet it can transform both monoliths and microservices. If you want to unlock smoother internal communication and future‑proof your architecture, this is a must‑read</description>
      <pubDate>Wed, 13 Aug 2025 07:26:54 Z</pubDate>
      <a10:updated>2026-09-26T09:30:21Z</a10:updated>
      <content:encoded><![CDATA[<h1>Integration Services in ABP — What they are, when to use them, and how they behave 🚦</h1>
<p>If you’ve been building with ABP for a while, you’ve probably used Application Services for your UI and APIs in your .NET and ASP.NET Core apps. Integration Services are similar—but with a different mission: they exist for service-to-service or module-to-module communication, not for end users.</p>
<p>If you want the formal spec, see the official doc: <a href="https://github.com/abpframework/abp/blob/dev/docs/en/framework/api-development/integration-services.md">Integration Services</a>. This post is the practical, no-fluff guide.</p>
<h2>What is an Integration Service?</h2>
<p>An Integration Service is an application service or ASP.NET Core MVC controller marked with the <code>[IntegrationService]</code> attribute. That marker tells ABP “this endpoint is for internal communication.”</p>
<ul>
<li>They are not exposed by default (safer for reusable modules and monoliths).</li>
<li>When exposed, their route prefix is <code>/integration-api</code> (so you can easily protect them at your gateway or firewall).</li>
<li>Auditing is disabled by default for them (less noise for machine-to-machine calls).</li>
</ul>
<p>Quick look:</p>
<pre><code class="language-csharp">[IntegrationService]
public interface IProductIntegrationService : IApplicationService
{
    Task&lt;List&lt;ProductDto&gt;&gt; GetProductsByIdsAsync(List&lt;Guid&gt; ids);
}

public class ProductIntegrationService : ApplicationService, IProductIntegrationService
{
    public Task&lt;List&lt;ProductDto&gt;&gt; GetProductsByIdsAsync(List&lt;Guid&gt; ids)
    {
        // fetch and return minimal product info for other services/modules
    }
}
</code></pre>
<h2>Are they HTTP endpoints?</h2>
<ul>
<li>By default: no (they won’t be reachable over HTTP in the ASP.NET Core routing pipeline).</li>
<li>If you need them over HTTP (typically for microservices), explicitly enable:</li>
</ul>
<pre><code class="language-csharp">Configure&lt;AbpAspNetCoreMvcOptions&gt;(options =&gt;
{
    options.ExposeIntegrationServices = true;
});
</code></pre>
<p>Once exposed, ABP puts them under <code>/integration-api/...</code> instead of <code>/api/...</code> in the ASP.NET Core routing pipeline. That’s your hint to restrict them from public internet access.</p>
<h2>Enable auditing (optional)</h2>
<p>If you want audit logs for integration calls, enable it explicitly:</p>
<pre><code class="language-csharp">Configure&lt;AbpAuditingOptions&gt;(options =&gt;
{
    options.IsEnabledForIntegrationServices = true;
});
</code></pre>
<h2>When should you use Integration Services?</h2>
<ul>
<li>Internal, synchronous operations between services or modules.</li>
<li>You need a “thin” API designed for other services (not for UI): minimal DTOs, no view concerns, predictable contracts.</li>
<li>You want to hide these endpoints from public clients, or only allow them inside your private network or k8s cluster.</li>
<li>You’re packaging a reusable module that might be used in both monolith and microservice deployments.</li>
</ul>
<h2>When NOT to use them</h2>
<ul>
<li>Public APIs or anything intended for browsers/mobile apps → use regular application services/controllers.</li>
<li>Asynchronous cross-service workflows → consider domain events + outbox/inbox; use Integration Services for sync calls.</li>
<li>Complex, chatty UI endpoints → those belong to your external API surface, not internal integration.</li>
</ul>
<h2>Common use-cases and examples</h2>
<ul>
<li>Identity lookups across services: an Ordering service needs basic user info from the Identity service.</li>
<li>Permission checks from another module: a CMS module asks a Permission service for access decisions.</li>
<li>Product data hydrations: a Cart service needs minimal product details (price, name) from Catalog.</li>
<li>Internal admin/maintenance operations that aren’t meant for end users but are needed by other services.</li>
</ul>
<h2>Example: microservice-to-microservice call</h2>
<ol>
<li>Mark and expose the integration service in the target service:</li>
</ol>
<pre><code class="language-csharp">[IntegrationService]
public interface IUserIntegrationService : IApplicationService
{
    Task&lt;UserBriefDto?&gt; FindByIdAsync(Guid id);
}

Configure&lt;AbpAspNetCoreMvcOptions&gt;(o =&gt; o.ExposeIntegrationServices = true);
</code></pre>
<ol start="2">
<li>In the caller service, add an HTTP client proxy only for Integration Services if you like to keep things clean:</li>
</ol>
<pre><code class="language-csharp">services.AddHttpClientProxies(
    typeof(TargetServiceApplicationModule).Assembly,
    remoteServiceConfigurationName: &quot;TargetService&quot;,
    asDefaultServices: true,
    applicationServiceTypes: ApplicationServiceTypes.IntegrationServices);
</code></pre>
<ol start="3">
<li>Call it just like a local service (ABP’s HTTP proxy handles the wire):</li>
</ol>
<pre><code class="language-csharp">public class OrderAppService : ApplicationService
{
    private readonly IUserIntegrationService _userIntegrationService;

    public OrderAppService(IUserIntegrationService userIntegrationService)
    {
        _userIntegrationService = userIntegrationService;
    }

    public async Task PlaceOrderAsync(CreateOrderDto input)
    {
        var user = await _userIntegrationService.FindByIdAsync(CurrentUser.GetId());
        // validate user status, continue placing order...
    }
}
</code></pre>
<h2>Monolith vs. Microservices</h2>
<ul>
<li>Monolith: keep them unexposed and call via DI in-process. You get the same clear contract with zero network overhead.</li>
<li>Microservices: expose them and route behind your gateway. The <code>/integration-api</code> prefix makes it easy to firewall/gateway-restrict.</li>
</ul>
<h2>Practical tips</h2>
<ul>
<li>Keep integration DTOs lean and stable. These are machine contracts—don’t mix UI concerns.</li>
<li>Name them clearly (e.g., <code>UserIntegrationService</code>) so intent is obvious.</li>
<li>Guard your ASP.NET Core gateway application: block <code>/integration-api/*</code> from public traffic.</li>
<li>Enable auditing only if you truly need the logs for these calls.</li>
</ul>
<h2>Further reading</h2>
<ul>
<li>Official docs: <a href="https://github.com/abpframework/abp/blob/dev/docs/en/framework/api-development/integration-services.md">Integration Services</a></li>
</ul>
<p>That’s it! Integration Services give you a clean, intentional way to design internal APIs—great in monoliths, essential in microservices.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1bb47f-fd59-4cca-5eb2-add4bdfd623a" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1bb47f-fd59-4cca-5eb2-add4bdfd623a" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/white-labeling-in-abp-framework-5trwmrfm</guid>
      <link>https://abp.io/community/posts/white-labeling-in-abp-framework-5trwmrfm</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <title>White Labeling in ABP Framework</title>
      <description>Let your tenants use their own Logo, Name and design!</description>
      <pubDate>Fri, 30 May 2025 15:23:14 Z</pubDate>
      <a10:updated>2026-09-26T07:26:50Z</a10:updated>
      <content:encoded><![CDATA[<h1>White Labeling in ABP</h1>
<p>ABP Framework covers all the <strong>multi-tenant</strong> features already and you can easily build a SAAS application with ABP Framework. But... &quot;How about While Labeling?</p>
<h2>White Labeling</h2>
<p>White-labeling refers to the practice of developing a software solution that can be <strong>rebranded</strong> and <strong>resold</strong> by different companies under their own branding. So, our purpose is to build a software solution that can be <strong>rebranded</strong> by the tenant admin itself by changing the theme, logo, etc.</p>
<h2>Getting Started</h2>
<p>I skip the steps to create a new ABP solution and go straight to the point. I'll show you how to white-label to your existing ABP solution. Let's see prerequisites first.</p>
<ul>
<li>An ABP solution in MVC UI with <a href="https://abp.io/docs/latest/framework/ui/mvc-razor-pages/basic-theme">Basic Theme</a>.
<ul>
<li><em>(other templates should be ok, but you need to use the correct components to override)</em></li>
</ul>
</li>
<li>Setting Management module is installed. <em>(It's already installed in the ABP project templates)</em></li>
</ul>
<h2>Plan</h2>
<p>The plan is to create a new setting group and settings in the Setting Management module to use different settings for each tenant. We'll provide a UI to the tenant admin to change the settings. Then, we'll use these settings to change the theme, logo, etc by overriding the theme components.</p>
<h2>Create a new setting group</h2>
<p>Navigate to <code>WhileLabelAppSettingDefinitionProvider.cs</code> file and add a new setting that we'll use:</p>
<pre><code class="language-cs">public class WhileLabelAppSettingDefinitionProvider : SettingDefinitionProvider
{
    public override void Define(ISettingDefinitionContext context)
    {
        // Skip the default values since we'll do not change anything if it's not set.
        context.Add(
            new SettingDefinition(WhileLabelAppSettings.AppName),
            new SettingDefinition(WhileLabelAppSettings.Theme),
            new SettingDefinition(WhileLabelAppSettings.AppLogo),
            new SettingDefinition(WhileLabelAppSettings.PrimaryColor)
        );
    }
}
</code></pre>
<pre><code class="language-cs">
// WhileLabelAppSettings.cs
public static class WhileLabelAppSettings
{
    private const string Prefix = &quot;WhileLabelApp&quot;;
    public const string AppName = Prefix + &quot;AppName&quot;;
    public const string AppLogo = Prefix + &quot;AppLogo&quot;;
    public const string Theme = Prefix + &quot;Theme&quot;;
    public const string PrimaryColor = Prefix + &quot;PrimaryColor&quot;;
}
</code></pre>
<p>Now, we'll provide an API to get and update the settings by using Application Services.</p>
<ul>
<li>Define a simple permission for the settings.</li>
</ul>
<pre><code class="language-cs">public class WhileLabelAppPermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var myGroup = context.AddGroup(WhileLabelAppPermissions.GroupName);

        // Add related constant in WhileLabelAppPermissions.cs and add localization in en.json
        myGroup.AddPermission(WhileLabelAppPermissions.WhiteLabelSettings, L(&quot;Permission:WhiteLabelSettings&quot;));
    }
    // ...
}
</code></pre>
<h2>Building the Settings UI</h2>
<p>Now, we'll build the settings UI. We'll use the Setting Management module to create a new setting group and settings. Each tenant will see their own settings in the Setting Management page. To build this UI, we'll need to implement by using <code>ISettingManager</code> from <strong>Setting Management</strong> module. It provides keeping the settings tenant-specific.</p>
<ul>
<li>Create <code>IWhiteLabelSettingsAppService</code> interface in <strong>Aplication.Contracts</strong> project and <code>WhiteLabelSettingsAppService</code> class in <strong>Application</strong> project.</li>
</ul>
<pre><code class="language-cs">
public interface IWhiteLabelSettingsAppService : IApplicationService
{
    Task&lt;WhiteLabelSettingsDto&gt; GetAsync();
    Task UpdateAsync(WhiteLabelSettingsDto input);
}

public class WhiteLabelSettingsDto
{
    public string? AppName { get; set; }
    public string? AppLogo { get; set; }
    public string? Theme { get; set; }
    public string? PrimaryColor { get; set; }
}
</code></pre>
<pre><code class="language-cs">public class WhiteLabelSettingsAppService(ISettingManager settingManager) : ApplicationService, IWhiteLabelSettingsAppService   
{
    [Authorize(WhileLabelAppPermissions.WhiteLabelSettings)]
    public async Task&lt;WhiteLabelSettingsDto&gt; GetAsync()
    {
        return new WhiteLabelSettingsDto
        {
            AppName = await settingManager.GetOrNullForCurrentTenantAsync(WhileLabelAppSettings.AppName),
            AppLogo = await settingManager.GetOrNullForCurrentTenantAsync(WhileLabelAppSettings.AppLogo),
            Theme = await settingManager.GetOrNullForCurrentTenantAsync(WhileLabelAppSettings.Theme),
            PrimaryColor = await settingManager.GetOrNullForCurrentTenantAsync(WhileLabelAppSettings.PrimaryColor)
        };
    }

    [Authorize(WhileLabelAppPermissions.WhiteLabelSettings)]
    public async Task UpdateAsync(WhiteLabelSettingsDto input)
    {
        await settingManager.SetForCurrentTenantAsync(WhileLabelAppSettings.AppName, input.AppName);
        await settingManager.SetForCurrentTenantAsync(WhileLabelAppSettings.AppLogo, input.AppLogo);
        await settingManager.SetForCurrentTenantAsync(WhileLabelAppSettings.Theme, input.Theme);
        await settingManager.SetForCurrentTenantAsync(WhileLabelAppSettings.PrimaryColor, input.PrimaryColor);
    }
}
</code></pre>
<p>Now we're ready to build the UI. I'll use pre-defined bootstrap themes to allow tenants to choose a base design.</p>
<blockquote>
<p>Download 3 themes from <a href="https://bootswatch.com/">here</a> and put them in the <code>wwwroot/themes</code> folder.
I downloaded <code>darky</code>, <code>morph</code> and <code>quartz</code> themes and renamed them to <code>darky.min.css</code>, <code>morph.min.css</code> and <code>quartz.min.css</code> respectively. They all were <code>bootstrap.min.css</code> before renaming.</p>
</blockquote>
<ul>
<li>Create a new ViewComponent in <strong>Web</strong> project.
<ul>
<li><code>/Components/WhiteLabelSettings/Default.cshtml</code></li>
<li><code>/Components/WhiteLabelSettings/WhiteLabelSettingsViewComponent.cshtml.cs</code></li>
</ul>
</li>
</ul>
<pre><code class="language-html">@using Acme.WhileLabelApp.Settings
@model WhiteLabelSettingsDto

&lt;form id=&quot;WhiteLabelSettingsForm&quot;&gt;
    &lt;abp-input asp-for=&quot;AppName&quot; /&gt;
    &lt;abp-input asp-for=&quot;AppLogo&quot; /&gt;

    &lt;div class=&quot;form-group mb-3&quot;&gt;
        @Html.LabelFor(m =&gt; m.Theme)
        &lt;select class=&quot;form-select&quot; asp-for=&quot;Theme&quot;&gt;
            &lt;option value=&quot;&quot;&gt;Default&lt;/option&gt;
            &lt;option value=&quot;darky&quot;&gt;Darky&lt;/option&gt;
            &lt;option value=&quot;morph&quot;&gt;Morph&lt;/option&gt;
            &lt;option value=&quot;quartz&quot;&gt;Quartz&lt;/option&gt;
        &lt;/select&gt;
    &lt;/div&gt;

    &lt;div class=&quot;form-group&quot;&gt;    
        @Html.LabelFor(m =&gt; m.PrimaryColor)
        &lt;div class=&quot;mb-2&quot;&gt;
            &lt;input type=&quot;checkbox&quot; id=&quot;useDefaultColor&quot; class=&quot;form-check-input me-2&quot; /&gt;
            &lt;label for=&quot;useDefaultColor&quot; class=&quot;form-check-label&quot;&gt;Use Default&lt;/label&gt;
        &lt;/div&gt;
        &lt;div id=&quot;colorPickerContainer&quot;&gt;
            &lt;input type=&quot;color&quot; class=&quot;form-control w-25&quot; asp-for=&quot;PrimaryColor&quot; id=&quot;primaryColorPicker&quot; /&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;hr /&gt;
    &lt;abp-button type=&quot;submit&quot; class=&quot;mt-3&quot; button-type=&quot;Primary&quot; text=&quot;Save&quot; icon=&quot;fa-solid fa-save&quot; /&gt;
&lt;/form&gt;


&lt;script&gt;
    $(function() {
        const form = $('#WhiteLabelSettingsForm');
        const useDefaultCheckbox = $('#useDefaultColor');
        const colorPickerContainer = $('#colorPickerContainer');
        const primaryColorPicker = $('#primaryColorPicker');
        
        // Initialize checkbox state based on existing value
        
        @if (Model.PrimaryColor.IsNullOrEmpty())
        {
            &lt;text&gt;
                useDefaultCheckbox.prop('checked', true);
                colorPickerContainer.hide();
            &lt;/text&gt;
        }
        
        // Handle checkbox change
        useDefaultCheckbox.change(function() {
            if (this.checked) {
                colorPickerContainer.hide();
                primaryColorPicker.val(''); // Clear the color picker value
            } else {
                colorPickerContainer.show();
            }
        });
        
        form.submit(function(e) {
            e.preventDefault();
            
            var data = form.serializeFormToObject();
            
            // If &quot;Use Default&quot; is checked, set PrimaryColor to null
            if (useDefaultCheckbox.prop('checked')) {
                data.PrimaryColor = null;
            }

            acme.whileLabelApp.settings.whiteLabelSettings.update(data).then(function() {
                abp.notify.success('White label settings updated successfully');
                window.location.reload();
            }).catch(function(error) {
                abp.notify.error(error.message);
            });
        });
    });
    
&lt;/script&gt;
</code></pre>
<pre><code class="language-cs">public class WhiteLabelSettingsViewComponent(IWhiteLabelSettingsAppService whiteLabelSettingsAppService) : AbpViewComponent
{
    public async Task&lt;IViewComponentResult&gt; InvokeAsync()
    {
        var settings = await whiteLabelSettingsAppService.GetAsync();
        return View(&quot;~/Components/WhiteLabelSettings/Default.cshtml&quot;, settings);
    }
}
</code></pre>
<ul>
<li>Create a new <code>WhiteLabelAppSettingPageContributor</code> to add the ViewComponent to the Setting Management page.</li>
</ul>
<pre><code class="language-cs">public class WhiteLabelAppSettingPageContributor : ISettingPageContributor
{
    public async Task&lt;bool&gt; CheckPermissionsAsync(SettingPageCreationContext context)
    {
        var authService = context.ServiceProvider.GetRequiredService&lt;IAuthorizationService&gt;();
        return await authService.IsGrantedAsync(WhileLabelAppPermissions.WhiteLabelSettings);
    }

    public Task ConfigureAsync(SettingPageCreationContext context)
    {
        var l = context.ServiceProvider.GetRequiredService&lt;IStringLocalizer&lt;WhileLabelAppResource&gt;&gt;();
         context.Groups.Add(
            new SettingPageGroup(
                &quot;Volo.Abp.WhiteLabelSettingsGroup&quot;,
                l[&quot;WhiteLabelSettings&quot;],
                typeof(WhiteLabelSettingsViewComponent),
                order : 1
            )
        );

        return Task.CompletedTask;
    }
}
</code></pre>
<ul>
<li>⚠️ <strong>Important</strong> ⚠️
<ul>
<li>You need to add the <code>WhiteLabelAppSettingPageContributor</code> to the <code>SettingManagementPageOptions</code> in the <code>WhileLabelAppWebModule.cs</code> file.</li>
</ul>
</li>
</ul>
<pre><code class="language-cs">Configure&lt;SettingManagementPageOptions&gt;(options =&gt;
{
    options.Contributors.Add(new WhiteLabelAppSettingPageContributor());
});
</code></pre>
<p><img src="https://raw.githubusercontent.com/enisn/Acme.WhileLabelApp/main/images/settings-ui.png" alt="ABP White Label Settings" /></p>
<h2>Applying the Styles</h2>
<h3>Application Name &amp; Logo</h3>
<p>Now we're ready to apply the styles. Let's start with <code>BrandingProvider</code>.</p>
<pre><code class="language-cs">[Dependency(ReplaceServices = true)]
public class WhileLabelAppBrandingProvider(
    IStringLocalizer&lt;WhileLabelAppResource&gt; l,
    ISettingProvider settingProvider
    ) : DefaultBrandingProvider
{
    public override string AppName 
    { 
        get 
        {
            var appNameSettingValue = settingProvider.GetOrNullAsync(WhileLabelAppSettings.AppName)
            .GetAwaiter().GetResult();
            
            return appNameSettingValue ?? l[&quot;AppName&quot;];
        }
    }


    public override string? LogoUrl
    {
        get
        {
            var logoUrlSettingValue = settingProvider.GetOrNullAsync(WhileLabelAppSettings.AppLogo).GetAwaiter().GetResult();
            return logoUrlSettingValue ?? base.LogoUrl;
        }
    }
}
</code></pre>
<blockquote>
<p>⚠️ Unfortunatelly, <code>BrandingProvider</code> doesn't support async operations since it implemented to use properties.
<strong>As a best practice,</strong> overriding the logo component from the theme is a better approach.</p>
</blockquote>
<h3>Theme for each tenant</h3>
<p>Now, we'll apply the bootstrap theme to the application. To achieve this, we'll create a new <code>BundleContributor</code> and override bundle files according to the setting value of the tenant.</p>
<ul>
<li>Create a new <code>WhiteLabelAppBundleContributor</code> class:</li>
</ul>
<pre><code class="language-cs">public class WhiteLabelAppBundleContributor(ISettingProvider settingProvider) : BundleContributor
{
    public override async Task ConfigureBundleAsync(BundleConfigurationContext context)
    {
        var theme = await settingProvider.GetOrNullAsync(WhileLabelAppSettings.Theme);

        if (theme.IsNullOrEmpty())
        {
            return;
        }

        context.Files.RemoveAll(f =&gt; f.FileName.Contains(&quot;bootstrap&quot;));
        context.Files.Add($&quot;/themes/{theme}.min.css&quot;);
    }
}
</code></pre>
<ul>
<li>Add the bundle contributor to the bundle:</li>
</ul>
<pre><code class="language-cs"> Configure&lt;AbpBundlingOptions&gt;(options =&gt;
{
    options.StyleBundles.Configure(
        BasicThemeBundles.Styles.Global,
        bundle =&gt;
        {
            bundle.AddFiles(&quot;/global-styles.css&quot;);
            // Add the bundle contributor to the bundle 👇
            bundle.AddContributors(typeof(WhiteLabelAppBundleContributor));
        }
    );

    // ...
});
</code></pre>
<h3>Primary Color</h3>
<p>Now, we'll apply the primary color to the application. To achieve this, we'll need a new <code>style</code> as the latest element of the head section.</p>
<p>We'll use ABP's <a href="https://abp.io/docs/latest/framework/ui/blazor/layout-hooks">Layout Hooks</a> feature to achieve this.</p>
<ul>
<li>Create a new component <code>BootstrapStyleViewComponent</code> class:</li>
</ul>
<pre><code class="language-cs">public class BootstrapStyleViewComponent(ISettingProvider settingProvider) : AbpViewComponent
{
    public async Task&lt;IViewComponentResult&gt; InvokeAsync()
    {
        var primaryColor = await settingProvider.GetOrNullAsync(WhileLabelAppSettings.PrimaryColor);
        return View(&quot;~/Components/BootstrapStyle/Default.cshtml&quot;, primaryColor);
    }
}
</code></pre>
<ul>
<li>Add the <code>Default.cshtml</code> file to the <code>Components/BootstrapStyle</code> folder:</li>
</ul>
<pre><code class="language-html">@model string
@if (Model.IsNullOrEmpty())
{
    return;
}
&lt;style&gt;
    body {
        --primary-color: @Model;
    }

    .btn-primary {
        background-color: var(--primary-color);
        border-color: var(--primary-color);
    }

    .btn-primary:hover {
        background-color: var(--primary-color);
        border-color: var(--primary-color);
        opacity: 0.8;
    }

    .btn-primary:focus {
        background-color: var(--primary-color);
        border-color: var(--primary-color);
        box-shadow: 0 0 0 0.25rem rgba(var(--primary-color), 0.25);
    }

    .bg-primary {
        background-color: var(--primary-color);
    }

    .text-primary {
        color: var(--primary-color);
    }

    .border-primary {
        border-color: var(--primary-color);
    }

    .btn-outline-primary {
        border-color: var(--primary-color);
        color: var(--primary-color);
    }

    .btn-outline-primary:hover {
        background-color: var(--primary-color);
        border-color: var(--primary-color);
        color: var(--primary-color);
    }

    .btn-outline-primary:focus {
        background-color: var(--primary-color);
        border-color: var(--primary-color);
        color: var(--primary-color);
    }

    .nav-pills .nav-link.active, .nav-pills .show&gt;.nav-link {
        background-color: var(--primary-color);
    }

    .nav-link.active:hover {
        background-color: var(--primary-color);
        opacity: 0.8;
    }
&lt;/style&gt;
</code></pre>
<h2>Result</h2>
<p>Now, you can see the result. You can easily customize the application by using the Setting Management page.
<img src="https://raw.githubusercontent.com/enisn/Acme.WhileLabelApp/main/images/abp-white-label.gif" alt="ABP White Label Settings" /></p>
<p>Now create another tenant and switch to the new tenant. You can see the application is opened without any changes when you switch to the new tenant. Login with the new tenant's credentials and you can see the tenant has its own branding that it can customize itself.
<img src="https://raw.githubusercontent.com/enisn/Acme.WhileLabelApp/main/images/white-labeling-result.png" alt="ABP White Label Result" /></p>
<h2>Source Code</h2>
<p>You can find the source code of the example application in <a href="https://github.com/enisn/Acme.WhileLabelApp">GitHub</a>.</p>
<h2>Conclusion</h2>
<p>ABP Framework provides a powerful set of features to implement white-labeling easily in your application. This tutorial is a proof of concept to show how to implement white-labeling in your application.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a1a33f7-2298-d6a4-45c0-1d77d0e7d250" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1a33f7-2298-d6a4-45c0-1d77d0e7d250" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/you-do-it-wrong-customizing-abp-login-page-correctly-bna7wzt5</guid>
      <link>https://abp.io/community/posts/you-do-it-wrong-customizing-abp-login-page-correctly-bna7wzt5</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>customization</category>
      <category>mvc</category>
      <title>You do it wrong! Customizing ABP Login Page Correctly</title>
      <description>One of the most frequently asked questions within the ABP Community is:

"How do I change the login page?"

The answer often seems simple, but is it always the correct or most suitable approach for your specific needs? Let's delve into the details.</description>
      <pubDate>Tue, 27 May 2025 08:22:30 Z</pubDate>
      <a10:updated>2026-04-18T05:11:13Z</a10:updated>
      <content:encoded><![CDATA[One of the most frequently asked questions within the ABP Community is:

"How do I change the login page?"

The answer often seems simple, but is it always the correct or most suitable approach for your specific needs? Let's delve into the details.<br \><a href="https://dev.to/enisn/you-do-it-wrong-customizing-abp-login-page-correctly-l2k" 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/3a1a2302-dad1-937e-9b97-20d2097664af" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a1a2302-dad1-937e-9b97-20d2097664af" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/videos/using-vue-components-in-a-razor-pages-abp-application-jy8r8bgp</guid>
      <link>https://abp.io/community/videos/using-vue-components-in-a-razor-pages-abp-application-jy8r8bgp</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <title>Using Vue components in a Razor Pages ABP Application</title>
      <description>In modern web development, integrating dynamic front-end frameworks with server-side technologies has become increasingly essential for creating responsive and interactive applications. This article explores how to effectively use Vue components within Razor Pages in an ABP Framework application. We will delve into the process of consuming endpoints through ABP Client Proxies, leveraging ABP's powerful localization features to enhance user experience, and implementing ABP permissions to ensure secure access</description>
      <pubDate>Fri, 21 Mar 2025 04:39:21 Z</pubDate>
      <a10:updated>2026-04-26T16:18:22Z</a10:updated>
      <content:encoded><![CDATA[In modern web development, integrating dynamic front-end frameworks with server-side technologies has become increasingly essential for creating responsive and interactive applications. This article explores how to effectively use Vue components within Razor Pages in an ABP Framework application. We will delve into the process of consuming endpoints through ABP Client Proxies, leveraging ABP's powerful localization features to enhance user experience, and implementing ABP permissions to ensure secure access <br \> <a href="https://www.youtube.com/watch?v=sZ8iSMovHZs" rel="nofollow noopener noreferrer" title="Go to the Video">Go to the Video</a>]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a18c92c-7b88-88b6-624b-1699443fa0e5" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a18c92c-7b88-88b6-624b-1699443fa0e5" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-vue-components-in-a-razor-pages-abp-application-z3jr07tv</guid>
      <link>https://abp.io/community/posts/using-vue-components-in-a-razor-pages-abp-application-z3jr07tv</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>mvc</category>
      <category>vue</category>
      <title>Using Vue components in a Razor Pages ABP Application</title>
      <description>This article won't use any SPA approach. The goal of this article is to use Razor Pages with simple Vue components to eliminate jQuery while developing MVC application.</description>
      <pubDate>Tue, 18 Mar 2025 21:00:58 Z</pubDate>
      <a10:updated>2026-09-26T07:39:04Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using Vue components in a Razor Pages ABP Application</h1>
<p>In modern web development, integrating dynamic front-end frameworks with server-side technologies has become increasingly essential for creating responsive and interactive applications. This article explores how to effectively use Vue components within Razor Pages in an ABP Framework application. We will delve into the process of consuming endpoints through ABP Client Proxies, leveraging ABP's powerful localization features to enhance user experience, and implementing ABP permissions to ensure secure access control. By the end of this guide, you will have a comprehensive understanding of how to seamlessly blend Vue.js with Razor Pages, empowering you to build robust and user-friendly applications.</p>
<p>This article won't use any SPA approach. The goal of this article is to use Razor Pages with simple Vue components to eliminate jQuery while developing MVC application.</p>
<blockquote>
<p><strong>🎉 Also video version is available!</strong></p>
<p><a href="https://youtu.be/sZ8iSMovHZs?si=GynuJjsLEI1p2g6w">Watch on YouTube Now!</a></p>
</blockquote>
<h2>Creating the Solution</h2>
<p>Let's create a simple TODO list application to demonstrate how to use Vue components in Razor Pages. I'll build a really simple backend without a connection to a database for demonstration purposes. We will focus on the frontend part.</p>
<ul>
<li>Creating a solution with ABP CLI:</li>
</ul>
<pre><code class="language-bash">abp new MyTodoApp -t app-nolayers -csf
</code></pre>
<h2>Configure Vue</h2>
<p>We need to add the <code>@abp/vue</code> package to the project to use Vue components.</p>
<pre><code class="language-bash">npm install @abp/vue
</code></pre>
<ul>
<li>Install client libraries by using ABP CLI:</li>
</ul>
<pre><code class="language-bash">abp install-libs
</code></pre>
<p>As a last step, we need to configure our bundle in the <code>ConfigureBundles</code> method in the <code>MyTodoAppModule.cs</code> file:</p>
<pre><code class="language-csharp">private void ConfigureBundles()
{
    Configure&lt;AbpBundlingOptions&gt;(options =&gt;
    {
        // ...

        options.ScriptBundles.Configure(
            // Or BasicThemeBundles.Scripts.Global
            // Or LeptonXLiteThemeBundles.Scripts.Global
            // 👇 Depends on the theme you are using
            LeptonXThemeBundles.Scripts.Global,
            bundle =&gt;
            {
                bundle.AddFiles(&quot;/global-scripts.js&quot;);
                // 👇 Make sure to add this line
                bundle.AddContributors(typeof(VueScriptContributor));
            }
        );
    });
}
</code></pre>
<blockquote>
<p>If your IDE doesn't recognize the namespace of the <code>VueScriptContributor</code>, you can add it manually:</p>
<pre><code class="language-csharp">using Volo.Abp.AspNetCore.Mvc.UI.Packages.Vue;
</code></pre>
</blockquote>
<p>Now we're ready to use Vue components in our Razor Pages.</p>
<h2>Creating a Vue Component</h2>
<p>Let's create a simple Vue component to display the TODO list.</p>
<h3>Passing a simple message to the component</h3>
<ul>
<li>Remove existing HTML codes in <code>Index.cshtml</code> and replace with the following code:</li>
</ul>
<pre><code class="language-html">&lt;div id=&quot;vue-app&quot;&gt;
    &lt;message-component :message=&quot;'Welcome, @CurrentUser.UserName !'&quot;&gt;&lt;/message-component&gt;
&lt;/div&gt;
</code></pre>
<ul>
<li>Navigate to the <code>Index.cshtml.js</code> file and add the following code:</li>
</ul>
<pre><code class="language-js">Vue.component('message-component', {
    template: '&lt;div&gt;Hello, {{ message }}&lt;/div&gt;',
    props: ['message']
});

new Vue({
    el: '#vue-app'
});
</code></pre>
<p>Run the application and you should see the following output:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-18-Using-Vue-Components/vue-message.png" alt="Vue Component" /></p>
<blockquote>
<p><em>Hard refresh might be required to see the component since we added a new vue js file to the bundle.</em></p>
<p>If still you can't see the component, please check the browser console for any errors.</p>
</blockquote>
<h3>Interacting with the component</h3>
<p>Let's add a button to the component to interact with the component.</p>
<ul>
<li>Add another component in the <code>Index.cshtml</code> file:</li>
</ul>
<pre><code class="language-html">&lt;div id=&quot;vue-app&quot;&gt;
    &lt;message-component :message=&quot;'Welcome, @CurrentUser.UserName !'&quot;&gt;&lt;/message-component&gt;
    &lt;counter-component&gt;&lt;/counter-component&gt;
&lt;/div&gt;
</code></pre>
<pre><code class="language-js">Vue.component('counter-component', {
    template:`
    &lt;div class=&quot;card&quot;&gt;
        &lt;div class=&quot;card-body&quot;&gt;
            &lt;p&gt;Count: {{ count }}&lt;/p&gt;
            &lt;button class=&quot;btn btn-primary&quot; @click=&quot;increment&quot;&gt;Increment&lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    `,
    data: function () {
        return {
            count: 0
        };
    },
    methods: {
        increment: function () {
            this.count++;
        }
    }
});
</code></pre>
<blockquote>
<p><em>Do not replicate <code>new Vue({})</code> code block in the file. It's already in the <code>Index.cshtml.js</code> file. Keep it at the bottom of the file as it is.</em></p>
</blockquote>
<p>Run the application and you should see the following output:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-18-Using-Vue-Components/vue-counter-result.gif" alt="Vue Component" /></p>
<h2>Using ABP Client Proxy, Authorization and Localization</h2>
<h3>Building the backend</h3>
<p>Before we go, let's build our backend to use in the component.</p>
<ul>
<li>Creating a simple Application Service:</li>
</ul>
<pre><code class="language-csharp">public class TodoAppService : MyTodoAppAppService, ITodoAppService
{
    public static List&lt;TodoItem&gt; Items { get; } = new List&lt;TodoItem&gt;();

    [Authorize(&quot;Todo.Create&quot;)]
    public async Task&lt;TodoItem&gt; AddTodoItemAsync(TodoItem input)
    {
        Items.Add(input);
        return input;
    }

    [Authorize(&quot;Todo&quot;)]
    public async Task&lt;List&lt;TodoItem&gt;&gt; GetAllAsync()
    {
        await Task.Delay(1500);
        return Items;
    }
}
</code></pre>
<ul>
<li><code>TodoItem.cs</code></li>
</ul>
<pre><code class="language-csharp">public class TodoItem
{
    public string Description { get; set; }
    public bool IsDone { get; set; }
}
</code></pre>
<ul>
<li><code>ITodoAppService.cs</code></li>
</ul>
<pre><code class="language-csharp">public interface ITodoAppService
{
    Task&lt;List&lt;TodoItem&gt;&gt; GetAllAsync();
    Task&lt;TodoItem&gt; AddTodoItemAsync(TodoItem input);
}
</code></pre>
<ul>
<li><p>Run the application and if you can see the following client proxy in the browser console, you're ready to go:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-18-Using-Vue-Components/abp-js-proxy.png" alt="Client Proxy" /></p>
</li>
</ul>
<blockquote>
<p>[!NOTE]
If you can't see the client proxy in the browser console, please check the <a href="https://abp.io/docs/latest/framework/ui/mvc-razor-pages/dynamic-javascript-proxies">Dynamic JavaScript Proxies</a> to learn how to enable it.</p>
</blockquote>
<ul>
<li>Add a new permission in the <code>MyTodoAppPermissionDefinitionProvider.cs</code> file:</li>
</ul>
<pre><code class="language-csharp">public override void Define(IPermissionDefinitionContext context)
{
    var myGroup = context.AddGroup(MyTodoAppPermissions.GroupName);

    var todo = myGroup.AddPermission(&quot;Todo&quot;);
    todo.AddChild(&quot;Todo.Create&quot;);
}
</code></pre>
<blockquote>
<p><em>I go without localization or constants for simplicity.</em></p>
</blockquote>
<ul>
<li>Add a localization key in the <code>en.json</code> file:</li>
</ul>
<pre><code class="language-json">{
    &quot;TodoItems&quot;: &quot;Todo Items Localized&quot;
}
</code></pre>
<h3>Building the Vue Component: Using ABP Localization, Authorization and Client Proxy</h3>
<p>Since the component it directly loaded into the page, we can access the <code>abp</code> object on the page.</p>
<p>So we can use:</p>
<ul>
<li><code>abp.localization.localize()</code> to localize a string.</li>
<li><code>abp.auth.isGranted()</code> to check the authorization.</li>
<li><code>myTodoApp.todo.getAll()</code> and <code>myTodoApp.todo.addTodoItem</code> to call the Application Service.</li>
</ul>
<p>inside <strong>Vue Component</strong> code.</p>
<ul>
<li>Let's add another component named <code>todo-component</code> and usee all the <strong>ABP Features</strong> in it.</li>
</ul>
<pre><code class="language-html">&lt;div id=&quot;vue-app&quot;&gt;
    &lt;!-- ... --&gt;
    &lt;todo-component&gt;&lt;/todo-component&gt;
&lt;/div&gt;
</code></pre>
<ul>
<li>Implement the <code>todo-component</code> in <code>Index.cshtml.js</code> file:</li>
</ul>
<pre><code class="language-js">Vue.component('todo-component', {
    template: `
    &lt;div class=&quot;card&quot; v-if=&quot;abp.auth.isGranted('Todo')&quot;&gt;
        &lt;div class=&quot;card-header border-bottom&quot;&gt;
            &lt;h3&gt;{{ abp.localization.localize('TodoItems') }}&lt;/h3&gt;
        &lt;/div&gt;
        &lt;div class=&quot;card-body&quot;&gt;
            &lt;div v-if=&quot;isBusy&quot; class=&quot;w-100 text-center&quot;&gt; 
                &lt;div class=&quot;spinner-border&quot; role=&quot;status&quot;&gt;
                    &lt;span class=&quot;visually-hidden&quot;&gt;Loading...&lt;/span&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;ul v-else-if=&quot;todos.length &gt; 0&quot; class=&quot;list-group&quot;&gt;
                &lt;li class=&quot;list-group-item&quot; v-for=&quot;item in todos&quot; :key=&quot;item.description&quot;&gt;
                    &lt;input class=&quot;form-check-input&quot; type=&quot;checkbox&quot; v-model=&quot;item.isDone&quot;&gt;
                    &lt;label class=&quot;form-check-label&quot;&gt;{{ item.description }}&lt;/label&gt;
                &lt;/li&gt;
            &lt;/ul&gt;
            &lt;p v-else&gt;No todos yet&lt;/p&gt;
        &lt;/div&gt;
        &lt;div v-if=&quot;abp.auth.isGranted('Todo.Create')&quot; class=&quot;card-footer d-flex flex-column gap-2 border-top pt-2&quot;&gt;
            &lt;input class=&quot;form-control&quot; type=&quot;text&quot; v-model=&quot;newTodo.description&quot; placeholder=&quot;Add a new todo&quot;&gt;
            &lt;div class=&quot;form-check&quot;&gt;
                &lt;input class=&quot;form-check-input&quot; type=&quot;checkbox&quot; v-model=&quot;newTodo.isDone&quot; id=&quot;isDone&quot;&gt;
                &lt;label class=&quot;form-check-label&quot; for=&quot;isDone&quot;&gt;Is Done&lt;/label&gt;
            &lt;/div&gt;

            &lt;button class=&quot;btn btn-primary&quot; @click=&quot;addTodo&quot;&gt;Add&lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    `,
    data: function () {
        return {
            newTodo: {
                description: '',
                isDone: false
            },
            isBusy: false,
            todos: []
        };
    },
    methods: {
        addTodo() {
            myTodoApp.todo.addTodoItem(this.newTodo);
            this.newTodo = { description: '', isDone: false };
            this.todos.push(this.newTodo);

            // Preferrable, you can load entire list of todos again.
            // this.loadTodos();
        },
        async loadTodos() {
             if (!abp.auth.isGranted('Todo')) {
                return;
            }
            this.isBusy = true;
            this.todos = await myTodoApp.todo.getAll();
            this.isBusy = false;
        }
    },
    mounted() {
        this.loadTodos();
    }
});
</code></pre>
<p>And see the result:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-18-Using-Vue-Components/todo-component-result.gif" alt="Vue Component" /></p>
<p>Since we use <code>abp.auth.isGranted()</code> to check the authorization, we can see the component only if we have the permission.</p>
<p>Whenever you remove <code>Todo.Create</code> permission, you can see the component is not rendered.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-18-Using-Vue-Components/todo-permission.png" alt="Todo Permission" /></p>
<p>You won't see the card footer:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2025-03-18-Using-Vue-Components/todo-permission-vue.png" alt="Todo Permission" /></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a18bd3c-175f-e830-2436-ab0df94c29ea" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a18bd3c-175f-e830-2436-ab0df94c29ea" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/c-13-features-1aq5pzuy</guid>
      <link>https://abp.io/community/posts/c-13-features-1aq5pzuy</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>dotnet-9.0</category>
      <title>C# 13 Features</title>
      <description>Explaining new features of C# 13</description>
      <pubDate>Fri, 15 Nov 2024 06:54:01 Z</pubDate>
      <a10:updated>2026-09-26T04:17:55Z</a10:updated>
      <content:encoded><![CDATA[<h1>C# 13 Features</h1>
<p>C# 13 is the latest version of C# and it comes with a lot of new features. In this article, we will discuss some of the new features of C# 13.</p>
<h2><code>params</code> collections</h2>
<p>With the C# 13, method parameter with <code>params</code> keyword isn't limited to be an array. You can now use any collection type that implements <code>IEnumerable&lt;T&gt;</code> interface.</p>
<p>Let's see how it can help us in our code.</p>
<pre><code class="language-csharp">public IEnumerable&lt;int&gt; GetOdds(params IEnumerable&lt;int&gt; numbers)
{
    foreach (var number in numbers)
    {
        if (number % 2 != 0)
        {
            Console.WriteLine(number);
        }
    }
}
</code></pre>
<h2>New lock object</h2>
<p>I'm sure you have used <code>lock</code> statement in your code to synchronize access to a shared resource. With C# 13, you can now use a new lock object that is more efficient than the traditional lock object.
The new <code>Lock</code> type provides better thread synchronization through its API. When <code>Lock.EnterScope()</code> method is called, it returns a struct named <code>Scope</code> that contains a <code>Dispose</code> method. The <code>Dispose</code> method is called when the <code>Scope</code> object goes out of scope, which releases the lock. C# <code>using</code> statement recognizes the <code>Dispose</code> method and calls it automatically like it does with other <code>IDisposable</code> objects.</p>
<p>It was something similar before:</p>
<pre><code class="language-csharp">private object _lock = new();

public void DoSomething()
{
    lock (_lock)
    {
        // Do something
    }
}
</code></pre>
<p>Now, you can use the new lock object like this:</p>
<pre><code class="language-csharp">System.Threading.Lock x = new System.Threading.Lock();
public void DoSomething()
{
    using (x.EnterScope())
    {
        // Do something
    }
}
</code></pre>
<h2>New escape sequence</h2>
<p>In C# 13, a new escape sequence <code>\e</code> has been introduced to represent the <code>ESCAPE</code> character, Unicode <code>U+001B</code>. Previously, you had to use <code>\u001b</code> or <code>\x1b</code> to represent this character. The new <code>\e</code> escape sequence simplifies this process and avoids potential issues with hexadecimal digits following <code>\x1b</code>.</p>
<blockquote>
<p>You can check <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#C0_control_codes">here</a> for ANSI escape codes.</p>
</blockquote>
<h2>Implicit index access</h2>
<p>The implicit &quot;from the end&quot; index operator, <code>^</code>, is now allowed in an object initializer expression.</p>
<p>It was not possible before, but now you can do this:</p>
<pre><code class="language-csharp">var countdown = new TimerRemaining()
{
    buffer =
    {
        [^1] = 0,
        [^2] = 1,
        [^3] = 2,
        [^4] = 3,
        [^5] = 4,
        [^6] = 5,
        [^7] = 6,
        [^8] = 7,
        [^9] = 8,
        [^10] = 9
    }
};
</code></pre>
<p>It's a great feature that makes the code more readable and maintainable. Still not a big deal, but it's nice to have it.</p>
<h2><code>ref</code> and <code>unsafe</code> in iterators and async methods</h2>
<p>In C# 13, the restrictions on using <code>ref</code> and <code>unsafe</code> constructs in iterators and async methods have been relaxed. Previously, you couldn't declare local <code>ref</code> variables or use unsafe contexts in these methods. Now, you can declare ref local variables and use unsafe contexts in async methods and iterators, provided they are not accessed across <code>await</code> or <code>yield</code> boundaries</p>
<p>This change allows for more expressive and efficient code, especially when working with types like <code>System.Span&lt;T&gt;</code> and <code>System.ReadOnlySpan&lt;T&gt;</code>. The compiler ensures that these constructs are used safely, and it will notify you if any safety rules are violated.</p>
<p>You can read more about this feature on the <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-13.0/ref-unsafe-in-iterators-async">Microsoft Learn page</a>.</p>
<h2>More partial members</h2>
<p>In C# 13, the concept of partial members has been expanded to include partial properties and partial indexers. Previously, only methods could be defined as partial members. This means you can now split the definition of properties and indexers across multiple files, just like you could with methods.</p>
<p>For example, you can declare a partial property in one part of your class and implement it in another part. Here's a simple illustration:</p>
<pre><code class="language-csharp">public partial class MyClass
{
    // Declaring declaration
    public partial string MyProperty { get; set; }
}

public partial class MyClass
{
    // Implementing declaration
    private string _myProperty;
    public partial string MyProperty
    {
        get =&gt; _myProperty;
        set =&gt; _myProperty = value;
    }
}
</code></pre>
<p>This feature allows for better organization and modularization of your code, especially in large projects where different parts of a class might be implemented by different team members.</p>
<h2>Overload resolution priority</h2>
<p>What does &quot;Overload resolution priority&quot; section mean in this page?
In C# 13, the OverloadResolutionPriority attribute allows library authors to specify which method overload should be preferred by the compiler when multiple overloads are available. This attribute helps avoid ambiguity and ensures that the most appropriate overload is chosen, even if it might not be the most obvious choice based on traditional overload resolution rules.</p>
<p>This may be useful in scenarios where you have multiple overloads that are equally valid, but you want to prioritize one over the others. The attribute can be applied to a method or constructor to indicate its priority in the overload resolution process. It can prevent unexpected behavior and make your code more predictable and maintainable.</p>
<p>Let me show with an example:</p>
<pre><code class="language-csharp">public class Example
{
    // Existing method
    public void Display(string message = &quot;Hello!&quot;)
    {
        Console.WriteLine(&quot;Message: &quot; + message);
    }

    // New, more efficient method with higher priority
    [OverloadResolutionPriority(1)]
    public void Display(string message = &quot;Hello!&quot;, int repeatCount = 3)
    {
        for (int i = 0; i &lt; repeatCount; i++)
        {
            Console.WriteLine(&quot;Message: &quot; + message);
        }
    }
}

class Program
{
    static void Main()
    {
        Example example = new Example();

        // Normally, you can't compile this code because of ambiguity:
        example.Display();
    }
}
</code></pre>
<p>Output:</p>
<pre><code>Message: Hello!
Message: Hello!
Message: Hello!
</code></pre>
<h2>The <code>field</code> keyword</h2>
<p>n C# 13, the <code>field</code> keyword is introduced as a preview feature to simplify property accessors. This keyword allows you to reference the compiler-generated backing <code>field</code> directly within a property accessor, eliminating the need to declare an explicit backing <code>field</code> in your type declaration.</p>
<p>For example, instead of writing:</p>
<pre><code class="language-csharp">private int _value;
public int Value
{
    get =&gt; _value;
    set =&gt; _value = value;
}
</code></pre>
<p>You can now write:</p>
<pre><code class="language-csharp">public int Value
{
    get =&gt; field;
    set =&gt; field = value;
}
</code></pre>
<p>This makes your code cleaner and more concise. However, be cautious if you have a <code>field</code> named <code>field</code> in your class, as it could cause confusion. You can disambiguate by using <code>@field</code> or <code>this.field</code>.</p>
<p>Make sure you're using the latest <code>LangVersion</code> in your <code>.csproj</code> project file to enable this feature.</p>
<pre><code class="language-xml">&lt;LangVersion&gt;preview&lt;/LangVersion&gt;
</code></pre>
]]></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/native-aot-compilation-in-.net-8-oq7qtwov</guid>
      <link>https://abp.io/community/posts/native-aot-compilation-in-.net-8-oq7qtwov</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>net8</category>
      <category>aot</category>
      <title>Native AOT Compilation in .NET 8</title>
      <description>Native AOT (Ahead-of-Time) compilation is a feature that allows developers to create a self-contained app compiled to native code that can run on machines without the .NET runtime installed. It results in benefits such as minimized disk footprint, reduced executable size, reduced startup time, and reduced memory demand.</description>
      <pubDate>Tue, 07 Nov 2023 13:28:42 Z</pubDate>
      <a10:updated>2026-09-26T07:02:58Z</a10:updated>
      <content:encoded><![CDATA[<h1>Native AOT Compilation in .NET 8</h1>
<p>Native AOT (Ahead-of-Time) compilation is a feature that allows developers to create a self-contained app compiled to native code that can run on machines without the .NET runtime installed. It results in benefits such as minimized disk footprint, reduced executable size, reduced startup time, and reduced memory demand.</p>
<p>Native AOT compilation isn't a new feature in .NET 8. It's first introduced in .NET 7.</p>
<p>Differences between the AOT Compilation of .NET 7 and .NET 8 are:</p>
<ul>
<li><strong>System.Text.Json improvements</strong>: .NET 8 adds support for more types, source generation, interface hierarchies, naming policies, read-only properties, and more.</li>
<li><strong>New types for performance</strong>: .NET 8 introduces new types such as FrozenDictionary, FrozenSet, SearchValues, CompositeFormat, TimeProvider, and ITimer to improve the app performance.</li>
<li><strong>System.Numerics and System.Runtime.Intrinsics enhancements</strong>: .NET 8 adds support for Vector512, AVX-512, IUtf8SpanFormattable, Lerp, and more.</li>
<li><strong>System.ComponentModel.DataAnnotations additions</strong>: .NET 8 adds new data validation attributes for cloud-native services and a new ValidateOptionsResultBuilder type.</li>
<li><strong>Hosted services lifecycle methods</strong>: .NET 8 adds new methods such as StartAsync, StopAsync, StartBackgroundAsync, and StopBackgroundAsync for hosted services.</li>
</ul>
<p>It's important to note that not all features in ASP.NET Core are currently compatible with native AOT. For more information, see <a href="https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/">Native AOT deployment overview</a>.</p>
<h2>How to use Native AOT Compilation in .NET 8</h2>
<p>You can add <code>&lt;PublishAot&gt;true&lt;/PublishAot&gt;</code> in your project .csproj file to enable Native AOT Compilation.</p>
<ul>
<li>For the new projects, you can create them with the <code>--aot</code> parameter. Example: <code>dotnet new console --aot</code>.</li>
</ul>
<p>By default, the compiler chooses a blended approach code optimization but you can specify an optimization preference inside your .csproj file. You can choose <strong>size</strong> or <strong>speed</strong> according your requirements.</p>
<pre><code class="language-xml">&lt;OptimizationPreference&gt;Size&lt;/OptimizationPreference&gt;
</code></pre>
<p>or</p>
<pre><code class="language-xml">&lt;OptimizationPreference&gt;Speed&lt;/OptimizationPreference&gt;
</code></pre>
<h3>Results</h3>
<p>I have created a simple console application to test the Native AOT Compilation. I have used a simple console application that writes &quot;Hello World!&quot; to the console 100 times. I have tested the application with different optimization preferences. I have used the following results:</p>
<p>|       | Size | Speed |
| ---   | ---   | ---  |
| .NET 8 <br/><em>(Self-Contained, Single File)</em>   |  65938 kb     | 00.0051806  ~5ms   |
| .NET 7 AOT (default)          |   4452 kb     | 00.0029823  ~2ms |
| .NET 8 AOT (default)          |   1242 kb     | 00.0028638  ~2ms |
| AOT (Speed)| 1280 kb | 00.0023838  ~2ms |
| AOT (Size) | 1111 kb | 00.0025145  ~2ms |</p>
<p>Most of existing libraries don't support AOT compilation yet, so I couldn't use <a href="https://github.com/dotnet/BenchmarkDotNet">BenchmarkDotnet</a> to measure the performance. I have used <a href="https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-8.0">Stopwatch</a> to measure the performance. So the performance results may not be accurate but gives insight about the performance difference.</p>
<h2>AOT Support in MAUI</h2>
<p>You can now use Native AOT Compilation on iOS-like target frameworks in .NET MAUI. You can enable AOT compilation with the exact same method by adding <code>&lt;PublishAot&gt;true&lt;/PublishAot&gt;</code> to your project .csproj file. According to the dotnet team, apps sizes reduced by 35% and startup times reduced by 28% with AOT compilation. And runtime performance is also improved by 50%.</p>
<p>But there are some limitations in MAUI AOT Compilation. A lot of libraries still don't support AOT compilation and some of platform-specific feaetures may not work at the moment.</p>
<h2>When to use Native AOT Compilation?</h2>
<p>Native AOT Compilation is beneficial when you need to optimize your .NET application for speed and size. It's particularly useful for applications that require quick startup times and efficient runtime performance, such as mobile apps or high-performance computing applications.</p>
<p>However, due to its current limitations, it might not be suitable for all projects. If your project relies heavily on libraries that do not support AOT compilation, or if it uses platform-specific features that are not yet compatible with AOT, you might want to hold off on using Native AOT Compilation until further improvements are made.</p>
<p>Always consider the specific needs and constraints of your project before deciding to use Native AOT Compilation.</p>
<h2>Conclusion</h2>
<p>Native AOT Compilation is a great feature that improves the performance of .NET applications. It's still in early-stages and not all libraries support it yet. But it's a great beginning for the future of .NET 🚀</p>
<h2>Links</h2>
<ul>
<li>Native AOT deployment overview - .NET | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/.</li>
<li>Optimize AOT deployments https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/optimizing</li>
<li>What's new in .NET 8 | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8.</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-add-dark-mode-support-to-the-basic-theme-in-3-steps-ge9c0f85</guid>
      <link>https://abp.io/community/posts/how-to-add-dark-mode-support-to-the-basic-theme-in-3-steps-ge9c0f85</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>mvc</category>
      <category>basic-theme</category>
      <category>bootstrap</category>
      <category>dark-mode</category>
      <title>How to add dark mode support to the Basic Theme in 3 steps?</title>
      <description>This article will show you how to add toggle dark/light mode support to the Basic Theme.</description>
      <pubDate>Tue, 08 Aug 2023 07:16:47 Z</pubDate>
      <a10:updated>2026-09-26T09:59:34Z</a10:updated>
      <content:encoded><![CDATA[<h1>Adding Dark Mode Support to the Basic Theme</h1>
<p>Basic Theme uses plain bootstrap and does not have any custom colors &amp; styles. This article will show you how to add dark mode support to the <a href="https://docs.abp.io/en/abp/latest/UI/AspNetCore/Basic-Theme">Basic Theme</a>.</p>
<p>Bootstrap brings the <a href="https://getbootstrap.com/docs/5.3/customize/color-modes/#dark-mode">Color Modes</a> feature with version <strong>5.3</strong>. This feature allows you to add dark mode support to your website with a single line of code. Adding the <code>data-bs-theme=&quot;dark&quot;</code> attribute changes the color mode of the element to dark mode.</p>
<h2>Instructions</h2>
<ol>
<li><p>Create a new project with the following command:</p>
<pre><code class="language-bash">abp new BasicThemeDarkMode -t app --theme basic
</code></pre>
</li>
<li><p>Create a component that toggles the color mode.</p>
<ul>
<li><p>Create a new file named <code>Components/ChangeTheme/Default.cshtml</code>:</p>
<pre><code class="language-html">&lt;div class=&quot;text-light mt-1&quot;&gt;    
    &lt;button class=&quot;btn text-light&quot; href=&quot;#&quot; id=&quot;ToolbarChangeTheme&quot;&gt;
        &lt;i class=&quot;fas fa-sun&quot;&gt;&lt;/i&gt;
    &lt;/button&gt;
&lt;/div&gt;
</code></pre>
</li>
<li><p>Create a new file named <code>Components/ChangeTheme/ChangeThemeViewComponent.cs</code>:</p>
<pre><code class="language-csharp">using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;

namespace BasicThemeDarkMode.Web.Components.ChangeTheme;

[Widget(ScriptFiles = new[]{&quot;/Components/ChangeTheme/ChangeTheme.js&quot;})]
public class ChangeThemeViewComponent : AbpViewComponent
{
    public IViewComponentResult Invoke()
    {
        return View(&quot;~/Components/ChangeTheme/Default.cshtml&quot;);
    }
}
</code></pre>
</li>
<li><p>Create a JavaScript that manages the last selected theme and toggles the color mode. It stores the last selected theme in the <em>local storage</em>. So, you don't need to store it in the database.</p>
<ul>
<li>Create a new file named <code>Components/ChangeTheme/ChangeTheme.js</code>:
<pre><code class="language-js">$(function () {
     function changeTheme(theme) {
         window.localStorage.setItem('theme', theme);
         document.getElementsByTagName('body')[0].setAttribute('data-bs-theme', theme);
     }

     function toggleTheme(){
         getTheme() == 'light' ? changeTheme('dark') : changeTheme('light');
     }

     function getTheme(){
         return window.localStorage.getItem('theme') ?? 'dark';
     }

     function init(){
         let theme = getTheme();
         if(theme){
             changeTheme(theme);
         }
     }

     document.getElementById('ToolbarChangeTheme').addEventListener('click', () =&gt; {
         toggleTheme();
     });

     init();
});
</code></pre>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Create a new <a href="https://docs.abp.io/en/abp/latest/UI/AspNetCore/Toolbars">Toolbar Contributor</a> and add a newly created view component to the application toolbar.</p>
<ul>
<li><p>Create a new class named <code>BasicThemeDarkModeToolbarContributor.cs</code>:</p>
<pre><code class="language-csharp">using BasicThemeDarkMode.Web.Components.ChangeTheme;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars;

namespace BasicThemeDarkMode.Web;

public class BasicThemeDarkModeToolbarContributor : IToolbarContributor
{
    public Task ConfigureToolbarAsync(IToolbarConfigurationContext context)
    {
        if (context.Toolbar.Name == StandardToolbars.Main)
        {
            context.Toolbar.Items
                .Add(new ToolbarItem(typeof(ChangeThemeViewComponent)));
        }

        return Task.CompletedTask;
    }
}
</code></pre>
</li>
<li><p>Configure <a href="https://docs.abp.io/en/abp/latest/UI/AspNetCore/Toolbars">Toolbar Options</a> and add a newly created contributor:</p>
<pre><code class="language-csharp">Configure&lt;AbpToolbarOptions&gt;(options =&gt;
{
    options.Contributors.Add(new BasicThemeDarkModeToolbarContributor());
});
</code></pre>
</li>
</ul>
</li>
</ol>
<p>That's it! Now, you can toggle the color mode by clicking the sun icon in the toolbar:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-08-07-Basic-Theme-Dark-Mode/basictheme-toggle-demo.gif" alt="Dark Mode" /></p>
<ul>
<li><p>Users Page in Dark Mode:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-08-07-Basic-Theme-Dark-Mode/basictheme-dark-users.png" alt="Users Page in Dark Mode" /></p>
</li>
<li><p>Settings Page in Dark Mode:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-08-07-Basic-Theme-Dark-Mode/basictheme-dark-settings.png" alt="Settings Page in Dark Mode" /></p>
</li>
<li><p>Login Page in Dark Mode:</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-08-07-Basic-Theme-Dark-Mode/basictheme-dark-login.png" alt="Login Page in Dark Mode" /></p>
</li>
</ul>
<h2>Conclusion</h2>
<p>The theme is stored in <strong>local storage</strong> and it's initialized on the client-side. You can use <strong>Cookies</strong> to render the page in the last selected theme on <strong>server-side</strong> to prevent the flash effect while navigating pages. This document shows the concept of adding dark mode support of bootstrap to the Basic Theme.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a0ce62e-3952-c54c-8069-1f872003ab7e" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a0ce62e-3952-c54c-8069-1f872003ab7e" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/converting-createedit-modal-to-page-blazor-eexdex8y</guid>
      <link>https://abp.io/community/posts/converting-createedit-modal-to-page-blazor-eexdex8y</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>blazor</category>
      <category>tutorial</category>
      <category>modal</category>
      <title>Converting Create/Edit Modal to Page - Blazor</title>
      <description>In this document we will explain how to convert BookStore's Books create &amp; edit modals to regular blazor pages.</description>
      <pubDate>Tue, 28 Mar 2023 14:51:08 Z</pubDate>
      <a10:updated>2026-09-26T05:30:11Z</a10:updated>
      <content:encoded><![CDATA[<h1>Convert Create/Edit Modals to Page</h1>
<p>In this document we will explain how to convert BookStore's Books create &amp; edit modals to regular blazor pages.</p>
<h2>Before</h2>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-03-28-Converting-Create-Edit-Modal-To-Page/images/old.gif" alt="bookstore-crud-before" /></p>
<h2>After</h2>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-03-28-Converting-Create-Edit-Modal-To-Page/images/new.gif" alt="bookstore-crud-after" /></p>
<h1>Books.razor Page</h1>
<p>Books.razor page is the main page of the books management. Create &amp; Update operations are done in this page. So we'll remove create &amp; update operations from this page and move a separate blazor component for each operation. Each component will be a page.</p>
<ul>
<li><p>Remove both Create &amp; Update modals.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-03-28-Converting-Create-Edit-Modal-To-Page/images/books-remove-modals.png" alt="remove-all-modals" /></p>
</li>
<li><p>Replace <strong>NewBook</strong> button with a link to <strong>CreateBook</strong> page.</p>
<pre><code class="language-html">&lt;Button Color=&quot;Color.Primary&quot; Type=&quot;ButtonType.Link&quot; To=&quot;books/new&quot;&gt;
    @L[&quot;NewBook&quot;]
&lt;/Button&gt;
</code></pre>
</li>
<li><p>Inject <code>NavigationManager</code> to <code>Books.razor</code> page.</p>
<pre><code class="language-csharp">@inject NavigationManager NavigationManager
</code></pre>
</li>
<li><p>Replace <strong>Edit</strong> button with a link to <strong>UpdateBook</strong> page.</p>
<pre><code class="language-html">&lt;Button Color=&quot;Color.Primary&quot; Type=&quot;ButtonType.Link&quot; OnClick=&quot;() =&gt; NavigateToEdit(book.Id)&quot;&gt;
    @L[&quot;Edit&quot;]
&lt;/Button&gt;
</code></pre>
<pre><code class="language-csharp">private void NavigateToEdit(Guid id)
{
    NavigationManager.NavigateTo($&quot;books/{id}/edit&quot;);
}
</code></pre>
</li>
<li><p>Remove all methods in the <code>Books.razor</code> page except constructor. And add <code>GoToEditPage</code> as below:</p>
<pre><code class="language-csharp">protected void GoToEditPage(BookDto book)
{
    NavigationManager.NavigateTo($&quot;books/{book.Id}&quot;);
}
</code></pre>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2023-03-28-Converting-Create-Edit-Modal-To-Page/images/books-remove-methods.png" alt="bookstore-remove-methods" /></p>
</li>
<li><p>Change Edit button to a link in the table.</p>
<pre><code class="language-html">&lt;EntityAction TItem=&quot;BookDto&quot;
            Text=&quot;@L[&quot;Edit&quot;]&quot;
            Visible=HasUpdatePermission
            Clicked=&quot;() =&gt; GoToEditPage(context)&quot; /&gt;
</code></pre>
</li>
</ul>
<h1>CreateBooks Page</h1>
<p>Create new <code>CreateBook.razor</code> and <code>CreateBook.razor.cs</code> files in your project.</p>
<ul>
<li><code>CreateBook.razor</code></li>
</ul>
<pre><code class="language-html">@page &quot;/books/new&quot;
@attribute [Authorize(BookStorePermissions.Books.Create)]
@inherits BookStoreComponentBase

@using Acme.BookStore.Books;
@using Acme.BookStore.Localization;
@using Acme.BookStore.Permissions;
@using Microsoft.Extensions.Localization;
@using Volo.Abp.AspNetCore.Components.Web;

@inject IStringLocalizer&lt;BookStoreResource&gt; L
@inject AbpBlazorMessageLocalizerHelper&lt;BookStoreResource&gt; LH
@inject IBookAppService AppService
@inject NavigationManager NavigationManager

&lt;Card&gt;
    &lt;CardHeader&gt;
        &lt;HeadContent&gt;
            &lt;ModalTitle&gt;@L[&quot;NewBook&quot;]&lt;/ModalTitle&gt;
        &lt;/HeadContent&gt;
    &lt;/CardHeader&gt;
    &lt;CardBody&gt;
        &lt;Validations @ref=&quot;@CreateValidationsRef&quot; Model=&quot;@NewEntity&quot; ValidateOnLoad=&quot;false&quot;&gt;
            &lt;Validation MessageLocalizer=&quot;@LH.Localize&quot;&gt;
                &lt;Field&gt;
                    &lt;FieldLabel&gt;@L[&quot;Author&quot;]&lt;/FieldLabel&gt;
                    &lt;Select TValue=&quot;Guid&quot; @bind-SelectedValue=&quot;@NewEntity.AuthorId&quot;&gt;
                        @foreach (var author in authorList)
                        {
                            &lt;SelectItem TValue=&quot;Guid&quot; Value=&quot;@author.Id&quot;&gt;
                                @author.Name
                            &lt;/SelectItem&gt;
                        }
                    &lt;/Select&gt;
                &lt;/Field&gt;
                &lt;Field&gt;
                    &lt;FieldLabel&gt;@L[&quot;Name&quot;]&lt;/FieldLabel&gt;
                    &lt;TextEdit @bind-Text=&quot;@NewEntity.Name&quot;&gt;
                        &lt;Feedback&gt;
                            &lt;ValidationError /&gt;
                        &lt;/Feedback&gt;
                    &lt;/TextEdit&gt;
                &lt;/Field&gt;
            &lt;/Validation&gt;
            &lt;Field&gt;
                &lt;FieldLabel&gt;@L[&quot;Type&quot;]&lt;/FieldLabel&gt;
                &lt;Select TValue=&quot;BookType&quot; @bind-SelectedValue=&quot;@NewEntity.Type&quot;&gt;
                    @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
                    {
                        &lt;SelectItem TValue=&quot;BookType&quot; Value=&quot;@((BookType) bookTypeValue)&quot;&gt;
                            @L[$&quot;Enum:BookType.{bookTypeValue}&quot;]
                        &lt;/SelectItem&gt;
                    }
                &lt;/Select&gt;
            &lt;/Field&gt;
            &lt;Field&gt;
                &lt;FieldLabel&gt;@L[&quot;PublishDate&quot;]&lt;/FieldLabel&gt;
                &lt;DateEdit TValue=&quot;DateTime&quot; @bind-Date=&quot;NewEntity.PublishDate&quot; /&gt;
            &lt;/Field&gt;
            &lt;Field&gt;
                &lt;FieldLabel&gt;@L[&quot;Price&quot;]&lt;/FieldLabel&gt;
                &lt;NumericEdit TValue=&quot;float&quot; @bind-Value=&quot;NewEntity.Price&quot; /&gt;
            &lt;/Field&gt;
        &lt;/Validations&gt;
    &lt;/CardBody&gt;
    &lt;CardFooter&gt;
        &lt;Button Color=&quot;Color.Secondary&quot; Type=&quot;ButtonType.Link&quot; To=&quot;books&quot;&gt;
            @L[&quot;Cancel&quot;]
        &lt;/Button&gt;
        &lt;Button Color=&quot;Color.Primary&quot;
                Type=&quot;@ButtonType.Submit&quot;
                PreventDefaultOnSubmit=&quot;true&quot;
                Clicked=&quot;CreateEntityAsync&quot;&gt;
            @L[&quot;Save&quot;]
        &lt;/Button&gt;
    &lt;/CardFooter&gt;
&lt;/Card&gt;
</code></pre>
<ul>
<li><code>CreateBook.razor.cs</code></li>
</ul>
<pre><code class="language-csharp">using Acme.BookStore.Books;
using Blazorise;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp;

namespace Acme.BookStore.Blazor.Pages;

public partial class CreateBook
{
    protected Validations CreateValidationsRef;
    protected CreateUpdateBookDto NewEntity = new();
    IReadOnlyList&lt;AuthorLookupDto&gt; authorList = Array.Empty&lt;AuthorLookupDto&gt;();

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        authorList = (await AppService.GetAuthorLookupAsync()).Items;

        if (!authorList.Any())
        {
            throw new UserFriendlyException(message: L[&quot;AnAuthorIsRequiredForCreatingBook&quot;]);
        }

        NewEntity.AuthorId = authorList.First().Id;

        if (CreateValidationsRef != null)
        {
            await CreateValidationsRef.ClearAll();
        }
    }

    protected virtual async Task CreateEntityAsync()
    {
        try
        {
            var validate = true;
            if (CreateValidationsRef != null)
            {
                validate = await CreateValidationsRef.ValidateAll();
            }
            if (validate)
            {
                await AppService.CreateAsync(NewEntity);
                NavigationManager.NavigateTo(&quot;books&quot;);
            }
        }
        catch (Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }
}
</code></pre>
<h1>EditBooks Page</h1>
<p>Create new <code>EditBook.razor</code> and <code>EditBook.razor.cs</code> files in your project.</p>
<ul>
<li><code>EditBook.razor</code></li>
</ul>
<pre><code class="language-html">@page &quot;/books/{Id}&quot;
@attribute [Authorize(BookStorePermissions.Books.Edit)]
@inherits BookStoreComponentBase
@using Acme.BookStore.Books;
@using Acme.BookStore.Localization;
@using Acme.BookStore.Permissions;
@using Microsoft.Extensions.Localization;
@using Volo.Abp.AspNetCore.Components.Web;

@inject IStringLocalizer&lt;BookStoreResource&gt; L
@inject AbpBlazorMessageLocalizerHelper&lt;BookStoreResource&gt; LH
@inject IBookAppService AppService
@inject NavigationManager NavigationManager

&lt;Card&gt;
    &lt;CardHeader&gt;
        &lt;HeadContent&gt;
            &lt;ModalTitle&gt;@EditingEntity.Name&lt;/ModalTitle&gt;
        &lt;/HeadContent&gt;
    &lt;/CardHeader&gt;
    &lt;CardBody&gt;
        &lt;Validations @ref=&quot;@EditValidationsRef&quot; Model=&quot;@EditingEntity&quot; ValidateOnLoad=&quot;false&quot;&gt;
            &lt;Validation MessageLocalizer=&quot;@LH.Localize&quot;&gt;
                &lt;Field&gt;
                    &lt;FieldLabel&gt;@L[&quot;Author&quot;]&lt;/FieldLabel&gt;
                    &lt;Select TValue=&quot;Guid&quot; @bind-SelectedValue=&quot;@EditingEntity.AuthorId&quot;&gt;
                        @foreach (var author in authorList)
                        {
                            &lt;SelectItem TValue=&quot;Guid&quot; Value=&quot;@author.Id&quot;&gt;
                                @author.Name
                            &lt;/SelectItem&gt;
                        }
                    &lt;/Select&gt;
                &lt;/Field&gt;
                &lt;Field&gt;
                    &lt;FieldLabel&gt;@L[&quot;Name&quot;]&lt;/FieldLabel&gt;
                    &lt;TextEdit @bind-Text=&quot;@EditingEntity.Name&quot;&gt;
                        &lt;Feedback&gt;
                            &lt;ValidationError /&gt;
                        &lt;/Feedback&gt;
                    &lt;/TextEdit&gt;
                &lt;/Field&gt;
            &lt;/Validation&gt;
            &lt;Field&gt;
                &lt;FieldLabel&gt;@L[&quot;Type&quot;]&lt;/FieldLabel&gt;
                &lt;Select TValue=&quot;BookType&quot; @bind-SelectedValue=&quot;@EditingEntity.Type&quot;&gt;
                    @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
                    {
                        &lt;SelectItem TValue=&quot;BookType&quot; Value=&quot;@((BookType) bookTypeValue)&quot;&gt;
                            @L[$&quot;Enum:BookType.{bookTypeValue}&quot;]
                        &lt;/SelectItem&gt;
                    }
                &lt;/Select&gt;
            &lt;/Field&gt;
            &lt;Field&gt;
                &lt;FieldLabel&gt;@L[&quot;PublishDate&quot;]&lt;/FieldLabel&gt;
                &lt;DateEdit TValue=&quot;DateTime&quot; @bind-Date=&quot;EditingEntity.PublishDate&quot; /&gt;
            &lt;/Field&gt;
            &lt;Field&gt;
                &lt;FieldLabel&gt;@L[&quot;Price&quot;]&lt;/FieldLabel&gt;
                &lt;NumericEdit TValue=&quot;float&quot; @bind-Value=&quot;EditingEntity.Price&quot; /&gt;
            &lt;/Field&gt;
        &lt;/Validations&gt;
    &lt;/CardBody&gt;
    &lt;CardFooter&gt;
        &lt;Button Color=&quot;Color.Secondary&quot; Type=&quot;ButtonType.Link&quot; To=&quot;books&quot;&gt;
            @L[&quot;Cancel&quot;]
        &lt;/Button&gt;
        &lt;Button Color=&quot;Color.Primary&quot;
                Type=&quot;@ButtonType.Submit&quot;
                PreventDefaultOnSubmit=&quot;true&quot;
                Clicked=&quot;UpdateEntityAsync&quot;&gt;
            @L[&quot;Save&quot;]
        &lt;/Button&gt;
    &lt;/CardFooter&gt;
&lt;/Card&gt;
</code></pre>
<ul>
<li><code>EditBook.razor.cs</code></li>
</ul>
<pre><code class="language-csharp">using Acme.BookStore.Books;
using Blazorise;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp;

namespace Acme.BookStore.Blazor.Pages;

public partial class EditBook
{
    protected CreateUpdateBookDto EditingEntity = new();
    protected Validations EditValidationsRef;
    IReadOnlyList&lt;AuthorLookupDto&gt; authorList = Array.Empty&lt;AuthorLookupDto&gt;();

    [Parameter]
    public string Id { get; set; }

    public Guid EditingEntityId { get; set; }

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();

        // Blazor can't parse Guid as route constraint currently.
        // See https://github.com/dotnet/aspnetcore/issues/19008
        EditingEntityId = Guid.Parse(Id);

        authorList = (await AppService.GetAuthorLookupAsync()).Items;

        if (!authorList.Any())
        {
            throw new UserFriendlyException(message: L[&quot;AnAuthorIsRequiredForCreatingBook&quot;]);
        }

        var entityDto = await AppService.GetAsync(EditingEntityId);

        EditingEntity = ObjectMapper.Map&lt;BookDto,CreateUpdateBookDto&gt;(entityDto);

        if (EditValidationsRef != null)
        {
            await EditValidationsRef.ClearAll();
        }
    }

    protected virtual async Task UpdateEntityAsync()
    {
        try
        {
            var validate = true;
            if (EditValidationsRef != null)
            {
                validate = await EditValidationsRef.ValidateAll();
            }
            if (validate)
            {
                await AppService.UpdateAsync(EditingEntityId, EditingEntity);

                NavigationManager.NavigateTo(&quot;books&quot;);
            }
        }
        catch (Exception ex)
        {
            await HandleErrorAsync(ex);
        }
    }
}
</code></pre>
<p>You can check the following commit for details:
https://github.com/abpframework/abp-samples/commit/aae61ad6d66ebf6191dd4dcfb4e23d30bd680a4e</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/bulk-operations-with-entity-framework-core-7.0-zvr01mtn</guid>
      <link>https://abp.io/community/posts/bulk-operations-with-entity-framework-core-7.0-zvr01mtn</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>entity-framework-core</category>
      <category>dotnet7</category>
      <title>Bulk Operations with Entity Framework Core 7.0</title>
      <description>With .NET 7, there are two new methods such as ExecuteUpdate and ExecuteDelete available to execute bulk operations.</description>
      <pubDate>Wed, 30 Nov 2022 06:34:18 Z</pubDate>
      <a10:updated>2026-09-26T09:29:36Z</a10:updated>
      <content:encoded><![CDATA[<h1>Bulk Operations with Entity Framework Core 7.0</h1>
<p>Entity Framework tracks all the entity changes and applies those changes to the database one by one when the <code>SaveChanges()</code> method is called. There was no way to execute bulk operations in Entity Framework Core without a dependency.</p>
<p>As you know the <a href="https://entityframework-extensions.net/bulk-savechanges">Entity Framework Extensions</a> library was doing it but it was not free.</p>
<p>There was no other solution until now. <a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates">Bulk Operations</a> are now available in Entity Framework Core with .NET 7.</p>
<p>With .NET 7, there are two new methods such as <code>ExecuteUpdate</code> and <code>ExecuteDelete</code> available to execute bulk operations. It's a similar usage with the Entity Framework Core Extensions library if you're familiar with it.</p>
<p>You can visit the microsoft example <a href="https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates">here</a> about how to use it.</p>
<p>It can be easily used with the DbContext.</p>
<pre><code class="language-csharp">await context.Tags.Where(t =&gt; t.Text.Contains(&quot;.NET&quot;)).ExecuteDeleteAsync();
</code></pre>
<h2>Using with ABP Framework</h2>
<p>ABP Framework provides an abstraction over database operations and implements generic repository pattern. So, DbContext can't be accessed outside of <a href="https://docs.abp.io/en/abp/latest/Repositories">repositories</a>.</p>
<p>You can use the <code>ExecuteUpdate</code> and <code>ExecuteDelete</code> methods inside a repository.</p>
<pre><code class="language-csharp">public class BookEntityFrameworkCoreRepository : EfCoreRepository&lt;BookStoreDbContext, Book, Guid&gt;, IBookRepository
{
    public BookEntityFrameworkCoreRepository(IDbContextProvider&lt;BookStoreDbContext&gt; dbContextProvider) : base(dbContextProvider)
    {
    }

    public async Task UpdateListingAsync()
    {
        var dbSet = await GetDbSetAsync();

        await dbSet
            .Where(x =&gt; x.IsListed &amp;&amp; x.PublishedOn.Year &lt;= 2022)
            .ExecuteUpdateAsync(s =&gt; s.SetProperty(x =&gt; x.IsListed, x =&gt; false));
    }

    public async Task DeleteOldBooksAsync()
    {
        var dbSet = await GetDbSetAsync();

        await dbSet
            .Where(x =&gt; x.PublishedOn.Year &lt;= 2000)
            .ExecuteDeleteAsync();
    }
}
</code></pre>
<p>There is no need to take an action for bulk inserting. You can use the <code>InsertManyAsync</code> method of the repository instead of creating a new method for it if you don't have custom logic. It'll use a new bulk inserting feature automatically since it's available in EF Core 7.0.</p>
<pre><code class="language-csharp">public class MyDomainService : DomainService
{
    protected IRepository&lt;Book, Guid&gt; BookRepository { get; }

    public MyDomainService(IRepository&lt;Book, Guid&gt; bookRepository)
    {
        BookRepository = bookRepository;
    }

    public async Task CreateBooksAsync(List&lt;Book&gt; books)
    {
        // It'll use bulk inserting automatically.
        await BookRepository.InsertManyAsync(books);
    }
}
</code></pre>
<blockquote>
<p>If you use <code>ExecuteDeleteAsync</code> or <code>ExecuteUpdateAsync</code>, then ABP's soft delete and auditing features can not work. Because these ABP features work with EF Core's change tracking system and these new methods doesn't work with the change tracking system. So, use them carefully.</p>
</blockquote>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/7f90b25a-c2af-2ac6-b18d-3a07d96b2096" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/7f90b25a-c2af-2ac6-b18d-3a07d96b2096" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/blazor-webassembly-asp.net-core-hosted-zbjvgrc9</guid>
      <link>https://abp.io/community/posts/blazor-webassembly-asp.net-core-hosted-zbjvgrc9</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>blazor-wasm</category>
      <title>Blazor WebAssembly Asp.NET Core Hosted</title>
      <description>This article shows how to host a Blazor WebAssembly project in a HttpApi.Host project and use a single unified project instead separated HttpApi.Host &amp; Blazor apps.</description>
      <pubDate>Fri, 19 Aug 2022 12:35:12 Z</pubDate>
      <a10:updated>2026-09-26T09:32:25Z</a10:updated>
      <content:encoded><![CDATA[<h1>Blazor WebAssembly Asp.NET Core Hosted</h1>
<p>Microsoft provides a template named Blazor WebAssembly Asp.NET Core Hosted. This template is an Asp.NET Core Razor Pages application that hosts a Blazor WebAssembly application. Basically, <code>HttpApi.Host</code> and <code>Blazor</code> applications are hosted together. In this case, only one application will be deployed and blazor application will be served by the HttpApi.Host.</p>
<h2>Instructions</h2>
<ul>
<li><p>Create a new ABP Application with Blazor UI</p>
<pre><code class="language-bash">abp new BookStore -u blazor -t app -v 6.0.0-rc.2 --no-random-port
</code></pre>
</li>
<li><p>Add Blazor project reference and <code>Microsoft.AspNetCore.Components.WebAssembly.Server</code> package reference to the HttpApi.Host project.</p>
<pre><code class="language-xml">&lt;ItemGroup&gt;
    &lt;ProjectReference Include=&quot;..\BookStore.Blazor\BookStore.Blazor.csproj&quot; /&gt;
    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Components.WebAssembly.Server&quot; Version=&quot;6.0.8&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
</li>
<li><p>Add Blazor framework files middleware into <strong>OnApplicationInitializaiton</strong> method in <code>BookStoreHttpApiHostModule.cs</code> file.</p>
<pre><code class="language-csharp">public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    // ...

    app.UseBlazorFrameworkFiles(); // 👈 Make sure it's before UseStaticFiles()

    app.UseStaticFiles();

    // ...
}
</code></pre>
</li>
<li><p>Add a mapping for fallback to index.html file at the end of the <strong>OnApplicationInitializaiton</strong> method in <code>BookStoreHttpApiHostModule.cs</code> file.</p>
<pre><code class="language-csharp">if (app is WebApplication webApp)
{
    webApp.MapFallbackToFile(&quot;index.html&quot;);
}
</code></pre>
</li>
<li><p>Configure your blazor SelfUrl as HttpApi.Host URL in <code>BookStore.Blazor/wwwroot/appsettings.json</code></p>
<pre><code class="language-json">{
    &quot;App&quot;: {
        &quot;SelfUrl&quot;: &quot;https://localhost:44305&quot;
    },
    &quot;AuthServer&quot;: {
        &quot;Authority&quot;: &quot;https://localhost:44305&quot;,
        &quot;ClientId&quot;: &quot;BookStore_Blazor&quot;,
        &quot;ResponseType&quot;: &quot;code&quot;
    },
    &quot;RemoteServices&quot;: {
        &quot;Default&quot;: {
        &quot;BaseUrl&quot;: &quot;https://localhost:44305&quot;
        }
    },
    &quot;AbpCli&quot;: {
        &quot;Bundle&quot;: {
        &quot;Mode&quot;: &quot;BundleAndMinify&quot;, /* Options: None, Bundle, BundleAndMinify */
        &quot;Name&quot;: &quot;global&quot;,
        &quot;Parameters&quot;: {

            }
        }
    }
}
</code></pre>
</li>
<li><p>Configure DbMigrator too. Navigate to <code>BookStore.DbMigrator/appsettings.json</code> and change Blazor URL to HttpApi.Host URL.</p>
<pre><code class="language-json">{
    &quot;ConnectionStrings&quot;: {
        &quot;Default&quot;: &quot;XXX&quot;
    },
    &quot;OpenIddict&quot;: {
        &quot;Applications&quot;: {
        &quot;BookStore_Blazor&quot;: {
            &quot;ClientId&quot;: &quot;BookStore_Blazor&quot;,
            &quot;RootUrl&quot;: &quot;https://localhost:44305&quot;
        },
        &quot;BookStore_Swagger&quot;: {
            &quot;ClientId&quot;: &quot;BookStore_Swagger&quot;,
            &quot;RootUrl&quot;: &quot;https://localhost:44305&quot;
        }
        }
    }
}
</code></pre>
</li>
<li><p>Run <code>BookStore.DbMigrator</code> once.</p>
</li>
<li><p>Remove <strong>HomeController.cs</strong> from HttpApi.Host project to prevent <code>/swagger</code> redirection.</p>
</li>
<li><p>Run only <code>BookStore.HttpApi.Host</code> project and see the result.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp-samples/master/BlazorWasmAspNetCoreHosted/blazor-aspnetcore-hosted-demo.gif" alt="blazor-aspnetcore-hosted-demo" /></p>
<p>As you can see, URL is <code>localhost:44305</code> for blazor application and login razor page. MVC application and Blazor WebAssembly works together. As you can see swagger UI is available at <code>localhost:44305/swagger</code> too.</p>
</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/using-abp-client-proxies-in-maui-with-openid-connect-em7x1s8k</guid>
      <link>https://abp.io/community/posts/using-abp-client-proxies-in-maui-with-openid-connect-em7x1s8k</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>maui</category>
      <category>openid-connect</category>
      <category>client-proxy</category>
      <title>Using ABP Client Proxies in MAUI with OpenID Connect</title>
      <description>The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an AbpModule then make able consuming API using ABP IAppServices.</description>
      <pubDate>Thu, 24 Feb 2022 11:40:59 Z</pubDate>
      <a10:updated>2026-09-26T09:14:33Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using ABP Client Proxies in MAUI with OpenID Connect</h1>
<p>The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an <strong>AbpModule</strong> then make able consuming API using ABP IAppServices.</p>
<p>Before we start, I offer my special thanks to <a href="https://github.com/hikalkan/maui-abp-playing">@hikalkan</a> because this repository ( <a href="https://github.com/hikalkan/maui-abp-playing">hikalkan/maui-abp-playing</a> ) is a fantastic inspiration for the purpose of this article.</p>
<h2>Getting Started</h2>
<p>In this article, we'll work on an application that was built on the previous article: <a href="https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf">Integrating MAUI Client via Using OpenID Connect</a>.</p>
<h2>Source Code</h2>
<p>Source code is available on GitHub:
<a href="https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId">abpframework/abp-samples/MAUI-OpenId</a></p>
<h2>Configuring ABP Core</h2>
<p>As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.</p>
<ul>
<li><p>Add the following dependencies to MAUI Client.</p>
<pre><code class="language-xml">&lt;PackageReference Include=&quot;Volo.Abp.Http.Client.IdentityModel&quot; Version=&quot;5.1.3&quot; /&gt;
&lt;PackageReference Include=&quot;Volo.Abp.Autofac&quot; Version=&quot;5.1.3&quot; /&gt;
</code></pre>
</li>
<li><p>Add HttpApi.Client project reference</p>
<pre><code class="language-xml">&lt;ProjectReference Include=&quot;..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj&quot; /&gt;
</code></pre>
<p>And run <code>abp build</code> command under MAUI application folder.</p>
<blockquote>
<p><code>abp build</code> command is equivalent of <code>dotnet build /graphBuild</code>, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.</p>
</blockquote>
</li>
<li><p>Create <strong>BookStoreMauiClientModule</strong>.</p>
<pre><code class="language-csharp">using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;

namespace Acme.BookStore.MauiClient;

[DependsOn(
    typeof(AbpAutofacModule),
    typeof(AbpHttpClientIdentityModelModule),
    typeof(BookStoreHttpApiClientModule)
    )]
public class BookStoreMauiClientModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();

        Configure&lt;OidcClientOptions&gt;(configuration.GetSection(&quot;Oidc:Options&quot;));

        context.Services.AddTransient&lt;OidcClient&gt;(sp =&gt;
        {
            var options = sp.GetRequiredService&lt;IOptions&lt;OidcClientOptions&gt;&gt;().Value;
            options.Browser = sp.GetRequiredService&lt;WebAuthenticatorBrowser&gt;();
            return new OidcClient(options);
        });

        context.Services.AddTransient&lt;HttpClient&gt;(sp =&gt;
            new HttpClient(sp.GetRequiredService&lt;AccessTokenHttpMessageHandler&gt;())
            {
                // Temporarily. We'll use ABP's Proxy for sendind requests.
                BaseAddress = new Uri(configuration.GetValue&lt;string&gt;(&quot;RemoteServices:Default:BaseUrl&quot;))
            });
    }
}
</code></pre>
</li>
<li><p>Mark all dependencies with interfaces for registering as services.</p>
<pre><code class="language-csharp">internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
</code></pre>
<pre><code class="language-csharp">public partial class MainPage : ContentPage, ITransientDependency
</code></pre>
<pre><code class="language-csharp">public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
</code></pre>
</li>
<li><p>Add <code>appsettings.json</code> file to root path of your application and mark it as <strong>Embedded resource</strong>.</p>
<pre><code class="language-json">{
    &quot;Oidc&quot;: {
        &quot;Options&quot;: {
            &quot;Authority&quot;: &quot;https://46fd-45-156-29-175.ngrok.io&quot;,
            &quot;ClientId&quot;: &quot;BookStore_Maui&quot;,
            &quot;RedirectUri&quot;: &quot;bookstore://&quot;,
            &quot;Scope&quot;: &quot;openid email profile role BookStore offline_access&quot;,
            &quot;ClientSecret&quot;: &quot;1q2w3E*&quot;
        }
    },
    &quot;RemoteServices&quot;: {
        &quot;Default&quot;: {
            &quot;BaseUrl&quot;: &quot;https://46fd-45-156-29-175.ngrok.io&quot;
        }
    }
}
</code></pre>
</li>
<li><p>Finally, Go back <code>MauiApplication.cs</code> and clear old codes and initialize ABP.</p>
<pre><code class="language-csharp">using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;

namespace Acme.BookStore.MauiClient;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =&gt;
        {

        });
        builder
            .UseMauiApp&lt;App&gt;()
            .ConfigureFonts(fonts =&gt;
            {
                fonts.AddFont(&quot;OpenSans-Regular.ttf&quot;, &quot;OpenSansRegular&quot;);
            });

        ConfigureConfiguration(builder);

        builder.Services.AddApplication&lt;BookStoreMauiClientModule&gt;(options =&gt;
        {
            options.Services.ReplaceConfiguration(builder.Configuration);
        });

        var app = builder.Build();

        app.Services.GetRequiredService&lt;IAbpApplicationWithExternalServiceProvider&gt;()
            .Initialize(app.Services);

        return app;
    }

    private static void ConfigureConfiguration(MauiAppBuilder builder)
    {
        var assembly = typeof(App).GetTypeInfo().Assembly;
        builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), &quot;appsettings.json&quot;, optional: false, false);
    }
}
</code></pre>
<p>Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.</p>
<h3>Switching to SecureStorage</h3>
<p>.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at <strong>MainPage.xaml.cs</strong></p>
<pre><code class="language-csharp">private async void OnLoginClicked(object sender, EventArgs e)
{
    var loginResult = await OidcClient.LoginAsync(new LoginRequest());
    if (loginResult.IsError)
    {
        await DisplayAlert(&quot;Error&quot;, loginResult.Error, &quot;Close&quot;);
        return;
    }

    await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
    await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
</code></pre>
<blockquote>
<p>Additionally, please configure each platform according to <a href="https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android">Secure Storage documentation</a></p>
</blockquote>
</li>
</ul>
<h2>Configuring Client Proxies</h2>
<p>ABP Client-Proxies don't use HttpClient directly. They use <code>IHttpClientFactory</code> to activate a new HttpClient instead of injecting it directly. So, we won't need <strong>AccessTokenHttpMessageHandler</strong> anymore. But still there is a way needed to set access token in requests. No worries, ABP has <code>IRemoteServiceHttpClientAuthenticator</code> to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.</p>
<ul>
<li><p>Remove <strong>AccessTokenHttpMessageHandler.cs</strong> from the project.</p>
</li>
<li><p>Add <strong>AccessTokenRemoteServiceHttpClientAuthenticator.cs</strong> instead.</p>
<pre><code class="language-csharp">using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;

namespace Acme.BookStore.MauiClient;

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
    protected OidcClient OidcClient { get; }

    public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
    {
        OidcClient = oidcClient;
    }

    public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
    {
        var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);

        if (!currentAccessToken.IsNullOrEmpty())
        {
            // TODO: Find better way to find if token is expired instead of parsing it.
            var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
            if (jwtToken.ValidTo &lt;= DateTime.UtcNow)
            {
                var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
                if (!refreshToken.IsNullOrEmpty())
                {
                    var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);

                    await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
                    await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);

                    context.Request.SetBearerToken(refreshResult.AccessToken);
                }
                else
                {
                    var loginResult = await OidcClient.LoginAsync(new LoginRequest());

                    await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
                    await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);

                    context.Request.SetBearerToken(loginResult.AccessToken);
                }
            }

            context.Request.SetBearerToken(currentAccessToken);
        }
    }
}
</code></pre>
</li>
<li><p>Now we are ready to inject IAppServices to communicate with backend.</p>
</li>
</ul>
<h2>Displaying Data in UI</h2>
<ul>
<li><p>Go back to <strong>Acme.BookStore.Domain</strong> project and add a simple data seed contributor to generate some example data for users.</p>
<pre><code class="language-csharp">public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
    protected IIdentityUserRepository repository;

    protected IGuidGenerator guidGenerator;
    public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
    {
        this.repository = repository;
        this.guidGenerator = guidGenerator;
    }

    public async Task SeedAsync(DataSeedContext context)
    {
        var count = await repository.GetCountAsync();
        if(count &lt;= 1) // Not sure 'admin' user was seeded before or not.
        {
            // All the names below were generated by https://www.name-generator.org.uk/quick/
            // The names does not represent real people.
            await repository.InsertManyAsync(new []{
                new IdentityUser(guidGenerator.Create(), &quot;john.doe&quot;, &quot;john.doe@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Zane.Frost&quot;, &quot;Zane.Frost@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Oscar.Landry&quot;, &quot;Oscar.Landry@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Yasemin.Roberts&quot;, &quot;Yasemin.Roberts@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Yasmine.Perez&quot;, &quot;Yasmine.Perez@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Tobi.Becker&quot;, &quot;Tobi.Becker@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Fox.Gilmore&quot;, &quot;Fox.Gilmore@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Benny.Burris&quot;, &quot;Benny.Burris@abp.io&quot;),
                new IdentityUser(guidGenerator.Create(), &quot;Chad.Camacho&quot;, &quot;Chad.Camacho@abp.io&quot;),
            });
        }
    }
}
</code></pre>
</li>
<li><p>Run the <strong>Acme.BookStore.DbMigrator</strong> project.</p>
</li>
<li><p>Turn back to MAUI app, and create a folder named <strong>ViewModels</strong> and add a simple <code>UsersViewModel.cs</code> under it.</p>
<pre><code class="language-csharp">public class UsersViewModel : BindableObject, ITransientDependency
{
    protected IIdentityUserAppService IdentityUserAppService { get; }

    public GetIdentityUsersInput Input { get; } = new();

    public ObservableCollection&lt;IdentityUserDto&gt; Items { get; } = new();

    public Command RefreshCommand { get; }

    private bool isBusy;
    public bool IsBusy { get =&gt; isBusy; set =&gt; SetProperty(ref isBusy, value); }

    public UsersViewModel(IIdentityUserAppService identityUserAppService)
    {
        IdentityUserAppService = identityUserAppService;
        GetUsersAsync();
        RefreshCommand = new Command(GetUsersAsync);
    }

    protected async void GetUsersAsync()
    {
        if (IsBusy)
        {
            return; // For preventing parallel request while searching.
        }

        IsBusy = true;

        Items.Clear();

        var result = await IdentityUserAppService.GetListAsync(Input);
        foreach (var user in result.Items)
        {
            Items.Add(user);
        }

        IsBusy = false;
    }

    protected void SetProperty&lt;T&gt;(ref T backField, T value, [CallerMemberName] string propertyName = null)
    {
        backField = value;
        OnPropertyChanged(propertyName);
    }
}
</code></pre>
</li>
<li><p>Create a folder named <strong>Pages</strong> and add a content page named <code>UsersPage</code>.</p>
<p><em>(Make sure you're adding MAUI Content Page)</em></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png" alt="net-maui-abp-contentpage-template" /></p>
</li>
<li><p>And inject <code>UsersViewModel</code> into it.</p>
<pre><code class="language-csharp">public partial class UsersPage : ContentPage, ITransientDependency
{
    public UsersViewModel ViewModel { get; }

    public UsersPage(UsersViewModel viewModel)
    {
        ViewModel = viewModel;
        InitializeComponent();
    }
}
</code></pre>
</li>
<li><p>And use that ViewModel in XAML design page.</p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
&lt;ContentPage xmlns=&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;
            xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
            x:Class=&quot;Acme.BookStore.MauiClient.UsersPage&quot;
            Title=&quot;UsersPage&quot;
            x:Name=&quot;page&quot;
            BindingContext=&quot;{Binding ViewModel, Source={x:Reference page}}&quot;&gt;
    &lt;StackLayout&gt;
        &lt;ListView 
            IsPullToRefreshEnabled=&quot;True&quot;
            ItemsSource=&quot;{Binding Items}&quot;
            IsRefreshing=&quot;{Binding IsBusy}&quot;
            RefreshCommand=&quot;{Binding RefreshCommand}&quot;&gt;
            &lt;ListView.Header&gt;
                &lt;SearchBar Text=&quot;{Binding Input.Filter}&quot; SearchCommand=&quot;{Binding RefreshCommand}&quot; /&gt;
            &lt;/ListView.Header&gt;
            &lt;ListView.ItemTemplate&gt;
                &lt;DataTemplate&gt;
                    &lt;TextCell 
                        Text=&quot;{Binding UserName, StringFormat='@{0}'}&quot;
                        Detail=&quot;{Binding Email}&quot;/&gt;
                &lt;/DataTemplate&gt;
            &lt;/ListView.ItemTemplate&gt;
        &lt;/ListView&gt;
    &lt;/StackLayout&gt;
&lt;/ContentPage&gt;
</code></pre>
<blockquote>
<p>I've used binding while setting <strong>BindingContext</strong> as <strong>ViewModel</strong> because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png" alt="abp-maui-demo-xaml-intellisense" /></p>
</blockquote>
</li>
</ul>
<p>After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.</p>
<ul>
<li><p>Add <code>Shell Pagae (MAUI)</code> to root of your application with name <strong>AppShell.xaml</strong>.</p>
<p><em>I've got some help for design of shell page from microsoft's articles.</em></p>
<p><em>Additionally, you might want to put <a href="https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg">abp_icon.svg</a> file under your <strong>Resources/Images</strong> folder.</em></p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
&lt;Shell x:Class=&quot;Acme.BookStore.MauiClient.AppShell&quot;
    xmlns=&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
    xmlns:local=&quot;clr-namespace:Acme.BookStore.MauiClient&quot;&gt;
    &lt;Shell.Resources&gt;
        &lt;ResourceDictionary&gt;
            &lt;Color x:Key=&quot;Primary&quot;&gt;#512BD4&lt;/Color&gt;
            &lt;Style x:Key=&quot;BaseStyle&quot; TargetType=&quot;Element&quot;&gt;
                &lt;Setter Property=&quot;Shell.BackgroundColor&quot; Value=&quot;{StaticResource Primary}&quot; /&gt;
                &lt;Setter Property=&quot;Shell.ForegroundColor&quot; Value=&quot;White&quot; /&gt;
                &lt;Setter Property=&quot;Shell.TitleColor&quot; Value=&quot;White&quot; /&gt;
                &lt;Setter Property=&quot;Shell.DisabledColor&quot; Value=&quot;#B4FFFFFF&quot; /&gt;
                &lt;Setter Property=&quot;Shell.UnselectedColor&quot; Value=&quot;#95FFFFFF&quot; /&gt;
                &lt;Setter Property=&quot;Shell.TabBarBackgroundColor&quot; Value=&quot;{StaticResource Primary}&quot; /&gt;
                &lt;Setter Property=&quot;Shell.TabBarForegroundColor&quot; Value=&quot;White&quot;/&gt;
                &lt;Setter Property=&quot;Shell.TabBarUnselectedColor&quot; Value=&quot;#95FFFFFF&quot;/&gt;
                &lt;Setter Property=&quot;Shell.TabBarTitleColor&quot; Value=&quot;White&quot;/&gt;
            &lt;/Style&gt;
            &lt;Style TargetType=&quot;TabBar&quot; BasedOn=&quot;{StaticResource BaseStyle}&quot; /&gt;
            &lt;Style TargetType=&quot;FlyoutItem&quot; BasedOn=&quot;{StaticResource BaseStyle}&quot; /&gt;
            &lt;Style Class=&quot;FlyoutItemLabelStyle&quot; TargetType=&quot;Label&quot;&gt;
                &lt;Setter Property=&quot;TextColor&quot; Value=&quot;White&quot;&gt;&lt;/Setter&gt;
                &lt;Setter Property=&quot;Margin&quot; Value=&quot;16&quot;&gt;&lt;/Setter&gt;
            &lt;/Style&gt;
            &lt;Style Class=&quot;FlyoutItemLayoutStyle&quot; TargetType=&quot;Layout&quot; ApplyToDerivedTypes=&quot;True&quot;&gt;
                &lt;Setter Property=&quot;VisualStateManager.VisualStateGroups&quot;&gt;
                    &lt;VisualStateGroupList&gt;
                        &lt;VisualStateGroup x:Name=&quot;CommonStates&quot;&gt;
                            &lt;VisualState x:Name=&quot;Normal&quot;&gt;
                                &lt;VisualState.Setters&gt;
                                    &lt;Setter Property=&quot;BackgroundColor&quot; Value=&quot;{x:OnPlatform UWP=Transparent, iOS=White, Android=White}&quot; /&gt;
                                    &lt;Setter TargetName=&quot;FlyoutItemLabel&quot; Property=&quot;Label.TextColor&quot; Value=&quot;{StaticResource Primary}&quot; /&gt;
                                &lt;/VisualState.Setters&gt;
                            &lt;/VisualState&gt;
                            &lt;VisualState x:Name=&quot;Selected&quot;&gt;
                                &lt;VisualState.Setters&gt;
                                    &lt;Setter Property=&quot;BackgroundColor&quot; Value=&quot;{StaticResource Primary}&quot; /&gt;
                                &lt;/VisualState.Setters&gt;
                            &lt;/VisualState&gt;
                        &lt;/VisualStateGroup&gt;
                    &lt;/VisualStateGroupList&gt;
                &lt;/Setter&gt;
            &lt;/Style&gt;

            &lt;Style Class=&quot;MenuItemLayoutStyle&quot; TargetType=&quot;Layout&quot; ApplyToDerivedTypes=&quot;True&quot;&gt;
                &lt;Setter Property=&quot;VisualStateManager.VisualStateGroups&quot;&gt;
                    &lt;VisualStateGroupList&gt;
                        &lt;VisualStateGroup x:Name=&quot;CommonStates&quot;&gt;
                            &lt;VisualState x:Name=&quot;Normal&quot;&gt;
                                &lt;VisualState.Setters&gt;
                                    &lt;Setter TargetName=&quot;FlyoutItemLabel&quot; Property=&quot;Label.TextColor&quot; Value=&quot;{StaticResource Primary}&quot; /&gt;
                                &lt;/VisualState.Setters&gt;
                            &lt;/VisualState&gt;
                        &lt;/VisualStateGroup&gt;
                    &lt;/VisualStateGroupList&gt;
                &lt;/Setter&gt;
            &lt;/Style&gt;
        &lt;/ResourceDictionary&gt;
    &lt;/Shell.Resources&gt;

    &lt;FlyoutItem Title=&quot;Home&quot;&gt;
        &lt;ShellContent ContentTemplate=&quot;{DataTemplate local:MainPage}&quot; Route=&quot;main&quot; /&gt;
    &lt;/FlyoutItem&gt;

    &lt;FlyoutItem Title=&quot;Users&quot;&gt;
        &lt;ShellContent ContentTemplate=&quot;{DataTemplate local:UsersPage}&quot; Route=&quot;UsersPage&quot; /&gt;
    &lt;/FlyoutItem&gt;

    &lt;Shell.FlyoutHeader&gt;
        &lt;StackLayout&gt;
            &lt;Image 
                Source=&quot;abp_logo.svg&quot;
                HorizontalOptions=&quot;Center&quot;
                Margin=&quot;25&quot;/&gt;
        &lt;/StackLayout&gt;
    &lt;/Shell.FlyoutHeader&gt;

&lt;/Shell&gt;
</code></pre>
</li>
<li><p>One more step is required. Go to <strong>App.xaml.cs</strong> and replace MainPage with AppShell.</p>
<pre><code class="language-csharp">public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        MainPage = new AppShell();
    }
}
</code></pre>
</li>
<li><p>Run the application.</p>
</li>
<li><p>Login once if you haven't done before.</p>
</li>
<li><p>Navigate to Users page with hamburger menu at the right top.</p>
</li>
</ul>
<p>| Android| iOS|
| --- | --- |
| <img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif" alt="abp-maui-android-appservice" /> | <img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif" alt="ios-abp-maui" /> |</p>
<p>| UWP |
| --- |
| <img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif" alt="abp-maui-uwp-appservice" /> |</p>
<p>| MacCatalyst |
| --- |
| <img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif" alt="abp-maui-MacCatalyst-appservice" /> |</p>
<h2>Conclusion</h2>
<p>ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/715d0855-9ec5-a22b-50ae-3a023db5a30f" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/715d0855-9ec5-a22b-50ae-3a023db5a30f" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf</guid>
      <link>https://abp.io/community/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>maui</category>
      <category>openid-connect</category>
      <title>Integrating MAUI Client via using OpenID Connect</title>
      <description>A demonstration for connecting ABP backend from a MAUI app via using OpenID connect. In this flow, a web browser will be opened when user tries to log in and user will perform login operation in the browser. Then IdentityServer will redirect user to application with login credentials (state, token etc.) will be handled by application.</description>
      <pubDate>Tue, 22 Feb 2022 05:51:47 Z</pubDate>
      <a10:updated>2026-09-26T07:31:03Z</a10:updated>
      <content:encoded><![CDATA[<h1>Integrating MAUI Client via using OpenID Connect</h1>
<p>This is a demonstration for connecting ABP backend from MAUI app via using openid connect.</p>
<p>In this flow, a web browser will be opened when user tries to log in and user will perform login operation in the browser. Then IdentityServer will redirect user to application with login credentials (state, token etc.) will be handled by application.</p>
<blockquote>
<p>This is by intent. The code flow does not allow the user to log in using a native view in the app. The reason being that this flow ensures that the username and password are never seen by the client (except the browser, which is part of the OS system - aka we trust it). You could enable using a native login view with the Resource Owner Password Credentials (ROPC) flow. But this is also an attack vector. Suppose someone makes a fraud duplicate of your application and tricking users into entering their credentials. The fraudulent app could store those credentials in-between. You just got to enjoy those tin-foil-hat moments when doing security. In other words, using the code flow does not give an attacker that opportunity and therefore is the recommended option for mobile clients.</p>
<ul>
<li><a href="https://mallibone.com/post/xamarin-oidc">@Mark Allibone</a></li>
</ul>
</blockquote>
<p>By the way, my motivation for building this sample is presenting just another way for authentication. <strong>Resource Owner Password Credentials</strong> authentication is already provided and it's more common way to do. This is yet another way to authenticate users.</p>
<h2>Source Code</h2>
<p>You can also find source code on GitHub in ABP-Samples.</p>
<ul>
<li><a href="https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId">abpframework/abp-samples/MAUI-OpenId</a></li>
</ul>
<h2>Creating projects</h2>
<ul>
<li>Create an ABP project without UI</li>
</ul>
<pre><code class="language-bash">abp new Acme.BookStore -t app --no-ui -d mongodb --no-random-ports
</code></pre>
<ul>
<li>Create a maui application</li>
</ul>
<pre><code class="language-bash">mkdir maui
cd maui
dotnet new maui -n Acme.BookStore.MauiClient
</code></pre>
<p>There is a long way for configuring scopes and callback urls for both server and clients. We'll use <a href="https://docs.microsoft.com/en-us/xamarin/essentials/web-authenticator?tabs=android">WebAuthenticator</a> to perform this operation.</p>
<h2>Configuring IdentityServer</h2>
<ul>
<li>Go to DbMigrator folder and MAUI client in <strong>appsettings.json</strong>. Add following client code in <strong>IdentityServer:Clients</strong> path:</li>
</ul>
<pre><code class="language-json">    &quot;BookStore_Maui&quot;: {
        &quot;ClientId&quot;: &quot;BookStore_Maui&quot;,
        &quot;ClientSecret&quot;: &quot;1q2w3e*&quot;,
        &quot;RootUrl&quot;: &quot;bookstore://&quot;
    }
</code></pre>
<ul>
<li>Go to <strong>IdentityServerDataSeedContributor</strong> in Domain project under IdentityServer folder. Append following code section into <strong>CreateClientsAsync()</strong> method.</li>
</ul>
<pre><code class="language-csharp">// Maui Client
var mauiClientId = configurationSection[&quot;BookStore_Maui:ClientId&quot;];
if (!mauiClientId.IsNullOrWhiteSpace())
{
    var mauiRootUrl = configurationSection[&quot;BookStore_Maui:RootUrl&quot;];

    await CreateClientAsync(
        name: mauiClientId,
        scopes: commonScopes,
        grantTypes: new[] { &quot;authorization_code&quot; },
        secret: configurationSection[&quot;BookStore_Maui:ClientSecret&quot;]?.Sha256(),
        requireClientSecret: false,
        redirectUri: $&quot;{mauiRootUrl}&quot;
    );
}
</code></pre>
<ul>
<li><p>Run DbMigrator</p>
</li>
<li><p>Then run HttpApi.Host</p>
</li>
</ul>
<h3>Configuring NGROK</h3>
<p>Client will check configuration from <code>/.well-known/openid-configuration</code> path and it must be a secured connection between client &amp; server. I prefer to use ngrok to open my backend app to entire web.</p>
<ul>
<li><p>Go to <a href="https://dashboard.ngrok.com/get-started/setup">getting started</a> page of ngrok <em>(login or register first)</em> and download the ngrok tool.</p>
</li>
<li><p>Don't forget to login from tool:</p>
<pre><code class="language-bash">ngrok authtoken XXX
</code></pre>
<p><em>A sample command is being displayed at dashboard where you download ngrok from</em></p>
</li>
<li><p>Open your HttpApi.Host with ngrok</p>
<pre><code class="language-bash">.\ngrok.exe http https://localhost:44350
</code></pre>
</li>
<li><p>You'll see a generated xxx.ngrok.io url. Navigate to <code>/.well-known/openid-configuration</code> to check if it's working right.</p>
<p>You should see something like that:
<img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png" alt="" />
Issuer must be your URL, not localhost! If you see still localhost, try to disable host header rewrite.</p>
</li>
<li><p>Also, ValidIssuers must be defined to validate tokens.</p>
<ul>
<li><p>Add <strong>ValidIssuers</strong> section to your <code>appsettings.json</code> of HttpApi.Host</p>
<pre><code class="language-js">&quot;AuthServer&quot;: {
    &quot;Authority&quot;: &quot;https://localhost:44350&quot;,
    &quot;RequireHttpsMetadata&quot;: &quot;false&quot;,
    &quot;SwaggerClientId&quot;: &quot;BookStore_Swagger&quot;,
    &quot;SwaggerClientSecret&quot;: &quot;1q2w3e*&quot;,
    &quot;ValidIssuers&quot;: [
        &quot;https://46fd-45-156-29-175.ngrok.io&quot;
    ]
},
</code></pre>
</li>
<li><p>Then define it in <strong>ConfigureAuthentication</strong> method in Module class</p>
<pre><code class="language-csharp">private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddAuthentication()
        .AddJwtBearer(options =&gt;
        {
            // ...
            options.TokenValidationParameters.ValidIssuers = configuration.GetSection(&quot;AuthServer:ValidIssuers&quot;).Get&lt;string[]&gt;();
        });
}
</code></pre>
</li>
</ul>
</li>
</ul>
<p>We're done with backend. Let's continue with MAUI app.</p>
<h2>Developing MAUI App</h2>
<p>Before we go, there is something to do like configuring dependency injection to get rid of unnecessary huge class coupling.</p>
<h3>Configuring Dependency Injection</h3>
<ul>
<li><p>Go to <strong>MauiApplication</strong> class and add <code>MainPage</code> in services.</p>
<pre><code class="language-csharp">public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp&lt;App&gt;()
        .ConfigureFonts(fonts =&gt;
        {
            fonts.AddFont(&quot;OpenSans-Regular.ttf&quot;, &quot;OpenSansRegular&quot;);
        });

    builder.Services.AddTransient&lt;MainPage&gt;();

    return builder.Build();
}
</code></pre>
</li>
<li><p>And inject MainPage from constructor in <strong>App.xaml.cs</strong></p>
<pre><code class="language-csharp">public App(MainPage mainPage)
{
    InitializeComponent();

    MainPage = mainPage;
}
</code></pre>
</li>
</ul>
<p>Now MainPage is ready for injecting dependencies to it.</p>
<h3>Configuring OIDC</h3>
<ul>
<li><p>Add <code>IdentityModel.OidcClient</code> package to project</p>
<pre><code class="language-xml">&lt;ItemGroup&gt;
    &lt;PackageReference Include=&quot;IdentityModel.OidcClient&quot; Version=&quot;5.0.0&quot; /&gt;
&lt;/ItemGroup&gt;
</code></pre>
</li>
<li><p>Create <strong>WebAuthenticatorBrowser</strong></p>
<pre><code class="language-csharp">internal class WebAuthenticatorBrowser : IBrowser
{
    public async Task&lt;BrowserResult&gt; InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
    {
        try
        {
            WebAuthenticatorResult authResult =
                await WebAuthenticator.AuthenticateAsync(new Uri(options.StartUrl), new Uri(options.EndUrl));
            var authorizeResponse = ToRawIdentityUrl(options.EndUrl, authResult);

            return new BrowserResult
            {
                Response = authorizeResponse
            };
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);
            return new BrowserResult()
            {
                ResultType = BrowserResultType.UnknownError,
                Error = ex.ToString()
            };
        }
    }

    public string ToRawIdentityUrl(string redirectUrl, WebAuthenticatorResult result)
    {
        IEnumerable&lt;string&gt; parameters = result.Properties.Select(pair =&gt; $&quot;{pair.Key}={pair.Value}&quot;);
        var values = string.Join(&quot;&amp;&quot;, parameters);

        return $&quot;{redirectUrl}#{values}&quot;;
    }
}
</code></pre>
</li>
<li><p>Configure <strong>OidcClient</strong> in <strong>MauiProgram</strong></p>
<pre><code class="language-csharp">builder.Services.AddTransient&lt;WebAuthenticatorBrowser&gt;();

builder.Services.AddTransient&lt;OidcClient&gt;(sp =&gt;
    new OidcClient(new OidcClientOptions
    {
        // Use your own ngrok url:
        Authority = &quot;https://46fd-45-156-29-175.ngrok.io&quot;,
        ClientId = &quot;BookStore_Maui&quot;,
        RedirectUri = &quot;bookstore://&quot;,
        Scope = &quot;openid email profile role BookStore&quot;,
        ClientSecret = &quot;1q2w3E*&quot;,
        Browser = sp.GetRequiredService&lt;WebAuthenticatorBrowser&gt;(),
    })
);
</code></pre>
</li>
<li><p>Go to <strong>MainPage.xaml</strong>, remove everyting and add a button for login</p>
<pre><code class="language-xml">&lt;ContentPage xmlns=&quot;http://schemas.microsoft.com/dotnet/2021/maui&quot;
            xmlns:x=&quot;http://schemas.microsoft.com/winfx/2009/xaml&quot;
            x:Class=&quot;MauiApp1.MainPage&quot;&gt;

    &lt;ScrollView&gt;
        &lt;Grid RowSpacing=&quot;25&quot; RowDefinitions=&quot;Auto,Auto,Auto,Auto,*&quot;
            Padding=&quot;{OnPlatform iOS='30,60,30,30', Default='30'}&quot;&gt;

            &lt;Button Text=&quot;Click to Log In&quot; Clicked=&quot;OnLoginClicked&quot; VerticalOptions=&quot;CenterAndExpand&quot; HorizontalOptions=&quot;Center&quot;/&gt;

        &lt;/Grid&gt;
    &lt;/ScrollView&gt;
&lt;/ContentPage&gt;
</code></pre>
</li>
<li><p>Inject <strong>OidcClient</strong> in <strong>MainPage.xaml.cs</strong> and make login operation.</p>
<pre><code class="language-csharp">using IdentityModel.OidcClient;

namespace Acme.BookStore.MauiClient;

public partial class MainPage : ContentPage
{
    protected OidcClient OidcClient { get; }

    public MainPage(OidcClient oidcClient)
    {
        InitializeComponent();
        OidcClient = oidcClient;
    }

    private async void OnLoginClicked(object sender, EventArgs e)
    {
        try
        {
            var loginResult = await OidcClient.LoginAsync(new LoginRequest());
            await DisplayAlert(&quot;Login Result&quot;, &quot;Access Token is:\n\n&quot; + loginResult.AccessToken, &quot;Close&quot;);

        }
        catch (Exception ex)
        {
            await DisplayAlert(&quot;Error&quot;, ex.ToString(), &quot;ok&quot;);
        }
    }
}
</code></pre>
</li>
</ul>
<p>It still won't work because there is something more to do for each platform. Check out the next step and configure the platforms that you're using.</p>
<h2>Platform Specific Configurations</h2>
<p>Each platform (UWP, OSX, iOS and Android) requires some configuration to use authentication from browser. In that step, we'll open a browser and user will login on the browser. After that, as you see in IdentityServer configurations, IdentityServer will redirect 'bookstore://' url that only contains scheme and that scheme is not http. Our application will handle that scheme and will be launched with parameters.</p>
<h3>Android</h3>
<ul>
<li><p>Start with creating a new Activity named <strong>BookStoreWebAuthenticatorCallbackActivity</strong></p>
<pre><code class="language-csharp">using Android.App;
using Android.Content;
using Android.Content.PM;

namespace Acme.BookStore.MauiClient.Platforms.Android;

[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView },
    Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
    DataScheme = CALLBACK_SCHEME)]
public class BookStoreWebAuthenticatorCallbackActivity : Microsoft.Maui.Essentials.WebAuthenticatorCallbackActivity
{
    const string CALLBACK_SCHEME = &quot;bookstore&quot;;
}
</code></pre>
</li>
<li><p>Add <code>CustomTabsService</code> to <strong>AndroidManifest.xml</strong> as below. <em>(queries tags only.)</em></p>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;&gt;
    &lt;uses-sdk android:minSdkVersion=&quot;21&quot; android:targetSdkVersion=&quot;30&quot; /&gt;
    &lt;application android:allowBackup=&quot;true&quot; android:icon=&quot;@mipmap/appicon&quot; android:roundIcon=&quot;@mipmap/appicon_round&quot; android:supportsRtl=&quot;true&quot;&gt;&lt;/application&gt;
    &lt;uses-permission android:name=&quot;android.permission.ACCESS_NETWORK_STATE&quot; /&gt;
&lt;queries&gt;
    &lt;intent&gt;
    &lt;action android:name=&quot;android.support.customtabs.action.CustomTabsService&quot; /&gt;
    &lt;/intent&gt;
&lt;/queries&gt;
&lt;/manifest&gt;
</code></pre>
<blockquote>
<p>For some reason, an error occurs with my emulator while targeting SDK 31, so I've changed the target SDK to 30.</p>
</blockquote>
</li>
<li><p>Run the Application and perform a login operation.
AccessToken will be retrieved.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif" alt="abp-maui-android-openid-connect" /></p>
</li>
</ul>
<hr />
<h3>iOS/MacCatalyst</h3>
<ul>
<li><p>Add following key to <strong>Info.plist</strong></p>
<pre><code class="language-xml">&lt;key&gt;CFBundleURLTypes&lt;/key&gt;
&lt;array&gt;
    &lt;dict&gt;
        &lt;key&gt;CFBundleURLName&lt;/key&gt;
        &lt;string&gt;mauiessentials&lt;/string&gt;
        &lt;key&gt;CFBundleURLSchemes&lt;/key&gt;
        &lt;array&gt;
            &lt;string&gt;bookstore&lt;/string&gt;
        &lt;/array&gt;
        &lt;key&gt;CFBundleTypeRole&lt;/key&gt;
        &lt;string&gt;Editor&lt;/string&gt;
    &lt;/dict&gt;
&lt;/array&gt;
</code></pre>
</li>
<li><p>Open <strong>AppDelegate</strong> class and override <code>OpenUrl</code> and <code>ContinueUserActivity</code> methods</p>
<pre><code class="language-csharp">public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
    if (Microsoft.Maui.Essentials.Platform.OpenUrl(app, url, options))
        return true;

    return base.OpenUrl(app, url, options);
}

public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
    if (Microsoft.Maui.Platform.ContinueUserActivity(application, userActivity, completionHandler))
        return true;
    return base.ContinueUserActivity(application, userActivity, completionHandler);
}
</code></pre>
</li>
<li><p>Make all steps for MacCatalyst, too.</p>
</li>
</ul>
<blockquote>
<p><strong>Tip:</strong> If your IDE struggles while displaying references and namespace suggestions, make sure you're displaying that file with iOS Target Framework.</p>
<p>You'll find it at the top of the editor.
<img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png" alt="abp-maui-example-compiler-select" /></p>
</blockquote>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif" alt="abp-maui-ios-openid-connect" /></p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif" alt="abp-maui-MacCatalyst-openid-connect" /></p>
<hr />
<h3>UWP (Windows)</h3>
<ul>
<li><p>Add following protocol extension in <code>Package.appxmanifest</code> file.</p>
<pre><code class="language-xml">&lt;Applications&gt;
    &lt;Application Id=&quot;App&quot;
    Executable=&quot;$targetnametoken$.exe&quot;
    EntryPoint=&quot;$targetentrypoint$&quot;&gt;
    &lt;Extensions&gt;
            &lt;uap:Extension Category=&quot;windows.protocol&quot;&gt;
            &lt;uap:Protocol Name=&quot;bookstore&quot;&gt;
                &lt;uap:DisplayName&gt;BookStore&lt;/uap:DisplayName&gt;
            &lt;/uap:Protocol&gt;
            &lt;/uap:Extension&gt;
        &lt;/Extensions&gt;
    &lt;/Application&gt;
&lt;/Applications&gt;
</code></pre>
<blockquote>
<p>Currently UWP has a bug in MAUI Essentials, I believe the MAUI team will solve it as soon as possible. You can track the status of issue:
https://github.com/dotnet/maui/issues/2702</p>
</blockquote>
</li>
<li><p>That's it on Windows side. Run the application.</p>
</li>
</ul>
<h2>Refreshing the access token</h2>
<p>IdentityServer doesn't return a refresh token by default. So we have to add <code>offline_access</code> to our scope while sending login request.</p>
<ul>
<li><p>Add <code>offline_access</code> to scope in <strong>MauiApplication.cs</strong> that we configured before.</p>
<pre><code class="language-csharp">builder.Services.AddTransient&lt;OidcClient&gt;(sp =&gt;
            new OidcClient(new OidcClientOptions
            {
                // Use your own ngrok url:
                Authority = &quot;https://46fd-45-156-29-175.ngrok.io&quot;,
                ClientId = &quot;BookStore_Maui&quot;,
                RedirectUri = &quot;bookstore://&quot;,
                Scope = &quot;openid email profile role BookStore offline_access&quot;, // &lt;-- Final state must be like this.
                ClientSecret = &quot;1q2w3E*&quot;,
                Browser = sp.GetRequiredService&lt;WebAuthenticatorBrowser&gt;(),
            })
        );
</code></pre>
</li>
<li><p>Then check if it's working or not in <strong>MainPage.xaml.cs</strong>. Update OnLoginClicked method as below</p>
<pre><code class="language-csharp">private async void OnLoginClicked(object sender, EventArgs e)
{
    try
    {
        var loginResult = await OidcClient.LoginAsync(new LoginRequest());
        await DisplayAlert(&quot;Login Result&quot;, &quot;Access Token is:\n\n&quot; + loginResult.AccessToken, &quot;Close&quot;);

        var refreshResult = await OidcClient.RefreshTokenAsync(loginResult.RefreshToken);
        await DisplayAlert(&quot;Refresh Result&quot;, &quot;New Access Token is: \n\n&quot; + refreshResult.AccessToken, &quot;Close&quot;);
    }
    catch (Exception ex)
    {
        await DisplayAlert(&quot;Error&quot;, ex.ToString(), &quot;ok&quot;);
    }
}
</code></pre>
</li>
</ul>
<h2>Storing the access token</h2>
<p>In this step we have to store access token &amp; refresh token for future requests.</p>
<blockquote>
<p><a href="https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android">Secure Storage</a> is highly recommended to store this kind of sensitive data. But it's not topic of this article. You can configure and use SecureStorage on your own. I'll go with <code>App Properties</code>.</p>
</blockquote>
<ul>
<li><p>Add following class to store key names instead of using magic strings in code.</p>
<pre><code class="language-csharp">namespace Acme.BookStore.MauiClient;

public static class OidcConsts
{
    internal const string AccessTokenKeyName = &quot;__access_token&quot;;
    internal const string RefreshTokenKeyName = &quot;__refresh_token&quot;;
}
</code></pre>
</li>
<li><p>Then go back to <strong>MainPage.xaml.cs</strong> and save our tokens after a successfull login.</p>
<pre><code class="language-csharp">private async void OnLoginClicked(object sender, EventArgs e)
{
    try
    {
        var loginResult = await OidcClient.LoginAsync(new LoginRequest());

        App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
        App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;

        await App.Current.SavePropertiesAsync();

        // Navigate to an inner page here.
    }
    catch (Exception ex)
    {
        await DisplayAlert(&quot;Error&quot;, ex.ToString(), &quot;ok&quot;);
    }
}
</code></pre>
</li>
<li><p>Add following <strong>AccessTokenHttpMessageHandler</strong> to append AccessToken to our requests &amp; refresh token when required.</p>
<pre><code class="language-csharp">public class AccessTokenHttpMessageHandler : DelegatingHandler
{
    protected OidcClient OidcClient { get; }

    public AccessTokenHttpMessageHandler(OidcClient oidcClient) : base(new HttpClientHandler())
    {
        OidcClient = oidcClient;
    }

    protected override async Task&lt;HttpResponseMessage&gt; SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (App.Current.Properties.TryGetValue(OidcConsts.AccessTokenKeyName, out object currentTokenValue) &amp;&amp; currentTokenValue != null)
        {
            request.SetBearerToken(currentTokenValue?.ToString());
            request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(&quot;application/json&quot;));
        }

        var response = await base.SendAsync(request, cancellationToken);

        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            if (App.Current.Properties.TryGetValue(OidcConsts.RefreshTokenKeyName, out object refreshTokenValue) &amp;&amp; refreshTokenValue != null)
            {
                var refreshResult = await OidcClient.RefreshTokenAsync(refreshTokenValue?.ToString());

                App.Current.Properties[OidcConsts.AccessTokenKeyName] = refreshResult.AccessToken;
                App.Current.Properties[OidcConsts.RefreshTokenKeyName] = refreshResult.RefreshToken;
                await App.Current.SavePropertiesAsync();

                request.SetBearerToken(refreshResult.AccessToken);

                return await base.SendAsync(request, cancellationToken);
            }
            else
            {
                var result = await OidcClient.LoginAsync(new LoginRequest());
                request.SetBearerToken(result.AccessToken);

                App.Current.Properties[OidcConsts.AccessTokenKeyName] = result.AccessToken;
                App.Current.Properties[OidcConsts.RefreshTokenKeyName] = result.RefreshToken;
                await App.Current.SavePropertiesAsync();
                request.SetBearerToken(result.AccessToken);

                return await base.SendAsync(request, cancellationToken);
            }
        }

        return response;
    }
}
</code></pre>
</li>
<li><p>Then register to dependency injection.</p>
<pre><code class="language-csharp">builder.Services.AddSingleton&lt;AccessTokenHttpMessageHandler&gt;();
builder.Services.AddTransient&lt;HttpClient&gt;(sp =&gt;
    new HttpClient(sp.GetRequiredService&lt;AccessTokenHttpMessageHandler&gt;())
    {
        BaseAddress = new Uri(&quot;https://46fd-45-156-29-175.ngrok.io&quot;)
    });
</code></pre>
</li>
<li><p>Now make we can send request to backend with authentication. Go to <strong>MainPage.xaml.cs</strong> and send a request right after authentication.</p>
<pre><code class="language-csharp">private async void OnLoginClicked(object sender, EventArgs e)
{
    try
    {
        var loginResult = await OidcClient.LoginAsync(new LoginRequest());

        App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
        App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;

        await App.Current.SavePropertiesAsync();

        var json = await httpClient.GetStringAsync(&quot;/api/identity/users&quot;);

        await DisplayAlert(&quot;/api/identity/users&quot;, json, &quot;close&quot;);
    }
    catch (Exception ex)
    {
        await DisplayAlert(&quot;Error&quot;, ex.ToString(), &quot;ok&quot;);
    }
}
</code></pre>
</li>
<li><p>Following result will be returned from API.</p>
  <img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png" height="480">
</li>
</ul>
<hr />
<h2>Recap</h2>
<p>The purpose of this article is connecting to ABP backend with access token and it's working properly.</p>
<p>I'm planning to integrate HttpApi.Client library of backend project instead of making requests manually as a second part of this article. I'll get inspired by <a href="https://github.com/hikalkan/maui-abp-playing">hikalkan/maui-abp-playing</a> repo to achieve that.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/e60c1b3a-786a-da4b-dfea-3a02322937a9" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/e60c1b3a-786a-da4b-dfea-3a02322937a9" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/using-autofilterer-with-abp-framework-uuqv81jm</guid>
      <link>https://abp.io/community/posts/using-autofilterer-with-abp-framework-uuqv81jm</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>AutoFilterer</category>
      <title>Using AutoFilterer with ABP Framework</title>
      <description>his article is about filtering data automatically without writing any LINQ via using AutoFilterer.</description>
      <pubDate>Wed, 16 Feb 2022 17:46:11 Z</pubDate>
      <a10:updated>2026-09-26T09:17:38Z</a10:updated>
      <content:encoded><![CDATA[<h1>Using AutoFilterer with ABP</h1>
<p>This article is about filtering data automatically without writing any LINQ via using AutoFilterer.</p>
<p><a href="https://github.com/enisn/AutoFilterer">AutoFilterer</a> is a mini filtering framework library for dotnet. The main purpose of the library is to generate LINQ expressions for Entities over DTOs automatically. Creating queries without writing any expression code is the most powerful feature that is provided. The first aim of AutoFilterer is to be compatible with Open API 3.0 Specifications, unlike oData &amp; GraphQL.</p>
<p><strong>Disclaimer:</strong> AutoFilterer is the one of my personal projects. It's not supported by ABP Framework or ABP Team officially. This article can be shown as self-promotion, so I needed to explain that.</p>
<h2>Initializing a New Project</h2>
<p>If you are familiar with application development with ABP Framework, you can skip to the next step <strong>&quot;Designing the Application.Contracts Layer&quot;</strong>.</p>
<ul>
<li>Create a new project:
<em>(I prefer mongodb as database provider to get rid of Ef migrations. You can go with Ef on your own.)</em></li>
</ul>
<pre><code class="language-bash">abp new Acme.BookStore -t app -d mongodb
</code></pre>
<ul>
<li>Create an entity named <strong>Book</strong></li>
</ul>
<pre><code class="language-csharp">using System;
using Volo.Abp.Domain.Entities.Auditing;

namespace Acme.BookStore.Books;

public class Book : FullAuditedAggregateRoot&lt;Guid&gt;
{
    public string Title { get; set; }
    public string Language { get; set; }
    public string Country { get; set; }
    public string Author { get; set; }
    public int TotalPage { get; set; }
    public int Year { get; set; }
    public string Link { get; set; }
}
</code></pre>
<ul>
<li>Add following property to <strong>BookStoreMongoDbContext</strong></li>
</ul>
<pre><code class="language-csharp">public IMongoCollection&lt;Book&gt; Books { get; set; }
</code></pre>
<ul>
<li>Create a DataSeedContributor
<ul>
<li>Add this <a href="https://github.com/enisn/abp-autofilterer-sample/blob/main/src/Acme.BookStore.Domain/Books/initial-books.json">initial-books.json</a> file to <code>Acme.BookStore.Domain/Books/</code> path and make build action as <strong>Embedded Resource</strong>.</li>
</ul>
</li>
</ul>
<pre><code class="language-csharp">using Newtonsoft.Json;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Books;

public class BookstoreDataSeederContributor : IDataSeedContributor, ITransientDependency
{
    protected readonly IRepository&lt;Book, Guid&gt; _repository;

    public BookstoreDataSeederContributor(IRepository&lt;Book, Guid&gt; repository)
    {
        _repository = repository;
    }

    public async Task SeedAsync(DataSeedContext context)
    {
        if (!await _repository.AnyAsync())
        {
            await _repository.InsertManyAsync(GetInitialBooks());
        }
    }

    private Book[] GetInitialBooks()
    {
        var json = GetEmbeddedResourceAsText(&quot;Acme.Bookstore.Books.initial-books.json&quot;);

        return JsonConvert.DeserializeObject&lt;Book[]&gt;(json);
    }

    private string GetEmbeddedResourceAsText(string nameWithNamespace)
    {
        using var stream = GetType().Assembly.GetManifestResourceStream(nameWithNamespace);

        return Encoding.UTF8.GetString(stream.GetAllBytes());
    }
}
</code></pre>
<ul>
<li>Run the <strong>DbMigrator</strong> and database with existing data is ready!</li>
</ul>
<hr />
<h2>Designing the Application.Contracts Layer</h2>
<p>In this section, We'll implement AutoFilterer package and use it for only filtering data. We'll leave <strong>Sorting</strong> and <strong>Paging</strong> to ABP Framework, because it already does it well and works with more than one UI compatible.</p>
<ul>
<li>Add <code>AutoFilterer</code> package to your <strong>Application.Contracts</strong> project.</li>
</ul>
<pre><code class="language-bash">dotnet add package AutoFilterer
</code></pre>
<ul>
<li><p>Let's start coding with creating DTOs.</p>
<ul>
<li>BookDto</li>
</ul>
<pre><code class="language-csharp">using System;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Books;

[Serializable]
public class BookDto : AuditedEntityDto&lt;Guid&gt;
{
    public string Title { get; set; }
    public string Language { get; set; }
    public string Country { get; set; }
    public string Author { get; set; }
    public int TotalPage { get; set; }
    public int Year { get; set; }
    public string Link { get; set; }
}
</code></pre>
<ul>
<li>BookGetListInput</li>
</ul>
<pre><code class="language-csharp">using AutoFilterer.Attributes;
using AutoFilterer.Enums;
using AutoFilterer.Types;
using System;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Books;

[Serializable]
// We'll leave Paging and Sorting to ABP, we'll use only filtering feature of AutoFilterer.
// So using FilterBase as a base class is enough.
public class BookGetListInput : FilterBase, IPagedAndSortedResultRequest
{
    // Configure 'Filter' property for built-in search boxes.
    [CompareTo(
        nameof(BookDto.Title),
        nameof(BookDto.Language),
        nameof(BookDto.Author),
        nameof(BookDto.Country)
        )]
    [StringFilterOptions(StringFilterOption.Contains)]
    public string Filter { get; set; }

    // IPagedAndSortedResultRequest implementation below.
    public int SkipCount { get; set; }

    public int MaxResultCount { get; set; }

    public string Sorting { get; set; }
}
</code></pre>
<ul>
<li>IBookAppService</li>
</ul>
<pre><code class="language-csharp">using System;
using Volo.Abp.Application.Services;

namespace Acme.BookStore.Books;

public interface IBookAppService : ICrudAppService&lt;BookDto, Guid, BookGetListInput&gt;
{
}
</code></pre>
</li>
</ul>
<hr />
<h2>Implementing Application Layer</h2>
<p>I prefer using <strong>CrudAppService</strong> to skip unrelated CRUD operations.</p>
<ul>
<li>Create <strong>BookAppService</strong> and apply AutoFilterer filtering to queryable via overriding <strong>CreateFilteredQueryAsync</strong>.</li>
</ul>
<pre><code class="language-csharp">using AutoFilterer.Extensions;
using System;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Books;

public class BookAppService : CrudAppService&lt;Book, BookDto, Guid, BookGetListInput&gt;
{
    public BookAppService(IRepository&lt;Book, Guid&gt; repository) : base(repository)
    {
    }

    protected override async Task&lt;IQueryable&lt;Book&gt;&gt; CreateFilteredQueryAsync(BookGetListInput input)
    {
        return (await base.CreateFilteredQueryAsync(input))
            .ApplyFilter(input);
    }
}
</code></pre>
<ul>
<li>Add following mapping in <strong>BookStoreApplicationAutoMapperProfile</strong></li>
</ul>
<pre><code class="language-csharp">CreateMap&lt;Book, BookDto&gt;().ReverseMap();
</code></pre>
<hr />
<h2>Displaying on UI</h2>
<p>Let's start with creating a page to show data list and filter it with a textbox.</p>
<ul>
<li>Create <code>Books/Index.cshtml</code> / <code>Books/Index.cshtml.cs</code></li>
</ul>
<pre><code class="language-cs">namespace Acme.BookStore.Web.Pages.Books;

public class IndexModel : BookStorePageModel
{
}
</code></pre>
<pre><code class="language-html">@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.Extensions.Localization

@model IndexModel

@inject IStringLocalizer&lt;BookStoreResource&gt; L

&lt;h2&gt;Books&lt;/h2&gt;

@section scripts
{
	&lt;abp-script src=&quot;/Pages/Books/index.js&quot; /&gt;
}

&lt;abp-card&gt;
	&lt;abp-card-header&gt;
		&lt;h2&gt;@L[&quot;Books&quot;]&lt;/h2&gt;
	&lt;/abp-card-header&gt;
	&lt;abp-card-body&gt;
		&lt;abp-table striped-rows=&quot;true&quot; id=&quot;BooksTable&quot;&gt;&lt;/abp-table&gt;
	&lt;/abp-card-body&gt;
&lt;/abp-card&gt;
</code></pre>
<ul>
<li>Create index.js in the same folder</li>
</ul>
<pre><code class="language-js">$(function () {
    var l = abp.localization.getResource('BookStore');

    var dataTable = $('#BooksTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, &quot;asc&quot;]],
            searching: true,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
            columnDefs: [
                {
                    title: l('Title'),
                    data: &quot;title&quot;
                },
                {
                    title: l('Language'),
                    data: &quot;language&quot;,
                },
                {
                    title: l('Country'),
                    data: &quot;country&quot;,
                },
                {
                    title: l('Author'),
                    data: &quot;author&quot;
                },
                {
                    title: l('TotalPage'),
                    data: &quot;totalPage&quot;,
                    render: function (data) {
                        return data + ' pages'
                    }
                },
                {
                    title: l('Year'),
                    data: &quot;year&quot;
                },
                {
                    title: l('Link'),
                    data: &quot;link&quot;,
                    render: function (data) {
                        return '&lt;a href=&quot;' + data + '&quot; target=&quot;_blank&quot;&gt;Link&lt;/a&gt;';
                    }
                },
            ]
        })
    );
});
</code></pre>
<ul>
<li>Run the project and see how it's working!</li>
</ul>
<p><img src="https://raw.githubusercontent.com/enisn/abp-autofilterer-sample/main/art/images/filter-preview.gif" alt="autofilterer-preview-with-abp" /></p>
<h2>Filtering Specific Properties</h2>
<p>AutoFilterer supports some different features like <a href="https://github.com/enisn/AutoFilterer/wiki/Working-with-Range">Filtering with Range</a>. Let's filter <strong>TotalPage</strong> and <strong>Year</strong> properties with range.</p>
<ul>
<li>Add following <code>TotalPage</code> and <code>Year</code> properties to <strong>BookGetListInput</strong>.</li>
</ul>
<pre><code class="language-csharp">public class BookGetListInput : FilterBase, IPagedAndSortedResultRequest
{
    // Configure 'Filter' property for built-in search boxes.
    [CompareTo(
        nameof(BookDto.Title),
        nameof(BookDto.Language),
        nameof(BookDto.Author),
        nameof(BookDto.Country)
        )]
    [StringFilterOptions(StringFilterOption.Contains)]
    public string Filter { get; set; }

    public Range&lt;int&gt; TotalPage { get; set; } // &lt;-- Add this one

    public Range&lt;int&gt; Year { get; set; } // &lt;-- and this

    // IPagedAndSortedResultRequest implementation below.
    public int SkipCount { get; set; }

    public int MaxResultCount { get; set; }

    public string Sorting { get; set; }
}
</code></pre>
<ul>
<li>Update <strong>Index.cshtml</strong> too</li>
</ul>
<pre><code class="language-html">@page
@using Acme.BookStore.Localization
@using Acme.BookStore.Web.Pages.Books
@using Microsoft.Extensions.Localization

@model IndexModel

@inject IStringLocalizer&lt;BookStoreResource&gt; L

&lt;h2&gt;Books&lt;/h2&gt;

@section scripts
{
	&lt;abp-script src=&quot;/Pages/Books/index.js&quot; /&gt;
}

&lt;abp-card&gt;
	&lt;abp-card-header&gt;
		&lt;h2&gt;@L[&quot;Books&quot;]&lt;/h2&gt;
	&lt;/abp-card-header&gt;
	&lt;abp-card-body&gt;
		&lt;div id=&quot;books-filter-wrapper&quot;&gt;
			&lt;div class=&quot;row&quot;&gt;

				&lt;div class=&quot;col-6&quot;&gt;
					&lt;label class=&quot;form-label&quot;&gt; TotalPage &lt;/label&gt;
					&lt;div class=&quot;row&quot;&gt;
						&lt;div class=&quot;col-6&quot;&gt;
							&lt;label class=&quot;form-label&quot;&gt;Min&lt;/label&gt;
							&lt;input id=&quot;TotalPageMin&quot; type=&quot;number&quot; class=&quot;form-control&quot; /&gt;
						&lt;/div&gt;
						&lt;div class=&quot;col-6&quot;&gt;
							&lt;label class=&quot;form-label&quot;&gt;Max&lt;/label&gt;
							&lt;input id=&quot;TotalPageMax&quot; type=&quot;number&quot; class=&quot;form-control&quot; /&gt;
						&lt;/div&gt;
					&lt;/div&gt;
				&lt;/div&gt;

				&lt;div class=&quot;col-6&quot;&gt;
					&lt;label class=&quot;form-label&quot;&gt; Year &lt;/label&gt;
					&lt;div class=&quot;row&quot;&gt;
						&lt;div class=&quot;col-6&quot;&gt;
							&lt;label class=&quot;form-label&quot;&gt;Min&lt;/label&gt;
							&lt;input id=&quot;YearMin&quot; type=&quot;number&quot; class=&quot;form-control&quot; /&gt;
						&lt;/div&gt;
						&lt;div class=&quot;col-6&quot;&gt;
							&lt;label class=&quot;form-label&quot;&gt;Max&lt;/label&gt;
							&lt;input id=&quot;YearMax&quot; type=&quot;number&quot; class=&quot;form-control&quot; /&gt;
						&lt;/div&gt;
					&lt;/div&gt;
				&lt;/div&gt;

			&lt;/div&gt;
		&lt;/div&gt;
		&lt;div class=&quot;mt-2&quot;&gt;
			&lt;abp-table striped-rows=&quot;true&quot; id=&quot;BooksTable&quot;&gt;&lt;/abp-table&gt;
		&lt;/div&gt;
	&lt;/abp-card-body&gt;
&lt;/abp-card&gt;
</code></pre>
<ul>
<li>Update <strong>index.js</strong> file to send those parameters to API</li>
</ul>
<pre><code class="language-js">$(function () {
    var l = abp.localization.getResource('BookStore');

    var getFilter = function () {
        return {
            totalPage: {
                min: $('#TotalPageMin').val(),
                max: $('#TotalPageMax').val()
            },
            year: {
                min: $('#YearMin').val(),
                max: $('#YearMax').val()
            }
        };
    };

    $(&quot;#books-filter-wrapper :input&quot;).on('input', function () {
        dataTable.ajax.reload();
    });

    var dataTable = $('#BooksTable').DataTable(
        abp.libs.datatables.normalizeConfiguration({
            serverSide: true,
            paging: true,
            order: [[1, &quot;asc&quot;]],
            searching: true,
            scrollX: true,
            ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList, getFilter),
            columnDefs: [
                {
                    title: l('Title'),
                    data: &quot;title&quot;
                },
                {
                    title: l('Language'),
                    data: &quot;language&quot;,
                },
                {
                    title: l('Country'),
                    data: &quot;country&quot;,
                },
                {
                    title: l('Author'),
                    data: &quot;author&quot;
                },
                {
                    title: l('TotalPage'),
                    data: &quot;totalPage&quot;,
                    render: function (data) {
                        return data + ' pages'
                    }
                },
                {
                    title: l('Year'),
                    data: &quot;year&quot;
                },
                {
                    title: l('Link'),
                    data: &quot;link&quot;,
                    render: function (data) {
                        return '&lt;a href=&quot;' + data + '&quot; target=&quot;_blank&quot;&gt;Link&lt;/a&gt;';
                    }
                },
            ]
        })
    );
});
</code></pre>
<ul>
<li>Run the Application and see the result</li>
</ul>
<p><img src="https://raw.githubusercontent.com/enisn/abp-autofilterer-sample/main/art/images/filter-range-preview.gif" alt="autofilterer-with-abp-range-filter" /></p>
<hr />
<h2>Source-Code</h2>
<p>You can find final version of this example on github.</p>
<ul>
<li><a href="https://github.com/enisn/abp-autofilterer-sample">enisn/abp-autofilterer-sample</a></li>
</ul>
<hr />
<h2>Discussion</h2>
<p>There is a couple of questions that you can think about. In this <em>&quot;Discussion&quot;</em> section I'll try to answer them.</p>
<h3>Is Defining Comparison in Dto ok?</h3>
<p>As a first impression, I tough, it's not ok because <code>Application.Contracts</code> can be shipped to clients. It's true, definition of filter is in DTO, but the implementation is in <code>Application</code> layer. So if a developer who implements client-side, can see something like below.</p>
<pre><code class="language-csharp">public class BookGetListInput : FilterBase, IPagedAndSortedResultRequest
{
    // Configure 'Filter' property for built-in search boxes.
    [CompareTo(
        nameof(BookDto.Title),
        nameof(BookDto.Language),
        nameof(BookDto.Author),
        nameof(BookDto.Country)
        )]
    [StringFilterOptions(StringFilterOption.Contains)]
    public string Filter { get; set; }
}
</code></pre>
<p>I think that's ok, because the developer'll understand what filtering does. It's kind of documentation with attributes.</p>
<h3>Why didn't use  Sorting &amp; Pagination feature of AutoFilterer</h3>
<p>ABP does those features well and UI frameworks(Razor Pages, Angular and Blazor) already implemented sorting and pagination with those existin parameters. Chaning cost is high because if you change, you'll need to implement them for each UI 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/using-cosmosdb-with-the-abp-framework-via-mongodb-api-x47bjeik</guid>
      <link>https://abp.io/community/posts/using-cosmosdb-with-the-abp-framework-via-mongodb-api-x47bjeik</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>CosmosDB</category>
      <title>Using CosmosDB with the ABP Framework via MongoDB API</title>
      <description>An example about how to use Cosmos DB with MongoDB Driver in ABP Framework </description>
      <pubDate>Mon, 22 Nov 2021 07:55:28 Z</pubDate>
      <a10:updated>2026-09-26T09:29:00Z</a10:updated>
      <content:encoded><![CDATA[<h1>How to use Cosmos DB in ABP?</h1>
<p>This is an example project to show how to use Cosmos DB in an ABP application. See the article that explains this project:</p>
<p><strong>https://abp.io/community/articles/using-cosmosdb-with-the-abp-framework-via-mongodb-api-x47bjeik</strong></p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/add54efe-1721-5c68-9d02-3a0058d1655a" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/add54efe-1721-5c68-9d02-3a0058d1655a" medium="image" />
    </item>
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/introducing-the-eshoponabp-project-9k1yvrp6</guid>
      <link>https://abp.io/community/posts/introducing-the-eshoponabp-project-9k1yvrp6</link>
      <a10:author>
        <a10:name>enisn</a10:name>
        <a10:uri>https://abp.io/community/members/enisn</a10:uri>
      </a10:author>
      <category>abp</category>
      <category>sample</category>
      <title>Introducing the eShopOnAbp Project</title>
      <description>Introducing a new example microservice solution built with the ABP Framework</description>
      <pubDate>Thu, 08 Jul 2021 07:08:39 Z</pubDate>
      <a10:updated>2026-09-26T06:10:43Z</a10:updated>
      <content:encoded><![CDATA[<p>We are happy to introduce the <strong>eShopOnAbp</strong> project as an example microservice solution built with the ABP Framework by the core ABP team. This solution demonstrates the strength of ABP Framework and using it in a real-life case. The goal of the project is to create a full-featured cloud-native microservices reference application. The project is inspired by the <a href="https://github.com/dotnet-architecture/eShopOnContainers">eShopOnContainers</a> project and shows how it can be implemented with the ABP Framework.</p>
<blockquote>
<p><strong>Project Status</strong>: Currently, the project doesn't have any business logic. We've just brought ABP's pre-built modules (Identity, Tenant Management, IdentityServer, etc) together as a base solution. However, it is fully working and you can now take it as a base solution for your microservice project. From now on, we will build the example application functionalities / business logic on top of it.</p>
</blockquote>
<h2>Source Code</h2>
<p>The source code is available on <a href="https://github.com/abpframework/eShopOnAbp">abpframework/eShopOnAbp</a> repository.</p>
<h2>The Big Picture</h2>
<p>The project follows micro-service architecture and overall structure is presented below.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2021-07-08-introducing-the-eshoponabp-project/9eade951e022722ac39439fd971f14d1.png" alt="overall-solution.png" /></p>
<h2>How to Run?</h2>
<p>You can either run in Visual Studio, or using <a href="https://github.com/dotnet/tye">Microsoft Tye</a>. Tye is a developer tool that makes developing, testing, and deploying micro-services and distributed applications easier.</p>
<h3>Requirements</h3>
<ul>
<li>.NET 5.0+</li>
<li>Docker</li>
<li>Yarn</li>
</ul>
<h3>Instructions</h3>
<ul>
<li><p>Clone the repository ( <a href="https://github.com/abpframework/eShopOnAbp">eShopOnAbp</a> )</p>
</li>
<li><p>Install Tye (<em>follow <a href="https://github.com/dotnet/tye/blob/main/docs/getting_started.md#installing-tye">these steps</a></em>)</p>
</li>
<li><p>Execute <code>run-tye.ps1</code></p>
</li>
<li><p>Wait until all applications are up!</p>
<ul>
<li>You can check running application from tye dashboard (<a href="http://127.0.0.1:8000/">localhost:8000</a>)</li>
</ul>
</li>
<li><p>After all your backend services are up, start the angular application:</p>
<pre><code class="language-bash">cd apps/angular
yarn start
</code></pre>
</li>
</ul>
<h2>What's Next?</h2>
<p>We'll work on deployment &amp; CI-CD processes as a next step and build the business logic on. First goal is deploying the entire application on <a href="https://kubernetes.io/">Kubernetes</a>.</p>
<h2>Feedback</h2>
<p>Your comments and suggestions is important for us. You can ask your questions or post your feedback under <a href="https://github.com/abpframework/abp/discussions/9536">this discussion entry</a>.</p>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/a16641b2-c838-84ca-b691-39fd971f4a9b" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/a16641b2-c838-84ca-b691-39fd971f4a9b" medium="image" />
    </item>
  </channel>
</rss>